├── .editorconfig ├── .env ├── .env.production ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.simple.yml ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── NOTES.md ├── README.cra.md ├── README.md ├── bin ├── cors-anywhere.js ├── expand-metadatas.js ├── generate-changelog.js ├── lib │ ├── record-http-mocks.js │ └── static-proxies.js ├── record-http-mocks.js └── static-proxies.js ├── common.js ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── about.spec.js │ ├── home.spec.js │ ├── index.spec.js │ ├── package.spec.js │ ├── qrcode.spec.js │ ├── search.spec.js │ └── searchResults.spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ ├── index.js │ └── precyrun.js ├── package-lock.json ├── package.json ├── public ├── apple-touch-icon-120x120.png ├── apple-touch-icon-180x180.png ├── cypress-screenshot-small.png ├── favicon-128x128.png ├── favicon-144x144.png ├── favicon-192x192.png ├── favicon-256x256.png ├── favicon-32x32.png ├── favicon-384x384.png ├── favicon-512x512.png ├── favicon-64x64.png ├── favicon.ico ├── index.html ├── manifest.json └── react-banner.png └── src ├── Routes.js ├── assets └── images │ ├── github-retina-white.png │ ├── github-retina.png │ ├── logo.svg │ ├── n-64-white.png │ ├── qrcode.png │ ├── twitter-retina-white.png │ └── twitter-retina.png ├── components ├── CodeBlock.js ├── Footer.js ├── Gravatar.js ├── Header.css ├── Header.js ├── KeywordsList.js ├── Loader.js ├── MainDrawer.js ├── MainLayout.js ├── Markdown.js ├── Package │ ├── DependenciesTab.js │ ├── InfosContents.js │ ├── NotFound.js │ ├── Package.js │ ├── PackageJsonTab.js │ ├── Readme.js │ ├── StatsContents.js │ ├── Title.js │ ├── VersionsTab.js │ ├── __tests__ │ │ └── NotFound.spec.js │ └── index.js ├── RetryButton.js ├── Search.js ├── SearchResults │ ├── SearchResultItem.js │ ├── SearchResults.js │ └── index.js ├── Sparkline.js ├── TwitterButton.js ├── Waiting.js ├── WindowInfos.js └── __tests__ │ ├── CodeBlock.spec.js │ ├── Footer.spec.js │ ├── Gravatar.spec.js │ ├── Header.spec.js │ ├── KeywordsList.spec.js │ ├── Markdown.spec.js │ ├── TwitterButton.spec.js │ ├── WindowInfos.spec.js │ └── __snapshots__ │ └── TwitterButton.spec.js.snap ├── containers ├── AboutContainer.js ├── HomeContainer.js ├── PackageContainer.js ├── QrcodeContainer.js ├── RootContainer.js ├── SearchContainer.js └── SearchResultsContainer.js ├── index.css ├── index.js ├── libs ├── @fnando │ └── sparkline │ │ ├── README.md │ │ ├── dist │ │ ├── sparkline.js │ │ ├── sparkline.js.map │ │ ├── sparkline.min.js │ │ └── sparkline.min.js.map │ │ ├── package.json │ │ └── src │ │ └── sparkline.js └── README.md ├── registerServiceWorker.js ├── services └── apis │ ├── Manager.js │ ├── README.md │ ├── apiManager.js │ ├── constants.js │ ├── httpClient.js │ ├── httpClientMock.js │ ├── index.js │ └── mocks │ ├── npmApi.fixtures.json │ ├── npmRegistry.fixtures.json │ └── npmsIo.fixtures.json ├── setupTests.js ├── testUtils.js └── utils ├── __tests__ ├── github.spec.js ├── lodash-package-registry.fixtures.json ├── metadatas.spec.js ├── npmApiHelpers.spec.js ├── react-downloads-api-last-month.fixtures.json ├── react-package-registry.fixtures.json └── url.spec.js ├── github.js ├── helpers.js ├── metadatas.js ├── npmApiHelpers.js ├── string.js ├── time.js └── url.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | # to match prettier defaults 6 | indent_style = space 7 | indent_size = 2 -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_NPM_REGISTRY_API_BASE_URL=/api/npm-registry 2 | REACT_APP_NPM_REGISTRY_API_TIMEOUT=10000 3 | REACT_APP_NPM_REGISTRY_API_CACHE_ENABLED=true 4 | REACT_APP_NPM_REGISTRY_API_MOCKS_ENABLED=false 5 | REACT_APP_NPM_API_BASE_URL=/api/npm-api 6 | REACT_APP_NPM_API_TIMEOUT=10000 7 | REACT_APP_NPM_API_CACHE_ENABLED=true 8 | REACT_APP_NPM_API_MOCKS_ENABLED=false 9 | REACT_APP_NPMS_IO_API_BASE_URL=/api/npms-io 10 | REACT_APP_NPMS_IO_API_TIMEOUT=10000 11 | REACT_APP_NPMS_IO_API_CACHE_ENABLED=true 12 | REACT_APP_NPMS_IO_API_MOCKS_ENABLED=false -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_NPM_REGISTRY_API_BASE_URL=https://topheman-apis-proxy.herokuapp.com/npm-registry 2 | REACT_APP_NPM_API_BASE_URL=https://api.npmjs.org 3 | REACT_APP_NPMS_IO_API_BASE_URL=https://api.npms.io 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/** 2 | /coverage/** 3 | /docs/** 4 | /tmp/** 5 | /src/libs/** -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "airbnb", "prettier"], 3 | "plugins": ["prettier", "cypress"], 4 | "rules": { 5 | "prettier/prettier": "error", 6 | "no-console": "off", 7 | "react/jsx-filename-extension": "off", 8 | "react/forbid-prop-types": [2, { "forbid": ["any"] }], 9 | "react/no-did-mount-set-state": "off", 10 | "import/prefer-default-export": "off", 11 | "prefer-template": "off", 12 | "no-underscore-dangle": ["error", { "allowAfterThis": true }], 13 | "global-require": "off", 14 | "import/no-extraneous-dependencies": [ 15 | "error", 16 | { 17 | "devDependencies": true, 18 | "optionalDependencies": false, 19 | "peerDependencies": false 20 | } 21 | ] 22 | }, 23 | "globals": { 24 | "cy": true 25 | }, 26 | "env": { 27 | "mocha": true, 28 | "cypress/globals": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | /cypress/videos 9 | /cypress/screenshots 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git 2 | package-lock.json 3 | package.json 4 | .eslintignore 5 | .gitignore 6 | .prettierignore 7 | .gitignore 8 | *.fixtures.json 9 | src/libs/** -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.travis.simple.yml: -------------------------------------------------------------------------------- 1 | # This file only handles CI (Continuous Integration) 2 | # If you need both CI and CD check out .travis.yml 3 | # Also take a look at: 4 | # * https://github.com/topheman/npm-registry-browser#continuous-integration-ci 5 | # * https://github.com/topheman/npm-registry-browser/blob/master/NOTES.md#continuous-deployment-with-travis 6 | sudo: false 7 | language: node_js 8 | node_js: 9 | - "8" 10 | install: npm install 11 | script: 12 | - npm run lint 13 | - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run test:travis || npm run test:travis:pr' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This file handles both CI and CD (Continuous Deployment) 2 | # If you don't need the deployment part, use .travis.simple.yml 3 | # Also take a look at: 4 | # * https://github.com/topheman/npm-registry-browser#continuous-integration-ci 5 | # * https://github.com/topheman/npm-registry-browser/blob/master/NOTES.md#continuous-deployment-with-travis 6 | sudo: false 7 | language: node_js 8 | node_js: 9 | - "8" 10 | addons: 11 | apt: 12 | packages: 13 | - libgconf-2-4 14 | jobs: 15 | include: 16 | - stage: Testing / Deploy (staging/production) 17 | install: npm install 18 | script: 19 | - npm run lint 20 | - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && npm run test:travis || npm run test:travis:pr' # don't record with cypress (don't have access to API key on PRs) 21 | before_deploy: 22 | - tar czvf build.tar.gz -C build . 23 | deploy: 24 | - provider: surge # Deploy to staging on each commit to master 25 | project: ./build/ 26 | domain: staging-npm-registry-browser.surge.sh 27 | skip_cleanup: true 28 | on: 29 | branch: master 30 | - provider: releases # Upload build artefacts to github releases on each tag on master 31 | api_key: 32 | secure: i7v1KFG/fviiPx2g6QlFo4+56CdEbqj2FRgtRldVVMHAHlLizNxfFATeidWND3W9ydSKNddpCFN6CIr2CPhuzAbRnMPwsU3r7YxGJEINFubvlbxIvVu6XPSiGOiSEmZQ0kLhN7AS9f9gYP0Hklg70cEKdDGIxhsNCrXcGkS3aKADDvr3sTOkjnFcYZkIgCl/kTFKA+7HomfBxfmzu/bvbhEmKfDRmDzWSuKRVlYo1wVPCdmA//wvK8xzCBQd3payrcMTDcH0O8rNBarnQDJ1HlZKOmwyRAcYyAZHjIgqTQi+6czDRPVMJOZDoBEphYvVYTkJIZo8lhKfRmTi6OTuL3LJvh9Drd1IIuNaCtlMpjy+em+wrM/HsBnRniNdqWqc5jyTYPCSGSEVRUawMVHNTtnwLwhraDiubqDOsHatngGbAV6ESygamIIQB3A/EjvGSlCLARJdX4oiEIfexjDc5F0xXbHx8HoTZc62wNwP0H22T20NLG6+9prcLeLF3s3u/VDS6te39fEGu/vLPGPfJXjmhJUfLr51v/6xKmqscsjHDNI4j/rv+y+vNtbJjjPUOmmJ40YgzlcLRc2AqVOmW/qVR+pmhp/8QNKuj2w2B/uxdttDgQDAhllMLCQ7+byBRsdhFBltWq0XdH3upmTkQxTFYETGZ2aWleUaeJMUbXI= 33 | file: "./build.tar.gz" 34 | skip_cleanup: true 35 | on: 36 | tags: true 37 | branch: master 38 | - provider: pages # Deploy to gh-pages on each tag on master 39 | skip_cleanup: true 40 | github_token: 41 | secure: i7v1KFG/fviiPx2g6QlFo4+56CdEbqj2FRgtRldVVMHAHlLizNxfFATeidWND3W9ydSKNddpCFN6CIr2CPhuzAbRnMPwsU3r7YxGJEINFubvlbxIvVu6XPSiGOiSEmZQ0kLhN7AS9f9gYP0Hklg70cEKdDGIxhsNCrXcGkS3aKADDvr3sTOkjnFcYZkIgCl/kTFKA+7HomfBxfmzu/bvbhEmKfDRmDzWSuKRVlYo1wVPCdmA//wvK8xzCBQd3payrcMTDcH0O8rNBarnQDJ1HlZKOmwyRAcYyAZHjIgqTQi+6czDRPVMJOZDoBEphYvVYTkJIZo8lhKfRmTi6OTuL3LJvh9Drd1IIuNaCtlMpjy+em+wrM/HsBnRniNdqWqc5jyTYPCSGSEVRUawMVHNTtnwLwhraDiubqDOsHatngGbAV6ESygamIIQB3A/EjvGSlCLARJdX4oiEIfexjDc5F0xXbHx8HoTZc62wNwP0H22T20NLG6+9prcLeLF3s3u/VDS6te39fEGu/vLPGPfJXjmhJUfLr51v/6xKmqscsjHDNI4j/rv+y+vNtbJjjPUOmmJ40YgzlcLRc2AqVOmW/qVR+pmhp/8QNKuj2w2B/uxdttDgQDAhllMLCQ7+byBRsdhFBltWq0XdH3upmTkQxTFYETGZ2aWleUaeJMUbXI= 42 | keep_history: true 43 | local_dir: build 44 | on: 45 | tags: true 46 | branch: master 47 | after_deploy: 48 | - curl https://staging-npm-registry-browser.surge.sh 49 | - curl https://topheman.github.io/npm-registry-browser/ 50 | - stage: Mocked version deployment 51 | if: tag IS present # skip this stage if not a tag 52 | install: npm install 53 | script: skip # don't re-run the tests 54 | before_deploy: 55 | - echo "Building Mock version - see https://github.com/topheman/npm-registry-browser#make-a-build-with-mocks" 56 | - npm run build:mock 57 | - tar czvf build.mock.tar.gz -C build . 58 | deploy: # Deploy mock version server on each tag on master 59 | - provider: surge 60 | project: ./build/ 61 | domain: mock-npm-registry-browser.surge.sh 62 | skip_cleanup: true 63 | on: 64 | tags: true 65 | branch: master 66 | - provider: releases 67 | api_key: 68 | secure: i7v1KFG/fviiPx2g6QlFo4+56CdEbqj2FRgtRldVVMHAHlLizNxfFATeidWND3W9ydSKNddpCFN6CIr2CPhuzAbRnMPwsU3r7YxGJEINFubvlbxIvVu6XPSiGOiSEmZQ0kLhN7AS9f9gYP0Hklg70cEKdDGIxhsNCrXcGkS3aKADDvr3sTOkjnFcYZkIgCl/kTFKA+7HomfBxfmzu/bvbhEmKfDRmDzWSuKRVlYo1wVPCdmA//wvK8xzCBQd3payrcMTDcH0O8rNBarnQDJ1HlZKOmwyRAcYyAZHjIgqTQi+6czDRPVMJOZDoBEphYvVYTkJIZo8lhKfRmTi6OTuL3LJvh9Drd1IIuNaCtlMpjy+em+wrM/HsBnRniNdqWqc5jyTYPCSGSEVRUawMVHNTtnwLwhraDiubqDOsHatngGbAV6ESygamIIQB3A/EjvGSlCLARJdX4oiEIfexjDc5F0xXbHx8HoTZc62wNwP0H22T20NLG6+9prcLeLF3s3u/VDS6te39fEGu/vLPGPfJXjmhJUfLr51v/6xKmqscsjHDNI4j/rv+y+vNtbJjjPUOmmJ40YgzlcLRc2AqVOmW/qVR+pmhp/8QNKuj2w2B/uxdttDgQDAhllMLCQ7+byBRsdhFBltWq0XdH3upmTkQxTFYETGZ2aWleUaeJMUbXI= 69 | file: "./build.mock.tar.gz" 70 | skip_cleanup: true 71 | on: 72 | tags: true 73 | branch: master 74 | after_deploy: 75 | - curl https://mock-npm-registry-browser.surge.sh 76 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Christophe Rosset 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /bin/cors-anywhere.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This file will open a server that will proxy ANY requests 5 | * and add CORS to the response headers. 6 | * 7 | * It is meant to be used for development purpose. 8 | */ 9 | 10 | // Listen on a specific host via the HOST environment variable 11 | const host = process.env.HOST || "0.0.0.0"; 12 | // Listen on a specific port via the PORT environment variable 13 | const port = process.env.PORT || 8000; 14 | 15 | const corsProxy = require("cors-anywhere"); // eslint-disable-line 16 | 17 | corsProxy 18 | .createServer({ 19 | originWhitelist: [] // Allow all origins 20 | }) 21 | .listen(port, host, () => { 22 | console.log(`Running CORS Anywhere on ${host}:${port}`); 23 | }); 24 | -------------------------------------------------------------------------------- /bin/expand-metadatas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script is meant to be required by react-scripts to inject infos as env vars on the fly. 3 | * It's an alternative to the DefinePlugin of webpack for create-react-app without ejecting. 4 | * 5 | * That way the vars you expose on process.env bellow will accessible as create-react-app env vars. 6 | * 7 | * Limitations: 8 | * - They can only be strings. 9 | * - This script is required before any env vars are set by react-scripts 10 | * -> you can't rely on NODE_ENV for example 11 | * 12 | * Example usage: "start": "react-scripts --require ./bin/expand-metadatas.js start" 13 | */ 14 | 15 | const { getBanner, getInfos } = require("../common"); 16 | 17 | const isMock = 18 | process.env.REACT_APP_NPM_REGISTRY_API_MOCKS_ENABLED === "true" || 19 | process.env.REACT_APP_NPM_API_MOCKS_ENABLED === "true"; 20 | 21 | process.env.REACT_APP_METADATAS_BANNER_HTML = getBanner( 22 | "formatted", 23 | isMock ? ["This is a mocked version", ""] : [] 24 | ); 25 | 26 | process.env.REACT_APP_METADATAS_VERSION = getInfos().pkg.version; 27 | -------------------------------------------------------------------------------- /bin/generate-changelog.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Simple util based on npm package generate-changelog 5 | * 6 | * Usage: ./generate-changelog.js previousTag currentTag 7 | */ 8 | 9 | const program = require("commander"); 10 | const packageJson = require("../package.json"); 11 | const githubUrlFromGit = require("github-url-from-git"); 12 | const generateChangelog = require("generate-changelog"); 13 | const { exec } = require("child_process"); 14 | 15 | const githubRepoUrl = 16 | (packageJson.repository && 17 | packageJson.repository.url && 18 | githubUrlFromGit(packageJson.repository.url)) || 19 | undefined; 20 | 21 | /** 22 | * generate-changelog always takes the latest tag as title / date 23 | * this patches this little bug and retrieves the exact date of the targetted tag 24 | */ 25 | const retrieveGitTagDate = tag => 26 | new Promise((res, rej) => { 27 | exec(`git show -s --format=%ci ${tag}^{commit}`, (error, stdout) => { 28 | if (error) { 29 | return rej(error); 30 | } 31 | return res(stdout); 32 | }); 33 | }); 34 | 35 | program 36 | .usage(" ") 37 | .command("* ") 38 | .action((previousTag, currentTag) => { 39 | const githubDiffLink = `Diff: ${githubRepoUrl}/compare/${previousTag}...${currentTag}`; 40 | 41 | Promise.all([ 42 | generateChangelog.generate({ 43 | tag: `${previousTag}...${currentTag}`, 44 | repoUrl: githubRepoUrl 45 | }), 46 | retrieveGitTagDate(currentTag) 47 | ]).then(([changelog, currentTagDate]) => { 48 | console.log( 49 | changelog.replace( 50 | /^(.*)$/m, 51 | `#### ${currentTag} (${currentTagDate.substr(0, 10)})` 52 | ) + githubDiffLink 53 | ); 54 | }); 55 | }); 56 | 57 | program.parse(process.argv); 58 | -------------------------------------------------------------------------------- /bin/lib/record-http-mocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inspired by https://github.com/topheman/react-es6-redux/blob/master/bin/nock/call-nock.js 3 | * 4 | * This script records the http requests (requests / response with body and headers) 5 | * so that they could be mocked in development mode or tests. 6 | * This task is automated, this way, you don't have to bother to do it manually ! ;-) 7 | */ 8 | 9 | const nock = require("nock"); // eslint-disable-line 10 | const axios = require("axios"); 11 | const fs = require("fs-extra"); // eslint-disable-line 12 | 13 | /** 14 | * 15 | * @param {String} target Will only be used for logging 16 | * @param {Object} config The shared config you would pass to your http client for all requests 17 | * @param {String} outputPath Filename where to write result (will be created if not exists) 18 | * @param {Array} requests Declarations of the requests like you would do with your http client 19 | * @return {Promise} 20 | */ 21 | function record({ target, config, outputPath, requests = [] }) { 22 | const httpClient = axios.create(); 23 | 24 | const makeRequests = (clientConfig = {}) => 25 | requests.map(options => { 26 | let appliedOptions = {}; 27 | if (typeof options === "string") { 28 | appliedOptions = { 29 | url: options, 30 | method: "get" 31 | }; 32 | } else { 33 | appliedOptions = { 34 | url: options.url, 35 | method: options.method || "get", 36 | ...options 37 | }; 38 | } 39 | console.log(`[nock ${target}] ${appliedOptions.url}`); 40 | // @warn the per-request options and the config-options are not deep-merged (should-it ?) 41 | return httpClient.request({ 42 | ...appliedOptions, 43 | ...clientConfig 44 | }); 45 | }); 46 | 47 | /** 48 | * Extract a map of the match RegExp (that will be used at runtime) 49 | * @param {Array} requestsList 50 | */ 51 | const makeMatchMap = requestsList => 52 | requestsList.reduce((acc, cur) => { 53 | if (cur.match) { 54 | acc[typeof cur === "string" ? cur : cur.url] = cur.match; 55 | } 56 | return acc; 57 | }, {}); 58 | 59 | // reset any current recording 60 | nock.recorder.clear(); 61 | 62 | // start recording 63 | nock.recorder.rec({ 64 | output_objects: true, 65 | enable_reqheaders_recording: true, 66 | dont_print: true 67 | }); 68 | 69 | console.log(`[nock ${target}] Start recording`); 70 | 71 | return Promise.all(makeRequests(config)) 72 | .then(() => { 73 | // once all request have passed, retrieve the recorded data 74 | const nockCallObjects = nock.recorder.play() || []; 75 | const matchMap = makeMatchMap(requests); 76 | const output = nockCallObjects.map(item => { 77 | const newItem = { ...item }; 78 | console.log(newItem.scope, newItem.path); 79 | // add the match attribute containing a regexp to be used at runtime 80 | if (matchMap[item.path]) { 81 | newItem.match = matchMap[item.path]; 82 | } 83 | return newItem; 84 | }); 85 | fs.ensureFileSync(outputPath); // make sur the file exists (create it if it doesn't) 86 | fs.writeFileSync(outputPath, JSON.stringify(output)); 87 | console.log(`[nock ${target}] file created at`, outputPath); 88 | nock.restore(); 89 | }) 90 | .catch(error => { 91 | nock.restore(); 92 | console.log(`[ERRORS ${target}]`, error.message); 93 | }); 94 | } 95 | 96 | /** 97 | * Prepares the config from an object to an array for recordAll 98 | * @param {Object} recordConfig 99 | * @return {Array} 100 | */ 101 | const makeConfig = recordConfig => 102 | Object.entries(recordConfig).map(([key, value]) => ({ 103 | target: key, 104 | ...value 105 | })); 106 | 107 | /** 108 | * Will record sequentially multiple tracks of apis 109 | * @param {Object} recordConfig 110 | */ 111 | async function recordAll(recordConfig) { 112 | try { 113 | const config = makeConfig(recordConfig); 114 | // eslint-disable-next-line 115 | for (const currentConf of config) { 116 | await record(currentConf); // eslint-disable-line 117 | } 118 | await "Success"; 119 | } catch (e) { 120 | await e; 121 | } 122 | } 123 | 124 | module.exports.record = record; 125 | module.exports.recordAll = recordAll; 126 | -------------------------------------------------------------------------------- /bin/lib/static-proxies.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); // eslint-disable-line 2 | const proxy = require("http-proxy-middleware"); // eslint-disable-line 3 | 4 | module.exports.createServer = ({ 5 | staticFolders = [], 6 | proxyConfig = {} 7 | } = {}) => { 8 | const app = express(); 9 | 10 | Object.entries(proxyConfig).forEach(([path, config]) => { 11 | console.log(`Creating proxy for ${path}`); 12 | app.use(path, proxy(config)); 13 | }); 14 | 15 | (staticFolders || []).forEach(folder => { 16 | console.log(`Serving static folder ${folder}`); 17 | app.use(express.static(folder)); 18 | }); 19 | 20 | return app; 21 | }; 22 | -------------------------------------------------------------------------------- /bin/record-http-mocks.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Inspired by https://github.com/topheman/react-es6-redux/blob/master/bin/nock/call-nock.js 5 | * 6 | * This script records the http requests (requests / response with body and headers) 7 | * so that they could be mocked in development mode or tests. 8 | * This task is automated, this way, you don't have to bother to do it manually ! ;-) 9 | */ 10 | 11 | // we are importing ESM modules from the front 12 | require = require("esm")(module); // eslint-disable-line 13 | require("dotenv").config({ path: ".env.production" }); // eslint-disable-line 14 | 15 | const path = require("path"); 16 | 17 | const { recordAll } = require("./lib/record-http-mocks"); 18 | const { 19 | TARGET_API_NPM_API, 20 | TARGET_API_NPM_REGISTRY, 21 | TARGET_API_NPMS_IO 22 | } = require("../src/services/apis/constants"); 23 | 24 | const recordConfig = { 25 | [TARGET_API_NPM_REGISTRY]: { 26 | config: { 27 | baseURL: process.env.REACT_APP_NPM_REGISTRY_API_BASE_URL, 28 | headers: { 29 | origin: "https://github.io" 30 | } 31 | }, 32 | requests: [ 33 | "/react", 34 | "/@angular%2Fcore", 35 | { 36 | url: "/-/v1/search?text=react", 37 | match: "/-/v1/search\\?text=react(\\.*)" 38 | }, 39 | { 40 | url: "/-/v1/search?text=%40angular", 41 | match: "/-/v1/search\\?text=%40angular(\\.*)" 42 | } 43 | ], 44 | outputPath: path.join( 45 | __dirname, 46 | "..", 47 | "src", 48 | "services", 49 | "apis", 50 | "mocks", 51 | `${TARGET_API_NPM_REGISTRY}.fixtures.json` 52 | ) 53 | }, 54 | [TARGET_API_NPM_API]: { 55 | config: { 56 | baseURL: process.env.REACT_APP_NPM_API_BASE_URL 57 | }, 58 | requests: [ 59 | "/downloads/range/last-year/react", 60 | "/downloads/range/last-year/@angular%2Fcore" 61 | ], 62 | outputPath: path.join( 63 | __dirname, 64 | "..", 65 | "src", 66 | "services", 67 | "apis", 68 | "mocks", 69 | `${TARGET_API_NPM_API}.fixtures.json` 70 | ) 71 | }, 72 | [TARGET_API_NPMS_IO]: { 73 | config: { 74 | baseURL: process.env.REACT_APP_NPMS_IO_API_BASE_URL 75 | }, 76 | requests: [ 77 | "/v2/search?q=react", 78 | "/v2/search?q=%40angular", 79 | { 80 | url: "/v2/search/suggestions?q=react", 81 | match: "/v2/search/suggestions\\?q=react(\\.*)" 82 | }, 83 | { 84 | url: "/v2/search/suggestions?q=%40angular", 85 | match: "/v2/search/suggestions\\?q=%40angular(\\.*)" 86 | } 87 | ], 88 | outputPath: path.join( 89 | __dirname, 90 | "..", 91 | "src", 92 | "services", 93 | "apis", 94 | "mocks", 95 | `${TARGET_API_NPMS_IO}.fixtures.json` 96 | ) 97 | } 98 | }; 99 | 100 | recordAll(recordConfig) 101 | .then(() => console.log("Recording success")) 102 | .catch(e => console.log("[ERROR]", e.message)); 103 | -------------------------------------------------------------------------------- /bin/static-proxies.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This file is meant to be used to test your build in local 5 | * with the apis proxied on the same domain (in order to be able to seamlessly test 6 | * on mobile devices via wifi) 7 | * 8 | * Make sure you build with the same proxy config as in dev 9 | */ 10 | const { createServer } = require("./lib/static-proxies"); 11 | const packageJson = require("../package.json"); 12 | 13 | // Listen on a specific host via the HOST environment variable 14 | const host = process.env.HOST || "0.0.0.0"; 15 | // Listen on a specific port via the PORT environment variable 16 | const port = process.env.PORT || 8000; 17 | 18 | createServer({ 19 | proxyConfig: packageJson.proxy, 20 | staticFolders: ["build"] 21 | }).listen(port, host, () => { 22 | console.log(`Running static-proxies on ${host}:${port}`); 23 | }); 24 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies,no-underscore-dangle */ 2 | 3 | /** 4 | * Inspired by https://github.com/topheman/webpack-babel-starter 5 | */ 6 | function getRootDir() { 7 | return __dirname; 8 | } 9 | 10 | function projectIsGitManaged() { 11 | const fs = require("fs"); 12 | const path = require("path"); 13 | try { 14 | // Query the entry 15 | const stats = fs.lstatSync(path.join(__dirname, ".git")); 16 | 17 | // Is it a directory? 18 | if (stats.isDirectory()) { 19 | return true; 20 | } 21 | return false; 22 | } catch (e) { 23 | return false; 24 | } 25 | } 26 | 27 | function _getUrlToCommit(pkg, gitRevisionLong) { 28 | let urlToCommit = null; 29 | // if no repository return null 30 | if (typeof pkg.repository === "undefined") { 31 | return urlToCommit; 32 | } 33 | // retrieve and reformat repo url from package.json 34 | if (typeof pkg.repository === "string") { 35 | urlToCommit = pkg.repository; 36 | } else if (typeof pkg.repository.url === "string") { 37 | urlToCommit = pkg.repository.url; 38 | } 39 | // check that there is a git repo specified in package.json & it is a github one 40 | if (urlToCommit !== null && /^https:\/\/github.com/.test(urlToCommit)) { 41 | urlToCommit = urlToCommit.replace(/.git$/, "/tree/" + gitRevisionLong); // remove the .git at the end 42 | } 43 | return urlToCommit; 44 | } 45 | 46 | function getInfos() { 47 | const gitActive = projectIsGitManaged(); 48 | const gitRev = require("git-rev-sync"); 49 | const moment = require("moment"); 50 | const pkg = require("./package.json"); 51 | const infos = { 52 | pkg, 53 | today: moment(new Date()).format(), 54 | year: new Date().toISOString().substr(0, 4), 55 | gitRevisionShort: gitActive ? gitRev.short() : null, 56 | gitRevisionLong: gitActive ? gitRev.long() : null, 57 | author: 58 | pkg.author && pkg.author.name ? pkg.author.name : pkg.author || null, 59 | urlToCommit: null 60 | }; 61 | infos.urlToCommit = gitActive 62 | ? _getUrlToCommit(pkg, infos.gitRevisionLong) 63 | : null; 64 | return infos; 65 | } 66 | 67 | /** 68 | * Called in default mode by webpack (will format it correctly in comments) 69 | * Called in formatted mode by gulp (for html comments) 70 | * @param {String} mode default/formatted 71 | * @param {Array} moreInfos 72 | * @returns {String} 73 | */ 74 | function getBanner(mode, moreInfos = []) { 75 | const infos = getInfos(); 76 | const compiled = [ 77 | "", 78 | "", 79 | infos.pkg.name, 80 | "", 81 | infos.pkg.description, 82 | "", 83 | `@version v${infos.pkg.version} - ${infos.today}`, 84 | (infos.gitRevisionShort !== null 85 | ? `@revision #${infos.gitRevisionShort}` 86 | : "") + (infos.urlToCommit !== null ? ` - ${infos.urlToCommit}` : ""), 87 | infos.author !== null ? `@author ${infos.author}` : "", 88 | `@copyright ${infos.year}(c)` + 89 | (infos.author !== null ? ` ${infos.author}` : ""), 90 | infos.pkg.license ? `@license ${infos.pkg.license}` : "", 91 | "" 92 | ] 93 | .concat(moreInfos) 94 | .join(mode === "formatted" ? "\n * " : "\n"); 95 | return compiled; 96 | } 97 | 98 | function getBannerHtml() { 99 | return "\n"; 100 | } 101 | 102 | module.exports.getRootDir = getRootDir; 103 | module.exports.getInfos = getInfos; 104 | module.exports.getBanner = getBanner; 105 | module.exports.getBannerHtml = getBannerHtml; 106 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:5000", 3 | "video": false, 4 | "chromeWebSecurity": false, 5 | "projectId": "8r5nf4", 6 | "testFiles": "**/index.spec.js" 7 | } 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/integration/about.spec.js: -------------------------------------------------------------------------------- 1 | describe("About", () => { 2 | it("direct load of /about page should show About", () => { 3 | cy.visit("/#/about"); 4 | cy.url().should("contain", "/about"); 5 | cy.getByText("About").should("be.visible"); 6 | }); 7 | it("back Home link should work from About page", () => { 8 | cy.visit("/#/about"); 9 | cy.getByTestId("link-back-home").click(); 10 | cy.hash().should("equal", "#/"); 11 | }); 12 | it("should load /about page from MainDrawer", () => { 13 | cy.visit("/#/"); 14 | cy.get("[aria-label=Menu]") 15 | .click() 16 | .getByTestId("link-to-about") 17 | .click(); 18 | cy.url().should("contain", "/about"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /cypress/integration/home.spec.js: -------------------------------------------------------------------------------- 1 | describe("Home", () => { 2 | beforeEach(() => { 3 | // make sure to start from a blank page for each test 4 | cy.visit("/#/"); 5 | }); 6 | it("should load Home page", () => { 7 | cy.contains("npm-registry-browser").should("have.prop", "title", "Home"); 8 | }); 9 | it("should redirect to latest version when click on chip with no version", () => { 10 | cy.getByTestId("chip-wrapper") 11 | .getByText(/^react$/) 12 | .click(); 13 | cy.url().should("match", /\/package\/react@(\d+)\.(\d+)\.(\d+)/); 14 | }); 15 | it("should redirect to specific version when click on chip with version", () => { 16 | cy.getByTestId("chip-wrapper") 17 | .getByText(/^react@0\.14\.0$/) 18 | .click(); 19 | cy.url().should("contain", "/package/react@0.14.0"); 20 | }); 21 | it("should open main drawer when click hamburger menu", () => { 22 | cy.get("[aria-label=Close]").should("not.be.visible"); 23 | cy.get("[aria-label=Menu]").click(); 24 | cy.get("[aria-label=Close]") 25 | .should("be.visible") 26 | .click() 27 | .should("not.be.visible"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /cypress/integration/index.spec.js: -------------------------------------------------------------------------------- 1 | import "./home.spec"; 2 | import "./package.spec"; 3 | import "./qrcode.spec"; 4 | import "./about.spec"; 5 | import "./search.spec"; 6 | import "./searchResults.spec"; 7 | -------------------------------------------------------------------------------- /cypress/integration/package.spec.js: -------------------------------------------------------------------------------- 1 | const VISIT_URL = "/#/package/react@16.3.0"; 2 | 3 | describe("Package", () => { 4 | it("should access specific version from url", () => { 5 | cy.visit(VISIT_URL); // direct visit 6 | cy.getByText("react@16.3.0").should("be.visible"); 7 | }); 8 | it("button should show version then become interactive", () => { 9 | cy.visit("/#/"); // preload 10 | cy.visit(VISIT_URL); // direct visit 11 | cy.get("button[data-testid=button-version-choice]:disabled") // the button should be disabled 12 | .getByText("react@16.3.0"); 13 | cy.get("button[data-testid=button-version-choice]").should( 14 | "not.be.disabled" 15 | ); // then become interactive 16 | }); 17 | it("should follow dependencies", () => { 18 | let matchedDependency; 19 | cy.visit(VISIT_URL); // direct visit 20 | cy.getByTestId("dependencies-tab") 21 | .getByText(/Dependencies \(/) 22 | .click(); // open tab 23 | cy.getByTestId("dependencies-tab") 24 | .find("a:first") 25 | .then(el => { 26 | matchedDependency = Cypress.$(el).text(); 27 | return el; 28 | }) 29 | .click() // click on first dependency 30 | .then(() => cy.url().should("contain", `/package/${matchedDependency}@`)); 31 | }); 32 | it("should follow versions", () => { 33 | cy.visit(VISIT_URL); // direct visit 34 | cy.getByTestId("versions-tab") 35 | .getByText(/Versions \(/) 36 | .click(); // open tab 37 | cy.getByTestId("versions-tab") 38 | .getByText("15.4.2") 39 | .click(); 40 | cy.url().should("contain", `/package/react@15.4.2`); 41 | }); 42 | it("should open package.json tab", () => { 43 | cy.visit(VISIT_URL); // direct visit 44 | cy.getByTestId("packageJson-tab") 45 | .getByText(/package.json \(/) 46 | .click() // open tab 47 | .get("[data-testid=packageJson-tab] pre") 48 | .should("be.visible"); 49 | }); 50 | it("should follow keywords", () => { 51 | let matchedKeyword; 52 | cy.visit(VISIT_URL); // direct visit 53 | cy.getByTestId("keywords-list") 54 | .find("a:first") 55 | .then(el => { 56 | matchedKeyword = Cypress.$(el).text(); 57 | console.log({ matchedKeyword }); 58 | return el; 59 | }) 60 | .click() // click on first keyword 61 | .then(() => 62 | cy.url().should(url => { 63 | expect(decodeURIComponent(url)).to.include( 64 | `/search?q=keywords:"${matchedKeyword}"` 65 | ); 66 | }) 67 | ); 68 | }); 69 | ["DESKTOP", "MOBILE"].forEach(platform => { 70 | it(`[${platform}] should access version from title ${ 71 | platform === "MOBILE" ? "drawer" : "popup" 72 | }`, () => { 73 | if (platform === "MOBILE") { 74 | cy.viewport("iphone-6"); 75 | } 76 | cy.visit(VISIT_URL); // direct visit 77 | cy.get("button[data-testid=button-version-choice]") // open list 78 | .click() 79 | .get( 80 | '[data-testid=list-version-choice] [href="#/package/react@15.4.2"]:first' 81 | ) // click on specific version 82 | .click({ force: true }) // @todo try to use .scrollIntoView 83 | .should("not.exist") // make sure it disappeared 84 | .url() 85 | .should("contain", "/package/react@15.4.2"); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /cypress/integration/qrcode.spec.js: -------------------------------------------------------------------------------- 1 | describe("Qrcode", () => { 2 | it("direct load of /qrcode page should show Qrcode ", () => { 3 | cy.visit("/#/qrcode"); 4 | cy.url().should("contain", "/qrcode"); 5 | cy.getByTestId("qrcode-standalone").should("be.visible"); 6 | }); 7 | it("back Home link should work from QrCode page", () => { 8 | cy.visit("/#/qrcode"); 9 | cy.getByTestId("link-back-home").click(); 10 | cy.hash().should("equal", "#/"); 11 | }); 12 | it("should load /qrcode page from MainDrawer", () => { 13 | cy.visit("/#/"); 14 | cy.get("[aria-label=Menu]") 15 | .click() 16 | .getByTestId("link-to-qrcode") 17 | .click(); 18 | cy.url().should("contain", "/qrcode"); 19 | cy.getByTestId("qrcode-standalone").should("be.visible"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/integration/search.spec.js: -------------------------------------------------------------------------------- 1 | describe("Search", () => { 2 | it("autocomplete should display results, be usable with keyboard", () => { 3 | cy.visit("/#/"); 4 | cy.getByTestId("search-input") 5 | .type("react") 6 | // can't test for search indicator (only shows up if search-input has focus) - it seems, we loose the focus by getting an other selector ... 7 | // .getByTestId("search-loading-indicator") 8 | // .should("not.contain", "error") 9 | // wait for the results (and make sure it's correct) 10 | .getByTestId("search-result-react") 11 | .contains("react") 12 | .getByTestId("search-input") 13 | .type("{downarrow}{enter}") 14 | // make sure we were redirected 15 | .url() 16 | .should("contain", "/#/package/react@"); 17 | }); 18 | ["DESKTOP", "MOBILE"].forEach(platform => { 19 | it(`[${platform}] autocomplete should display results, be usable with mouse`, () => { 20 | if (platform === "MOBILE") { 21 | cy.viewport("iphone-6"); 22 | } 23 | cy.visit("/#/"); 24 | cy.getByTestId("search-input") 25 | .click() 26 | .wait(0) // on mobile, the backdrop will be over, wait next tick 27 | .getByTestId("search-input") 28 | .type("redux", platform === "MOBILE" ? { force: true } : {}) // force: true necessary for `cypress run` ... 29 | .should("have.value", "redux") 30 | // can't test for search indicator (only shows up if search-input has focus) - it seams, we loose the focus by getting an other selector ... 31 | // .getByTestId("search-loading-indicator") 32 | // .should("not.contain", "error") 33 | // wait for the results (and make sure it's correct) 34 | .getByTestId("search-result-redux") 35 | .contains("redux") 36 | .click() 37 | // make sure we were redirected 38 | .url() 39 | .should("contain", "/#/package/redux@"); 40 | }); 41 | it(`[${platform}] search box should redirect to search results when hit enter`, () => { 42 | if (platform === "MOBILE") { 43 | cy.viewport("iphone-6"); 44 | } 45 | cy.visit("/#/"); 46 | cy.getByTestId("search-input") 47 | .click() 48 | .wait(0) // on mobile, the backdrop will be over, wait next tick 49 | .getByTestId("search-input") 50 | .type("react{enter}", platform === "MOBILE" ? { force: true } : {}) // force: true necessary for `cypress run` ... 51 | .url() 52 | .should("contain", "/#/search?q=react") 53 | .getByTestId("search-input") 54 | .should("have.value", "react"); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /cypress/integration/searchResults.spec.js: -------------------------------------------------------------------------------- 1 | describe("SearchResults", () => { 2 | it("search should keep history", () => { 3 | cy.visit("/#/") 4 | // search a package / assert it has results and that the url has changed 5 | .getByTestId("search-input") 6 | .clear() 7 | .type("react{enter}") 8 | .url() 9 | .should("contain", "/#/search?q=react") 10 | .getByTestId("search-result-item-react") 11 | .should("exist") 12 | // search a package / assert it has results and that the url has changed 13 | .getByTestId("search-input") 14 | .clear() 15 | .type("redux{enter}") 16 | .url() 17 | .should("contain", "/#/search?q=redux") 18 | .getByTestId("search-result-item-redux") 19 | .should("exist") 20 | // search a package / assert it has results and that the url has changed 21 | .getByTestId("search-input") 22 | .clear() 23 | .type("lodash{enter}") 24 | .url() 25 | .should("contain", "/#/search?q=lodash") 26 | .getByTestId("search-result-item-lodash") 27 | .should("exist") 28 | // go back / assert url changes + results and input search are in sync 29 | .go("back") 30 | .getByTestId("search-input") 31 | .should("have.value", "redux") 32 | .url() 33 | .should("contain", "/#/search?q=redux") 34 | // go back / assert url changes + results and input search are in sync 35 | .go("back") 36 | .getByTestId("search-input") 37 | .should("have.value", "react") 38 | .url() 39 | .should("contain", "/#/search?q=react"); 40 | }); 41 | it("search keywords and navigate history", () => { 42 | cy.visit("/#/") 43 | // search package click on some keyword 44 | .getByTestId("search-input") 45 | .clear() 46 | .type("react{enter}") 47 | // clicks on the first "react" keyword - assert input / url / result are in sync 48 | .getByTestId("keyword-react") 49 | .click() 50 | .getByTestId("search-result-item-react") 51 | .should("exist") 52 | .getByTestId("search-input") 53 | .should("have.value", 'keywords:"react"') 54 | .url() 55 | .then(url => { 56 | expect(decodeURIComponent(url)).to.include( 57 | '/#/search?q=keywords:"react"' 58 | ); 59 | }) 60 | // then try another keyword (the first "redux" one) - assert input / url / result are in sync 61 | .getByTestId("keyword-redux") 62 | // .should("be.visible") 63 | .click() 64 | .getByTestId("search-result-item-redux") 65 | .should("exist") 66 | .getByTestId("search-input") 67 | .should("have.value", 'keywords:"redux"') 68 | .url() 69 | .then(url => { 70 | expect(decodeURIComponent(url)).to.include( 71 | '/#/search?q=keywords:"redux"' 72 | ); 73 | }) 74 | // go back / assert assert input / url / result are in sync 75 | .go("back") 76 | .wait(0) // history.go('back') on front router -> cypress timeout is messed up (doesn't wait for next items to be ready) 77 | .getByTestId("search-input") 78 | .should("have.value", 'keywords:"react"') 79 | .getByTestId("search-result-item-react-redux") 80 | .should("exist") 81 | .url() 82 | .then(url => { 83 | expect(decodeURIComponent(url)).to.include( 84 | '/#/search?q=keywords:"react"' 85 | ); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (/* on , config */) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | }; 18 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef, arrow-body-style */ 2 | // *********************************************** 3 | // This example commands.js shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add("login", (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This is will overwrite an existing command -- 26 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 27 | 28 | /** 29 | * Call this before running a test suite to make sure you have the latest 30 | * version of the code. 31 | * 32 | * See: https://github.com/cypress-io/cypress/issues/702 33 | * @return {Promise} 34 | */ 35 | Cypress.Commands.add("clearSWCache", () => { 36 | return window.caches.keys().then(cacheNames => { 37 | return Promise.all( 38 | cacheNames.map(cacheName => { 39 | return window.caches.delete(cacheName); 40 | }) 41 | ).then(() => { 42 | console.info("Service Worker cache flushed"); 43 | }); 44 | }); 45 | }); 46 | Cypress.Commands.add("prepareTestRun", () => { 47 | // set from env var CYPRESS_LAUNCH_MODE 48 | if (Cypress.env("LAUNCH_MODE") === "debug-build") { 49 | cy.log("Debug build mode"); 50 | cy.log("⚠️ You are testing the production build"); 51 | cy.log( 52 | `Reminder: Any changes to app sources won't be reflected until you re-build` 53 | ); 54 | cy.log("Use npm run test:cypress:dev to develop your tests"); 55 | cy.log(""); 56 | } 57 | cy.log("🚿 clear ServiceWorker cache"); // can't put it in cy.clearSWCache() due to Promise management of Cypress ... 58 | cy.clearSWCache(); 59 | }); 60 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // cypress lacks support for some method in `cypress open` mode (not same chrome version as in `cypress run`) 17 | import "babel-polyfill"; 18 | 19 | // Import command from cypress-testing-library 20 | import "cypress-testing-library/add-commands"; 21 | 22 | // Import commands.js using ES2015 syntax: 23 | import "./commands"; 24 | 25 | // Alternatively you can use CommonJS syntax: 26 | // require('./commands') 27 | 28 | // Execute / log some task before doing anything, like flushing service worker cache 29 | describe("Prepare test run ...", () => { 30 | it("Starting ...", () => { 31 | cy.prepareTestRun(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /cypress/support/precyrun.js: -------------------------------------------------------------------------------- 1 | // This file is called on "precy:run" script 2 | console.log(` 3 | = Running all test files in the same spec ========================================================== 4 | 5 | Since cypress@3.x.x, each spec file runs in isolation. The tests took much longer to run. 6 | 7 | To speed them up, I decided to run all the files in the same spec. 8 | 9 | When creating a new spec file, update cypress/integration/index.spec.js, add your file. 10 | 11 | If you want to revert to the regular usage: 12 | - remove cypress/integration/index.spec.js 13 | - update cypress.json, remove the "testFiles" entry. 14 | 15 | ──────────────────────────────────────────────────────────────────────────────────────────────────── 16 | `); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "npm-registry-browser", 3 | "version": "1.0.2", 4 | "private": true, 5 | "description": "More than a simple todo-list, a React app with real-world constraints and full dev/build/test workflows.", 6 | "keywords": [ 7 | "React", 8 | "create-react-app", 9 | "jest", 10 | "enzyme", 11 | "cypress", 12 | "axios", 13 | "unit tests", 14 | "end to end tests" 15 | ], 16 | "author": "Christophe Rosset (http://labs.topheman.com/)", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/topheman/npm-registry-browser.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/topheman/npm-registry-browser/issues" 23 | }, 24 | "license": "MIT", 25 | "dependencies": { 26 | "@fnando/sparkline": "^0.3.10", 27 | "@material-ui/core": "^3.1.2", 28 | "@material-ui/icons": "^3.0.1", 29 | "animated-scrollto": "^1.1.0", 30 | "axios": "^0.18.0", 31 | "axios-cache-adapter": "^2.1.1", 32 | "axios-mock-adapter": "^1.15.0", 33 | "axios-retry": "^3.1.1", 34 | "classnames": "^2.2.6", 35 | "downshift": "^2.2.3", 36 | "highlight.js": "^9.12.0", 37 | "hoist-non-react-statics": "^3.0.1", 38 | "invariant": "^2.2.4", 39 | "js-md5": "^0.7.3", 40 | "mdn-polyfills": "^5.12.0", 41 | "prop-types": "^15.6.2", 42 | "react": "^16.5.2", 43 | "react-dom": "^16.5.2", 44 | "react-markdown": "^3.6.0", 45 | "react-router-dom": "^4.3.1", 46 | "react-scripts": "1.1.4", 47 | "recompose": "^0.30.0", 48 | "relative-date": "^1.1.3", 49 | "semver-match": "^0.1.1", 50 | "warning": "^4.0.2" 51 | }, 52 | "scripts": { 53 | "start": "react-scripts --require ./bin/expand-metadatas.js start", 54 | "predeploy": "npm run build", 55 | "deploy": "gh-pages -d build", 56 | "dev": "npm start", 57 | "dev:mock": "echo 'Mock mode' && cross-env REACT_APP_NPM_REGISTRY_API_MOCKS_ENABLED=true REACT_APP_NPM_API_MOCKS_ENABLED=true REACT_APP_NPMS_IO_API_MOCKS_ENABLED=true npm start", 58 | "build": "react-scripts --require ./bin/expand-metadatas.js build", 59 | "build:mock": "echo 'Mock mode' && cross-env REACT_APP_NPM_REGISTRY_API_MOCKS_ENABLED=true REACT_APP_NPM_API_MOCKS_ENABLED=true REACT_APP_NPMS_IO_API_MOCKS_ENABLED=true npm run build", 60 | "test": "npm-run-all --parallel --silent test:unit test:cypress", 61 | "test:travis": "npm-run-all --parallel --silent test:unit test:cypress:travis", 62 | "test:travis:pr": "npm test", 63 | "test:unit": "cross-env CI=true npm run test:unit:watch", 64 | "test:unit:watch": "react-scripts --require ./bin/expand-metadatas.js test --env=jsdom", 65 | "test:cypress": "npm run build && npm run cy:start-server-and-test", 66 | "test:cypress:travis": "npm run build && npm run cy:start-server-and-test:travis", 67 | "test:cypress:dev": "npm-run-all --parallel --race start 'cy:open -- --config baseUrl=http://localhost:3000'", 68 | "test:cypress:debug-build": "npm run build && cross-env CYPRESS_LAUNCH_MODE=debug-build npm-run-all --parallel --race serve cy:open", 69 | "cy:open": "cypress open", 70 | "precy:run": "node cypress/support/precyrun.js", 71 | "cy:run": "cypress run", 72 | "cy:run:travis": "npm run cy:run -- --record --config video=true", 73 | "cy:start-server-and-test": "npx start-server-and-test serve :5000 cy:run", 74 | "cy:start-server-and-test:travis": "npx start-server-and-test serve :5000 cy:run:travis", 75 | "test:precommit": "npm test", 76 | "serve": "npx serve --no-clipboard --listen 5000 build", 77 | "eject": "react-scripts eject", 78 | "lint": "npx eslint .", 79 | "pretty": "npx prettier --write '**/*.{js,jsx,json,css,scss}'", 80 | "proxy-apis-deprecated": "echo 'DEPRECATED' && node ./bin/cors-anywhere.js", 81 | "record-http-mocks": "cross-env REACT_APP_NPM_REGISTRY_API_BASE_URL=https://registry.npmjs.org node ./bin/record-http-mocks.js", 82 | "generate-changelog": "./bin/generate-changelog.js" 83 | }, 84 | "lint-staged": { 85 | "**/*.{js,jsx,json,css,scss}": [ 86 | "prettier --write", 87 | "git add" 88 | ] 89 | }, 90 | "husky": { 91 | "hooks": { 92 | "pre-commit": "lint-staged && npm run lint && npm run test:precommit" 93 | } 94 | }, 95 | "devDependencies": { 96 | "babel-polyfill": "^6.26.0", 97 | "commander": "^2.18.0", 98 | "cors-anywhere": "^0.4.1", 99 | "cross-env": "^5.2.0", 100 | "cypress": "^3.1.0", 101 | "cypress-testing-library": "^2.2.2", 102 | "dom-testing-library": "^3.8.1", 103 | "enzyme": "^3.6.0", 104 | "enzyme-adapter-react-16": "^1.5.0", 105 | "enzyme-to-json": "^3.3.4", 106 | "eslint-config-airbnb": "^15.1.0", 107 | "eslint-config-prettier": "^2.10.0", 108 | "eslint-plugin-cypress": "^2.0.1", 109 | "eslint-plugin-prettier": "^2.7.0", 110 | "esm": "^3.0.84", 111 | "express": "^4.16.3", 112 | "fs-extra": "^5.0.0", 113 | "generate-changelog": "^1.7.1", 114 | "gh-pages": "^2.0.0", 115 | "git-rev-sync": "^1.12.0", 116 | "github-url-from-git": "^1.5.0", 117 | "http-proxy-middleware": "^0.19.0", 118 | "husky": "^1.1.0", 119 | "jest-dom": "^1.12.1", 120 | "lint-staged": "^7.3.0", 121 | "moment": "^2.22.2", 122 | "nock": "^9.6.1", 123 | "npm-run-all": "^4.1.5", 124 | "prettier": "1.19.1", 125 | "react-testing-library": "^5.1.0", 126 | "serve": "^10.0.2", 127 | "start-server-and-test": "^1.7.13" 128 | }, 129 | "proxy": { 130 | "/api/npm-registry": { 131 | "target": "https://registry.npmjs.org", 132 | "changeOrigin": true, 133 | "pathRewrite": { 134 | "^/api/npm-registry": "" 135 | } 136 | }, 137 | "/api/npm-api": { 138 | "target": "https://api.npmjs.org", 139 | "changeOrigin": true, 140 | "pathRewrite": { 141 | "^/api/npm-api": "" 142 | } 143 | }, 144 | "/api/npms-io": { 145 | "target": "https://api.npms.io", 146 | "changeOrigin": true, 147 | "pathRewrite": { 148 | "^/api/npms-io": "" 149 | } 150 | } 151 | }, 152 | "homepage": "." 153 | } 154 | -------------------------------------------------------------------------------- /public/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/cypress-screenshot-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/cypress-screenshot-small.png -------------------------------------------------------------------------------- /public/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-128x128.png -------------------------------------------------------------------------------- /public/favicon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-144x144.png -------------------------------------------------------------------------------- /public/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-192x192.png -------------------------------------------------------------------------------- /public/favicon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-256x256.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-384x384.png -------------------------------------------------------------------------------- /public/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-512x512.png -------------------------------------------------------------------------------- /public/favicon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon-64x64.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 31 | npm-registry-browser 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 54 | 55 | 56 | 57 | 60 |
61 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "npm-registry-browser", 3 | "name": "npm-registry-browser", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "favicon-64x64.png", 12 | "sizes": "64x64", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "favicon-128x128.png", 17 | "sizes": "128x128", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "favicon-192x192.png", 22 | "sizes": "192x192", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "favicon-192x192.png", 27 | "sizes": "192x192", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "favicon-256x256.png", 32 | "sizes": "256x256", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "favicon-384x384.png", 37 | "sizes": "384x384", 38 | "type": "image/png" 39 | }, 40 | { 41 | "src": "favicon-512x512.png", 42 | "sizes": "512x512", 43 | "type": "image/png" 44 | } 45 | ], 46 | "start_url": "./index.html", 47 | "display": "standalone", 48 | "theme_color": "#900000", 49 | "background_color": "#ffffff" 50 | } 51 | -------------------------------------------------------------------------------- /public/react-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/public/react-banner.png -------------------------------------------------------------------------------- /src/Routes.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { HashRouter, Route, Switch, Redirect } from "react-router-dom"; 3 | 4 | import MainLayout from "./components/MainLayout"; 5 | 6 | // Containers that will be loaded by the router 7 | import HomeContainer from "./containers/HomeContainer"; 8 | import PackageContainer from "./containers/PackageContainer"; 9 | import SearchResultsContainer from "./containers/SearchResultsContainer"; 10 | import QrcodeContainer from "./containers/QrcodeContainer"; 11 | import AboutContainer from "./containers/AboutContainer"; 12 | 13 | /** 14 | * Compiles a render method to pass to a Route that will redirect to "latest" version 15 | * Examples: 16 | * - /package/react -> package/react@latest 17 | * - /package/@angular/core -> /package/@angular/core@latest 18 | * The Package container will handle it from their (there is always a "latest" dist tag to match) 19 | * @param {Boolean} scoped 20 | */ 21 | const compileRedirectToLatest = scoped => ( 22 | { match: { params } } // eslint-disable-line 23 | ) => ( 24 | 32 | ); 33 | 34 | const Routes = () => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 51 | 56 | 61 | 62 | 63 | 64 | 65 | ); 66 | 67 | export default Routes; 68 | -------------------------------------------------------------------------------- /src/assets/images/github-retina-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/src/assets/images/github-retina-white.png -------------------------------------------------------------------------------- /src/assets/images/github-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/src/assets/images/github-retina.png -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/images/n-64-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/src/assets/images/n-64-white.png -------------------------------------------------------------------------------- /src/assets/images/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/src/assets/images/qrcode.png -------------------------------------------------------------------------------- /src/assets/images/twitter-retina-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/src/assets/images/twitter-retina-white.png -------------------------------------------------------------------------------- /src/assets/images/twitter-retina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topheman/npm-registry-browser/4fe3cef396cd6a0d5fbecb3ddecc3952be30251c/src/assets/images/twitter-retina.png -------------------------------------------------------------------------------- /src/components/CodeBlock.js: -------------------------------------------------------------------------------- 1 | // inspired by https://github.com/rexxars/react-markdown/blob/cf194cec7e016b3fec185adfd568eec28d9787c0/demo/src/code-block.js 2 | 3 | import React, { PureComponent } from "react"; 4 | import PropTypes from "prop-types"; 5 | import { highlightBlock } from "highlight.js"; 6 | 7 | class CodeBlock extends PureComponent { 8 | componentDidMount() { 9 | this.highlightCode(); 10 | } 11 | componentDidUpdate() { 12 | this.highlightCode(); 13 | } 14 | highlightCode() { 15 | highlightBlock(this.codeEl); 16 | } 17 | render() { 18 | const { value, language, ...remainingProps } = this.props; 19 | return ( 20 |
21 |          {
23 |             this.codeEl = ref;
24 |           }}
25 |           className={`language-${language}`}
26 |         >
27 |           {value}
28 |         
29 |       
30 | ); 31 | } 32 | } 33 | 34 | CodeBlock.propTypes = { 35 | value: PropTypes.string.isRequired, 36 | language: PropTypes.string 37 | }; 38 | CodeBlock.defaultProps = { 39 | language: "" 40 | }; 41 | 42 | export default CodeBlock; 43 | -------------------------------------------------------------------------------- /src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import classNames from "classnames"; 5 | 6 | import TwitterButton from "./TwitterButton"; 7 | 8 | const styles = { 9 | root: { 10 | opacity: 0.8, 11 | fontSize: "85%", 12 | textAlign: "center", 13 | marginTop: "20px", 14 | borderTop: "1px solid #e5e5e5" 15 | } 16 | }; 17 | 18 | const Footer = ({ 19 | classes, 20 | fromFullYear, 21 | toFullYear, 22 | className, 23 | ...remainingProps 24 | }) => ( 25 |
26 |

27 | © 28 | {fromFullYear === toFullYear 29 | ? toFullYear 30 | : `${fromFullYear}-${toFullYear}`}{" "} 31 | labs.topheman.com - Christophe 32 | Rosset - v{process.env.REACT_APP_METADATAS_VERSION} 33 |

34 |

35 | All data comes from npm &{" "} 36 | npms / This project is not affiliated with 37 | npm, Inc. in any way. 38 |

39 |

40 | 47 |

48 |
49 | ); 50 | 51 | Footer.propTypes = { 52 | toFullYear: PropTypes.number, 53 | fromFullYear: PropTypes.number.isRequired, 54 | classes: PropTypes.object.isRequired, 55 | className: PropTypes.string 56 | }; 57 | Footer.defaultProps = { 58 | toFullYear: new Date().getFullYear(), 59 | className: undefined 60 | }; 61 | 62 | export default withStyles(styles)(Footer); 63 | -------------------------------------------------------------------------------- /src/components/Gravatar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Avatar from "@material-ui/core/Avatar"; 4 | 5 | import md5 from "js-md5"; 6 | 7 | const Gravatar = ({ email, size, ...remainingProps }) => ( 8 | 15 | ); 16 | 17 | Gravatar.propTypes = { 18 | email: PropTypes.string.isRequired, 19 | size: PropTypes.number 20 | }; 21 | 22 | Gravatar.defaultProps = { 23 | size: undefined 24 | }; 25 | 26 | export default Gravatar; 27 | -------------------------------------------------------------------------------- /src/components/Header.css: -------------------------------------------------------------------------------- 1 | /** github logo from my other sites**/ 2 | /** networks header */ 3 | .site-networks { 4 | position: absolute; 5 | right: 20px; 6 | top: 16px; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | ul.site-networks { 11 | list-style: none; 12 | text-align: center; 13 | padding: 0px 0px 10px 0px; 14 | } 15 | ul.site-networks li { 16 | position: relative; 17 | display: inline-block; 18 | vertical-align: middle; 19 | margin-left: 15px; 20 | } 21 | ul.site-networks li a { 22 | display: block; 23 | width: 32px; 24 | height: 32px; 25 | text-decoration: none; 26 | padding-top: 0px; 27 | -webkit-transition: all 0.5s; 28 | -moz-transition: all 0.5s; 29 | -ms-transition: all 0.5s; 30 | -o-transition: all 0.5s; 31 | transition: all 0.5s; 32 | } 33 | ul.site-networks li a span.icon { 34 | position: absolute; 35 | display: block; 36 | width: 32px; 37 | height: 32px; 38 | -webkit-transition: all 0.5s; 39 | -moz-transition: all 0.5s; 40 | -ms-transition: all 0.5s; 41 | -o-transition: all 0.5s; 42 | transition: all 0.5s; 43 | } 44 | ul.site-networks li a span.desc { 45 | display: none; 46 | } 47 | ul.site-networks li a:hover { 48 | } 49 | ul.site-networks li a:hover span.icon { 50 | left: 0px; 51 | -webkit-transform: rotate(360deg); 52 | -moz-transform: rotate(360deg); 53 | -ms-transform: rotate(360deg); 54 | -o-transform: rotate(360deg); 55 | transform: rotate(360deg); 56 | } 57 | /** since logos are included with the css in base64, we don't bother about pixel ratio media query (everybody gets the retina version)*/ 58 | ul.site-networks li.twitter a span.icon { 59 | background-image: url(../assets/images/twitter-retina-white.png); 60 | background-size: 32px 32px; 61 | } 62 | ul.site-networks li.github a span.icon { 63 | background-image: url(../assets/images/github-retina-white.png); 64 | background-size: 32px 32px; 65 | } 66 | /** for small devices */ 67 | @media only screen and (max-width: 600px) { 68 | ul.site-networks { 69 | top: 12px; 70 | } 71 | } 72 | @media only screen and (max-width: 600px) and (orientation: landscape) { 73 | ul.site-networks { 74 | top: 8px; 75 | } 76 | } 77 | /** for extra-small devices */ 78 | @media only screen and (max-width: 360px) { 79 | ul.site-networks li { 80 | display: none; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | // inspired by https://material-ui-next.com/demos/app-bar/#app-bar-with-buttons 2 | 3 | import React, { Fragment } from "react"; 4 | import PropTypes from "prop-types"; 5 | import { withStyles } from "@material-ui/core/styles"; 6 | import AppBar from "@material-ui/core/AppBar"; 7 | import Toolbar from "@material-ui/core/Toolbar"; 8 | import Typography from "@material-ui/core/Typography"; 9 | import IconButton from "@material-ui/core/IconButton"; 10 | import MenuIcon from "@material-ui/icons/Menu"; 11 | import { compose, withStateHandlers } from "recompose"; 12 | import classNames from "classnames"; 13 | 14 | import { Link } from "react-router-dom"; 15 | 16 | import MainDrawer from "../components/MainDrawer"; 17 | 18 | import "./Header.css"; 19 | import npmLogo from "../assets/images/n-64-white.png"; 20 | 21 | const styles = theme => ({ 22 | root: { 23 | flexGrow: 1 24 | }, 25 | logo: { 26 | width: 48, 27 | height: 48, 28 | backgroundImage: `url(${npmLogo})`, 29 | backgroundSize: 45, 30 | backgroundPosition: "2px 2px", 31 | backgroundRepeat: "no-repeat", 32 | borderRadius: "0%", 33 | marginRight: 10 34 | }, 35 | title: { 36 | textDecoration: "none", 37 | fontWeight: 500, 38 | flex: 1, 39 | [theme.breakpoints.down("xs")]: { 40 | fontSize: "100%" 41 | }, 42 | [theme.breakpoints.up("sm")]: { 43 | fontSize: "130%" 44 | }, 45 | "&:hover": { 46 | color: "white" 47 | } 48 | }, 49 | menuButton: { 50 | marginLeft: -12, 51 | [theme.breakpoints.down("xs")]: { 52 | marginRight: 0 53 | }, 54 | [theme.breakpoints.up("sm")]: { 55 | marginRight: 10 56 | } 57 | } 58 | }); 59 | 60 | const Header = props => { 61 | const { 62 | classes, 63 | drawerOpen, 64 | toggleDrawer, 65 | className, 66 | ...remainingProps 67 | } = props; 68 | return ( 69 | 70 |
74 | 75 | 76 | toggleDrawer(true)} 81 | > 82 | 83 | 84 | 91 | 98 | npm-registry-browser 99 | 100 | 120 | 121 | 122 |
123 | toggleDrawer(false)} 127 | /> 128 |
129 | ); 130 | }; 131 | 132 | Header.propTypes = { 133 | classes: PropTypes.object.isRequired, 134 | drawerOpen: PropTypes.bool.isRequired, // from withStateHandlers() 135 | toggleDrawer: PropTypes.func.isRequired, // from withStateHandlers() 136 | className: PropTypes.string, 137 | style: PropTypes.object 138 | }; 139 | 140 | Header.defaultProps = { 141 | className: undefined, 142 | style: undefined 143 | }; 144 | 145 | export default compose( 146 | withStateHandlers( 147 | { drawerOpen: false }, 148 | { 149 | toggleDrawer: () => open => ({ drawerOpen: open }) 150 | } 151 | ), 152 | withStyles(styles) 153 | )(Header); 154 | -------------------------------------------------------------------------------- /src/components/KeywordsList.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-array-index-key */ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import { Link } from "react-router-dom"; 5 | import { withStyles } from "@material-ui/core/styles"; 6 | import classNames from "classnames"; 7 | 8 | import LocalOfferIcon from "@material-ui/icons/LocalOffer"; 9 | 10 | const styles = { 11 | root: { 12 | margin: "5px 0", 13 | "& > svg": { 14 | fill: "grey", 15 | width: "15px", 16 | height: "15px", 17 | marginRight: "5px", 18 | marginBottom: "-4px" 19 | }, 20 | "& > a": { 21 | fontSize: "0.9rem", 22 | color: "grey", 23 | textDecoration: "none" 24 | }, 25 | "& > a:hover": { 26 | textDecoration: "underline" 27 | }, 28 | // style separating "," to avoid conditional in loops inside render 29 | "& > a:after": { 30 | content: "','", 31 | display: "inline-block", // avoid underline on hover 32 | textDecoration: "none", 33 | paddingRight: "3px" 34 | }, 35 | "& > a:last-child:after": { 36 | content: "''" 37 | } 38 | } 39 | }; 40 | 41 | const KeywordsList = ({ keywords, classes, className, ...remainingProps }) => { 42 | if (keywords && keywords.length > 0) { 43 | return ( 44 |
45 | 46 | {keywords.map((keyword, index) => ( 47 | 52 | {keyword} 53 | 54 | ))} 55 |
56 | ); 57 | } 58 | return null; 59 | }; 60 | 61 | KeywordsList.propTypes = { 62 | classes: PropTypes.object.isRequired, 63 | keywords: PropTypes.array, 64 | className: PropTypes.string 65 | }; 66 | 67 | KeywordsList.defaultProps = { 68 | keywords: [], 69 | className: undefined 70 | }; 71 | 72 | export default withStyles(styles)(KeywordsList); 73 | -------------------------------------------------------------------------------- /src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import LinearProgress from "@material-ui/core/LinearProgress"; 5 | import { withStyles } from "@material-ui/core/styles"; 6 | 7 | import Waiting from "./Waiting"; 8 | 9 | const styles = theme => ({ 10 | customLoaderRoot: { 11 | verticalAlign: "center", 12 | [theme.breakpoints.down("xs")]: { 13 | padding: "10px 10px" 14 | }, 15 | [theme.breakpoints.up("sm")]: { 16 | padding: "60px 10px" 17 | } 18 | }, 19 | customLoaderMessage: { 20 | marginBottom: "10px", 21 | textAlign: "center" 22 | }, 23 | progress: { 24 | width: 50, 25 | margin: "0px auto" 26 | } 27 | }); 28 | 29 | const CustomLoader = ({ message, classes }) => ( 30 |
31 |
{message}
32 | 33 |
34 | ); 35 | CustomLoader.propTypes = { 36 | classes: PropTypes.object.isRequired, 37 | message: PropTypes.string 38 | }; 39 | CustomLoader.defaultProps = { 40 | message: "Loading" 41 | }; 42 | 43 | const Loader = ({ message, classes, overrideClasses, ...remainingProps }) => { 44 | const loaderClasses = { 45 | ...classes, 46 | ...overrideClasses 47 | }; 48 | return ( 49 | } 51 | {...remainingProps} 52 | /> 53 | ); 54 | }; 55 | 56 | Loader.propTypes = { 57 | classes: PropTypes.object.isRequired, 58 | overrideClasses: PropTypes.object, 59 | message: PropTypes.string.isRequired 60 | }; 61 | Loader.defaultProps = { 62 | message: "Loading", 63 | overrideClasses: {} // override the classes of the loader 64 | }; 65 | 66 | export default withStyles(styles)(Loader); 67 | -------------------------------------------------------------------------------- /src/components/MainDrawer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import Icon from "@material-ui/core/Icon"; 5 | import List from "@material-ui/core/List"; 6 | import ListItem from "@material-ui/core/ListItem"; 7 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 8 | import ListItemText from "@material-ui/core/ListItemText"; 9 | import Divider from "@material-ui/core/Divider"; 10 | import CloseIcon from "@material-ui/icons/Close"; 11 | import HomeIcon from "@material-ui/icons/Home"; 12 | import InfoIcon from "@material-ui/icons/Info"; 13 | import WifiIcon from "@material-ui/icons/Wifi"; 14 | import MuiDrawer from "@material-ui/core/Drawer"; 15 | import classNames from "classnames"; 16 | import { Link } from "react-router-dom"; 17 | 18 | import twitterIcon from "../assets/images/twitter-retina.png"; 19 | import githubIcon from "../assets/images/github-retina.png"; 20 | import qrcode from "../assets/images/qrcode.png"; 21 | 22 | const styles = { 23 | closeIcon: { 24 | cursor: "pointer", 25 | color: "gray", 26 | "&:hover": { 27 | color: "black" 28 | } 29 | }, 30 | svgColor: { 31 | fill: "#900000" 32 | }, 33 | verticalList: { 34 | width: 250 35 | }, 36 | horizontalList: { 37 | width: "auto" 38 | }, 39 | listItem: { 40 | paddingTop: 8, 41 | paddingBottom: 8 42 | }, 43 | listIcon: { 44 | width: 24, 45 | height: 24 46 | }, 47 | qrcodeIcon: { 48 | backgroundImage: `url(${qrcode})`, 49 | backgroundRepeat: "no-repeat", 50 | backgroundSize: "100%" 51 | } 52 | }; 53 | 54 | const MainDrawer = ({ classes, anchor, open, onClose, ...remainingProps }) => { 55 | const sideList = ( 56 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 78 | 79 | 82 | 83 | 84 | 85 | 93 | 94 | 97 | 98 | 99 | 100 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 125 | 126 | github 127 | 128 | 129 | 130 | 137 | 138 | twitter 139 | 140 | 141 | 142 | 149 | 150 | 153 | 154 | 155 | 156 | 157 |
158 | ); 159 | 160 | return ( 161 | 167 |
174 | {sideList} 175 |
176 |
177 | ); 178 | }; 179 | 180 | MainDrawer.propTypes = { 181 | classes: PropTypes.object.isRequired, 182 | anchor: PropTypes.string.isRequired, 183 | open: PropTypes.bool.isRequired, 184 | onClose: PropTypes.func.isRequired 185 | }; 186 | 187 | export default withStyles(styles)(MainDrawer); 188 | -------------------------------------------------------------------------------- /src/components/MainLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { withStyles } from "@material-ui/core/styles"; 5 | import CssBaseline from "@material-ui/core/CssBaseline"; 6 | 7 | import Header from "../components/Header"; 8 | import SearchContainer from "../containers/SearchContainer"; 9 | import Footer from "../components/Footer"; 10 | 11 | const styles = theme => ({ 12 | root: { 13 | margin: "0px 16px", 14 | marginBottom: 15 | ((process.env.REACT_APP_NPM_REGISTRY_API_MOCKS_ENABLED === "true" || 16 | process.env.REACT_APP_NPM_API_MOCKS_ENABLED === "true") && 17 | "50px") || 18 | "0" 19 | }, 20 | searchContainer: { 21 | marginTop: 80 22 | }, 23 | content: { 24 | margin: "0px auto", 25 | [theme.breakpoints.up("xs")]: { 26 | maxWidth: "1180px" // adjust for regular and small screens (default fixed maxWidth) 27 | }, 28 | [theme.breakpoints.up("lg")]: { 29 | maxWidth: "90vw" // adjust for wide screens 30 | }, 31 | [theme.breakpoints.up("xl")]: { 32 | maxWidth: "70vw" // adjust for very-wide screens 33 | } 34 | }, 35 | mockWarning: { 36 | position: "fixed", 37 | bottom: 0, 38 | width: 200, 39 | left: "50%", 40 | transform: "translate(-50%, 0)", 41 | backgroundColor: theme.palette.primary.main, 42 | color: theme.palette.primary.contrastText, 43 | padding: 4, 44 | textAlign: "center", 45 | borderRadius: "8px 8px 0px 0px" 46 | } 47 | }); 48 | 49 | const MainLayout = ({ children, classes }) => ( 50 | 51 | 52 |
53 |
54 | 55 |
56 | {children} 57 |
58 |
59 |
60 | {(process.env.REACT_APP_NPM_REGISTRY_API_MOCKS_ENABLED === "true" || 61 | process.env.REACT_APP_NPM_API_MOCKS_ENABLED === "true") && ( 62 |
⚠ Mocking http request
63 | )} 64 |
65 | ); 66 | 67 | MainLayout.propTypes = { 68 | children: PropTypes.element.isRequired, 69 | classes: PropTypes.object.isRequired 70 | }; 71 | 72 | export default withStyles(styles)(MainLayout); 73 | -------------------------------------------------------------------------------- /src/components/Markdown.js: -------------------------------------------------------------------------------- 1 | // inspired by https://github.com/rexxars/react-markdown/blob/master/demo/src/demo.js 2 | 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | import ReactMarkdown, { uriTransformer } from "react-markdown"; 6 | 7 | import CodeBlock from "./CodeBlock"; 8 | import { buildImageUrl, buildLinkUrl } from "../utils/github"; 9 | 10 | const ANCHOR_PREFIX = "anchor-"; 11 | 12 | /** 13 | * Transforms urls inside readme 14 | * - pointing to npmjs.com/package/* to #/package/* 15 | * - relative urls in github readme markdown to absolute ones to keep html links 16 | * 17 | * Only applied to mardown (not html code in markdown) 18 | * 19 | * @param {*} uri 20 | */ 21 | const makeTransformLinkUri = ({ repository }) => uri => { 22 | // make sure to sanitize links through XSS-filter 23 | let sanitizedUri = uriTransformer(uri); 24 | // transform github relative links to absolute ones 25 | // keep the anchors - will be rendered by LinkRenderer 26 | sanitizedUri = 27 | repository.isGithub && !(uri && uri.startsWith("#")) 28 | ? buildLinkUrl(repository, sanitizedUri) 29 | : sanitizedUri; 30 | // transform links to npm to be used by our front router 31 | return sanitizedUri 32 | ? sanitizedUri.replace( 33 | /http[s]?:\/\/(www\.)?npmjs.com\/package\//, 34 | "#/package/" 35 | ) 36 | : null; 37 | }; 38 | 39 | /** 40 | * Transforms relative image urls inside readme to absolute ones 41 | * poiting to raw.githubusercontent.com (that will serve the correct mime type) 42 | * 43 | * Only applied to mardown (not html code in markdown) 44 | * 45 | * @param {*} uri 46 | */ 47 | const makeTransformImageUri = ({ repository }) => uri => { 48 | // make sure to sanitize links through XSS-filter 49 | let sanitizedUri = uriTransformer(uri); 50 | // transform github relative links to images to absolute one to raw.githubusercontent.com 51 | sanitizedUri = repository.isGithub 52 | ? buildImageUrl(repository, sanitizedUri) 53 | : sanitizedUri; 54 | return sanitizedUri; 55 | }; 56 | 57 | /** Renderer Headers with ids to scroll to */ 58 | 59 | function flatten(text, child) { 60 | return typeof child === "string" 61 | ? text + child 62 | : React.Children.toArray(child.props.children).reduce(flatten, text); 63 | } 64 | 65 | const HeadingRenderer = props => { 66 | const children = React.Children.toArray(props.children); 67 | const text = children.reduce(flatten, ""); 68 | const slug = `${ANCHOR_PREFIX}${text.toLowerCase().replace(/\W/g, "-")}`; 69 | return React.createElement("h" + props.level, { id: slug }, props.children); 70 | }; 71 | HeadingRenderer.propTypes = { 72 | level: PropTypes.number.isRequired, 73 | children: PropTypes.array 74 | }; 75 | HeadingRenderer.defaultProps = { 76 | children: undefined 77 | }; 78 | 79 | /** Renderer Links with handler to prevent links to anchors (and scroll to headers) */ 80 | 81 | const scrollTo = anchorName => e => { 82 | e.preventDefault(); 83 | const el = document.getElementById(anchorName.replace("#", ANCHOR_PREFIX)); 84 | if (el) { 85 | window.scrollTo(0, el.offsetTop - 90); 86 | } 87 | }; 88 | 89 | const LinkRenderer = props => { 90 | const children = React.Children.toArray(props.children); 91 | if ( 92 | props.href && 93 | props.href.startsWith("#") && 94 | !props.href.startsWith("#/package") // exclude rewritten urls 95 | ) { 96 | return React.createElement( 97 | "a", 98 | { 99 | ...props, 100 | href: "", 101 | onClick: scrollTo(props.href) 102 | }, 103 | children 104 | ); 105 | } 106 | return React.createElement("a", props, children); 107 | }; 108 | LinkRenderer.propTypes = { 109 | href: PropTypes.string, 110 | children: PropTypes.array 111 | }; 112 | LinkRenderer.defaultProps = { 113 | href: undefined, 114 | children: undefined 115 | }; 116 | 117 | /** Markdown component */ 118 | 119 | // note: Markdown component only transfers className as remaining props (not style or others) 120 | const Markdown = ({ repository, source, ...remainingProps }) => ( 121 | 133 | ); 134 | Markdown.propTypes = { 135 | source: PropTypes.string, 136 | repository: PropTypes.object 137 | }; 138 | Markdown.defaultProps = { 139 | source: "", 140 | repository: {} 141 | }; 142 | 143 | export default Markdown; 144 | -------------------------------------------------------------------------------- /src/components/Package/DependenciesTab.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import ExpansionPanel from "@material-ui/core/ExpansionPanel"; 4 | import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; 5 | import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; 6 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; 7 | import Typography from "@material-ui/core/Typography"; 8 | import Chip from "@material-ui/core/Chip"; 9 | import { withStyles } from "@material-ui/core/styles"; 10 | 11 | import { Link } from "react-router-dom"; 12 | 13 | const styles = theme => ({ 14 | panelDetails: { 15 | display: "block" 16 | }, 17 | sectionTitle: { 18 | marginBottom: 10, 19 | marginTop: 15 20 | }, 21 | chipsWrapper: { 22 | display: "flex", 23 | justifyContent: "center", 24 | flexWrap: "wrap", 25 | margin: "5px 0px" 26 | }, 27 | chip: { 28 | margin: 3, 29 | cursor: "pointer", 30 | textDecoration: "none", 31 | "&:hover": { 32 | backgroundColor: "#cecece" 33 | }, 34 | "& > span": { 35 | [theme.breakpoints.down("sm")]: { 36 | maxWidth: "75vw", // on small screens, limit the maxWidth to 75% of the width of the window (vw unit) 37 | display: "inline-block", 38 | overflow: "hidden", 39 | textOverflow: "ellipsis" 40 | } 41 | } 42 | } 43 | }); 44 | 45 | const DependenciesTab = ({ 46 | version, 47 | packageInfos, 48 | classes, 49 | ...remainingProps 50 | }) => { 51 | const dependencies = 52 | (packageInfos && 53 | packageInfos.versions && 54 | packageInfos.versions[version] && 55 | packageInfos.versions[version].dependencies && 56 | Object.keys(packageInfos.versions[version].dependencies)) || 57 | []; 58 | const devDependencies = 59 | (packageInfos && 60 | packageInfos.versions && 61 | packageInfos.versions[version] && 62 | packageInfos.versions[version].devDependencies && 63 | Object.keys(packageInfos.versions[version].devDependencies)) || 64 | []; 65 | const peerDependencies = 66 | (packageInfos && 67 | packageInfos.versions && 68 | packageInfos.versions[version] && 69 | packageInfos.versions[version].peerDependencies && 70 | Object.keys(packageInfos.versions[version].peerDependencies)) || 71 | []; 72 | const lists = { 73 | [`Dependencies`]: dependencies, 74 | [`Dev Dependencies`]: devDependencies, 75 | [`Peer Dependencies`]: peerDependencies 76 | }; 77 | return ( 78 |
79 | 80 | }> 81 | Dependencies ({dependencies.length}) 82 | 83 | 84 | {Object.entries(lists).map(([sectionTitle, sectionDependencies]) => ( 85 | 86 | 87 | {sectionTitle} ({sectionDependencies.length}) 88 | 89 |
90 | {sectionDependencies.map(dependencyName => ( 91 | 99 | ))} 100 |
101 |
102 | ))} 103 |
104 |
105 |
106 | ); 107 | }; 108 | 109 | DependenciesTab.propTypes = { 110 | version: PropTypes.string.isRequired, 111 | packageInfos: PropTypes.object.isRequired, 112 | classes: PropTypes.object.isRequired 113 | }; 114 | 115 | export default withStyles(styles)(DependenciesTab); 116 | -------------------------------------------------------------------------------- /src/components/Package/InfosContents.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Typography from "@material-ui/core/Typography"; 4 | import { withStyles } from "@material-ui/core/styles"; 5 | import List from "@material-ui/core/List"; 6 | import ListItem from "@material-ui/core/ListItem"; 7 | import ListItemAvatar from "@material-ui/core/ListItemAvatar"; 8 | import relativeDate from "relative-date"; 9 | 10 | import { 11 | safeExtractVersion, 12 | extractLicenseInfos, 13 | extractUrl, 14 | extractRepositoryInfos, 15 | extractMaintainers 16 | } from "../../utils/metadatas"; 17 | import { displayUrl } from "../../utils/url"; 18 | import Gravatar from "../Gravatar"; 19 | 20 | const styles = { 21 | block: { 22 | margin: "4px 0 16px" 23 | }, 24 | safeWidth: { 25 | display: "block", 26 | overflow: "hidden", 27 | textOverflow: "ellipsis", 28 | whiteSpace: "nowrap" 29 | }, 30 | license: {}, 31 | homepage: {}, 32 | repository: {}, 33 | maintainers: {} 34 | }; 35 | 36 | const InfosContent = ({ 37 | packageInfos, 38 | version, 39 | classes, 40 | ...remainingProps 41 | }) => { 42 | const licenseInfos = extractLicenseInfos(packageInfos, version); 43 | const homepage = extractUrl( 44 | safeExtractVersion(packageInfos, version).homepage 45 | ); 46 | const repositoryInfos = extractRepositoryInfos( 47 | safeExtractVersion(packageInfos, version).repository 48 | ); 49 | const maintainers = extractMaintainers(packageInfos, version); 50 | const datePublishedRelative = 51 | (packageInfos && 52 | packageInfos.time && 53 | packageInfos.time[version] && 54 | relativeDate(new Date(packageInfos.time[version]))) || 55 | undefined; 56 | return ( 57 |
58 | {datePublishedRelative && ( 59 |
60 | v{version} published {datePublishedRelative} 61 |
62 | )} 63 | 64 | {(packageInfos && 65 | packageInfos.time && 66 | packageInfos.time[version] && 67 | new Date(packageInfos.time[version]).toLocaleDateString()) || 68 | "\u00A0"} 69 | 70 | {licenseInfos && ( 71 |
72 | License 73 | {licenseInfos.licenseId} 74 |
75 | )} 76 | {homepage && ( 77 |
78 | Homepage 79 | 80 | {displayUrl(homepage)} 81 | 82 |
83 | )} 84 | {repositoryInfos && ( 85 | 91 | )} 92 | {maintainers.length > 0 && ( 93 |
94 | Maintainers 95 | 96 | {maintainers.map(maintainer => ( 97 | 98 | 99 | 100 | 101 | 102 | {maintainer.name} 103 | 104 | 105 | ))} 106 | 107 |
108 | )} 109 |
110 | ); 111 | }; 112 | 113 | InfosContent.propTypes = { 114 | packageInfos: PropTypes.object.isRequired, 115 | version: PropTypes.string.isRequired, 116 | classes: PropTypes.object.isRequired 117 | }; 118 | 119 | export default withStyles(styles)(InfosContent); 120 | -------------------------------------------------------------------------------- /src/components/Package/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import classNames from "classnames"; 5 | 6 | const styles = { 7 | root: { 8 | "& h2, & p": { 9 | textAlign: "center" 10 | } 11 | }, 12 | emoji: { 13 | lineHeight: "100px", 14 | fontSize: "70px" 15 | } 16 | }; 17 | 18 | const NotFound = ({ classes, packageName, className, ...remainingProps }) => ( 19 |
20 |

Not Found

21 |

Package "{packageName}" not found

22 |

23 | 24 | 📦 25 | 26 |

27 |
28 | ); 29 | 30 | NotFound.propTypes = { 31 | packageName: PropTypes.string.isRequired, 32 | classes: PropTypes.object.isRequired, 33 | className: PropTypes.string 34 | }; 35 | NotFound.defaultProps = { 36 | className: undefined 37 | }; 38 | 39 | export default withStyles(styles)(NotFound); 40 | -------------------------------------------------------------------------------- /src/components/Package/PackageJsonTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ExpansionPanel from "@material-ui/core/ExpansionPanel"; 4 | import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; 5 | import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; 6 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; 7 | import Typography from "@material-ui/core/Typography"; 8 | import { withStyles } from "@material-ui/core/styles"; 9 | import classNames from "classnames"; 10 | 11 | import CodeBlock from "../CodeBlock"; 12 | 13 | const styles = theme => ({ 14 | codeBlock: { 15 | [theme.breakpoints.down("xs")]: { 16 | // on small screens, limit the maxWidth to 80% of the width of the window (vw unit) 17 | // so that
 tags in readme have specific width to overflow: scroll when
18 |       // piece of code exemple is to wide
19 |       maxWidth: "80vw"
20 |     },
21 |     [theme.breakpoints.up("sm")]: {
22 |       maxWidth: "58vw" // adjust for regular screens
23 |     },
24 |     [theme.breakpoints.up("md")]: {
25 |       maxWidth: "64vw" // adjust for regular screens
26 |     },
27 |     [theme.breakpoints.up("xl")]: {
28 |       maxWidth: "53vw" // adjust for very-wide screens
29 |     },
30 |     wordBreak: "normal",
31 |     overflow: "auto"
32 |   }
33 | });
34 | 
35 | const PackageJson = ({ value, classes, className, ...remainingProps }) => {
36 |   const { version } = value;
37 |   const formattedValue = JSON.stringify(
38 |     value,
39 |     (key, val) =>
40 |       typeof key === "string" && key.startsWith("_") ? undefined : val, // remove "private infos"
41 |     "  "
42 |   );
43 |   return (
44 |     
45 | 46 | }> 47 | 48 | package.json (v{version}) 49 | 50 | 51 | 52 | 57 | 58 | 59 |
60 | ); 61 | }; 62 | PackageJson.propTypes = { 63 | value: PropTypes.object.isRequired, 64 | classes: PropTypes.object.isRequired, 65 | className: PropTypes.string 66 | }; 67 | PackageJson.defaultProps = { 68 | className: undefined 69 | }; 70 | 71 | export default withStyles(styles)(PackageJson); 72 | -------------------------------------------------------------------------------- /src/components/Package/Readme.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withStyles } from "@material-ui/core/styles"; 4 | import classNames from "classnames"; 5 | 6 | import Markdown from "../Markdown"; 7 | 8 | const styles = theme => ({ 9 | root: { 10 | flexGrow: 1 11 | }, 12 | heading: { 13 | fontSize: theme.typography.pxToRem(12) 14 | }, 15 | bookIcon: { 16 | verticalAlign: "middle", 17 | marginRight: "8px" 18 | }, 19 | markdown: { 20 | [theme.breakpoints.down("xs")]: { 21 | // on small screens, limit the maxWidth to 80% of the width of the window (vw unit) 22 | // so that
 tags in readme have specific width to overflow: scroll when
23 |       // piece of code exemple is to wide
24 |       maxWidth: "80vw"
25 |     },
26 |     [theme.breakpoints.up("sm")]: {
27 |       maxWidth: "58vw" // adjust for regular screens
28 |     },
29 |     [theme.breakpoints.up("md")]: {
30 |       maxWidth: "64vw" // adjust for regular screens
31 |     },
32 |     [theme.breakpoints.up("xl")]: {
33 |       maxWidth: "53vw" // adjust for very-wide screens
34 |     },
35 |     wordBreak: "break-word", // revent long word from overflowing the layout
36 |     "& pre": {
37 |       wordBreak: "normal",
38 |       overflow: "auto"
39 |     },
40 |     "& code": {
41 |       fontSize: "1.1em"
42 |     },
43 |     "& img": {
44 |       maxWidth: "100%" // Prevent large images to overflow
45 |     }
46 |   }
47 | });
48 | 
49 | // note: Markdown component only transfers className as remaining props (not style or others) - so does Readme
50 | const Readme = ({
51 |   classes,
52 |   source,
53 |   repository,
54 |   className,
55 |   ...remainingProps
56 | }) => (
57 |   
63 | );
64 | 
65 | Readme.propTypes = {
66 |   classes: PropTypes.object.isRequired,
67 |   source: PropTypes.string,
68 |   repository: PropTypes.object,
69 |   className: PropTypes.string
70 | };
71 | Readme.defaultProps = {
72 |   source: "",
73 |   repository: {},
74 |   className: undefined
75 | };
76 | 
77 | export default withStyles(styles)(Readme);
78 | 


--------------------------------------------------------------------------------
/src/components/Package/StatsContents.js:
--------------------------------------------------------------------------------
 1 | import React, { Component } from "react";
 2 | import PropTypes from "prop-types";
 3 | import Typography from "@material-ui/core/Typography";
 4 | import { withStyles } from "@material-ui/core/styles";
 5 | 
 6 | import Sparkline from "../Sparkline";
 7 | import { yearDownloadsToWeaks } from "../../utils/npmApiHelpers";
 8 | 
 9 | const styles = {
10 |   root: {},
11 |   label: {},
12 |   downloadsCount: {},
13 |   datavizWrapper: {
14 |     textAlign: "right"
15 |   }
16 | };
17 | 
18 | class StatsContents extends Component {
19 |   static propTypes = {
20 |     downloads: PropTypes.object.isRequired,
21 |     classes: PropTypes.object.isRequired,
22 |     theme: PropTypes.object.isRequired
23 |   };
24 |   static defaultProps = {
25 |     className: undefined,
26 |     style: undefined
27 |   };
28 |   constructor(props) {
29 |     super(props);
30 |     this.state = {
31 |       infos: null,
32 |       downloads: null
33 |     };
34 |     this.onMouseMove = this.onMouseMove.bind(this);
35 |     this.onMouseOut = this.onMouseOut.bind(this);
36 |   }
37 |   onMouseMove(event, datapoint) {
38 |     if (datapoint) {
39 |       this.setState({
40 |         from: datapoint.from,
41 |         to: datapoint.to,
42 |         downloads: datapoint.value
43 |       });
44 |     }
45 |   }
46 |   onMouseOut() {
47 |     this.setState({
48 |       from: null,
49 |       to: null,
50 |       downloads: null
51 |     });
52 |   }
53 |   render() {
54 |     const data = yearDownloadsToWeaks(this.props.downloads.downloads);
55 |     const {
56 |       theme,
57 |       classes,
58 |       downloads: _omitDownloads, // remove this from props to prevent form flowing down
59 |       ...remainingProps
60 |     } = this.props;
61 |     const { from, to, downloads } = this.state;
62 |     const lastWeekDownloadsCount = data[data.length - 1].value;
63 |     return (
64 |       
65 |
66 | 67 | {downloads 68 | ? `From ${new Date(from).toLocaleDateString()} to ${new Date( 69 | to 70 | ).toLocaleDateString()}` 71 | : "Last 7 days (all versions)"} 72 | 73 |
74 | {(downloads || lastWeekDownloadsCount).toLocaleString()} 75 |
76 |
77 | 86 |
87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | export default withStyles(styles, { withTheme: true })(StatsContents); 94 | -------------------------------------------------------------------------------- /src/components/Package/VersionsTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import List from "@material-ui/core/List"; 4 | import ListSubheader from "@material-ui/core/ListSubheader"; 5 | import ExpansionPanel from "@material-ui/core/ExpansionPanel"; 6 | import ExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary"; 7 | import ExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails"; 8 | import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; 9 | import Typography from "@material-ui/core/Typography"; 10 | import { withStyles } from "@material-ui/core/styles"; 11 | import relativeDate from "relative-date"; 12 | 13 | import { Link } from "react-router-dom"; 14 | 15 | import { formatPackageString } from "../../utils/string"; 16 | 17 | const styles = theme => ({ 18 | listSubheader: { 19 | padding: "0" 20 | }, 21 | ul: { 22 | padding: 0 23 | }, 24 | listItem: { 25 | paddingBottom: 0 26 | }, 27 | listRoot: { 28 | width: "100%", 29 | [theme.breakpoints.down("sm")]: { 30 | maxWidth: "75vw" // on small screens, limit the maxWidth to 75% of the width of the window (vw unit) 31 | } 32 | }, 33 | listItemTextContent: { 34 | fontSize: "1.1rem", 35 | display: "flex", 36 | flexDirection: "row", 37 | "& > *:first-child": { 38 | overflow: "hidden", 39 | textOverflow: "ellipsis", 40 | whiteSpace: "nowrap" 41 | }, 42 | "& > *:last-child": { 43 | textAlign: "right" 44 | }, 45 | "& > *": { 46 | display: "block", 47 | "-webkit-box-flex": 1, 48 | "-ms-flex": "1 0 initial", 49 | flex: "1 0 initial", 50 | whiteSpace: "nowrap" 51 | } 52 | }, 53 | selected: { 54 | fontWeight: "bold" 55 | }, 56 | version: {}, 57 | space: { 58 | marginBottom: 5, 59 | borderBottom: "1px dotted rgba(0, 0, 0, .2)", 60 | marginLeft: 10, 61 | marginRight: 10, 62 | flex: "1 1 auto" 63 | }, 64 | date: {} 65 | }); 66 | 67 | const VersionsTab = ({ 68 | scope, 69 | name, 70 | version: currentVersion, 71 | packageInfos, 72 | classes, 73 | ...remainingProps 74 | }) => { 75 | const distTags = Object.entries(packageInfos["dist-tags"]); 76 | const versions = Object.keys(packageInfos.versions).reverse(); 77 | return ( 78 |
79 | 80 | }> 81 | 82 | Versions ({versions.length}) 83 | 84 | 85 | 86 | } dense className={classes.listRoot}> 87 |
  • 88 |
      89 | 90 | Tags ({distTags.length}) 91 | 92 | {distTags.map(([tag, version]) => ( 93 | 103 | {tag} 104 |
      105 | {version} 106 | 107 | ))} 108 | 109 | Versions ({versions.length}) 110 | 111 | {versions.map(version => ( 112 | 124 | {version} 125 |
      126 | 127 | 135 | 136 | 137 | ))} 138 |
    139 |
  • 140 |
    141 |
    142 |
    143 |
    144 | ); 145 | }; 146 | 147 | VersionsTab.propTypes = { 148 | scope: PropTypes.string, 149 | name: PropTypes.string.isRequired, 150 | version: PropTypes.string.isRequired, 151 | packageInfos: PropTypes.object.isRequired, 152 | classes: PropTypes.object.isRequired 153 | }; 154 | VersionsTab.defaultProps = { 155 | scope: undefined 156 | }; 157 | 158 | export default withStyles(styles)(VersionsTab); 159 | -------------------------------------------------------------------------------- /src/components/Package/__tests__/NotFound.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "../../../testUtils"; 3 | 4 | import NotFound from "../NotFound"; 5 | 6 | describe("/components/NotFound", () => { 7 | describe("render", () => { 8 | it("should pass down remainingProps", () => { 9 | const { container } = render( 10 | 15 | ); 16 | expect(container.firstChild).toHaveClass("hello-world"); 17 | expect(container.firstChild.style.color).toBe("red"); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Package/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Package"; 2 | -------------------------------------------------------------------------------- /src/components/RetryButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | import { withStyles } from "@material-ui/core/styles"; 6 | import Button from "@material-ui/core/Button"; 7 | import RefreshIcon from "@material-ui/icons/Refresh"; 8 | 9 | const styles = theme => ({ 10 | root: { 11 | textAlign: "center", 12 | [theme.breakpoints.down("xs")]: { 13 | padding: "10px 10px" 14 | }, 15 | [theme.breakpoints.up("sm")]: { 16 | padding: "60px 10px" 17 | } 18 | } 19 | }); 20 | 21 | const RetryButton = ({ onClick, classes, className, ...remainingProps }) => ( 22 |
    23 | 26 |
    27 | ); 28 | 29 | RetryButton.propTypes = { 30 | onClick: PropTypes.func.isRequired, 31 | classes: PropTypes.object.isRequired, 32 | className: PropTypes.string 33 | }; 34 | RetryButton.defaultProps = { 35 | className: undefined 36 | }; 37 | 38 | export default withStyles(styles)(RetryButton); 39 | -------------------------------------------------------------------------------- /src/components/SearchResults/SearchResultItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { withStyles } from "@material-ui/core/styles"; 5 | import relativeDate from "relative-date"; 6 | import classNames from "classnames"; 7 | 8 | import KeywordsList from "../KeywordsList"; 9 | import Gravatar from "../Gravatar"; 10 | 11 | const styles = { 12 | root: { 13 | marginBottom: "25px" 14 | }, 15 | packageName: { 16 | fontSize: "1.4rem", 17 | textDecoration: "none", 18 | "&:hover": { 19 | textDecoration: "underline" 20 | } 21 | }, 22 | version: { 23 | color: "grey", 24 | marginLeft: "6px", 25 | lineHeight: "1.4rem" 26 | }, 27 | description: { 28 | margin: "5px 0" 29 | }, 30 | publishInfos: { 31 | fontSize: "0.9rem", 32 | "& em": { 33 | fontWeight: 500 34 | } 35 | }, 36 | gravatar: { 37 | display: "inline-block", 38 | marginLeft: 8, 39 | marginBottom: -5, 40 | width: "20px", 41 | height: "20px" 42 | } 43 | }; 44 | 45 | const SearchResultItem = ({ 46 | package: packageInfos, 47 | classes, 48 | className, 49 | ...remainingProps 50 | }) => ( 51 |
    52 | 53 |
    54 | 58 | {packageInfos.name} 59 | 60 | (v{packageInfos.version}) 61 |
    62 |
    {packageInfos.description}
    63 | 67 |
    68 | 69 | published {relativeDate(new Date(packageInfos.date))} 70 | 71 | {packageInfos.publisher && packageInfos.publisher.username && ( 72 | 73 | {" "} 74 | by {packageInfos.publisher.username} 75 | {packageInfos.publisher.email && ( 76 | 81 | )} 82 | 83 | )} 84 |
    85 |
    86 |
    87 | ); 88 | 89 | SearchResultItem.propTypes = { 90 | classes: PropTypes.object.isRequired, 91 | package: PropTypes.object.isRequired, 92 | className: PropTypes.string 93 | }; 94 | SearchResultItem.defaultProps = { 95 | className: undefined 96 | }; 97 | 98 | export default withStyles(styles)(SearchResultItem); 99 | -------------------------------------------------------------------------------- /src/components/SearchResults/SearchResults.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import SearchResultItem from "./SearchResultItem"; 5 | 6 | const SearchResults = ({ results, total, ...remainingProps }) => ( 7 |
    8 | {results.map(result => ( 9 | 14 | ))} 15 | {results.length === 0 && ( 16 |
    No Results
    17 | )} 18 | {results.length > 0 && total && total > results.length &&
    ...
    } 19 |
    20 | ); 21 | 22 | SearchResults.propTypes = { 23 | total: PropTypes.number, 24 | results: PropTypes.array.isRequired 25 | }; 26 | 27 | SearchResults.defaultProps = { 28 | total: undefined 29 | }; 30 | 31 | export default SearchResults; 32 | -------------------------------------------------------------------------------- /src/components/SearchResults/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchResults"; 2 | -------------------------------------------------------------------------------- /src/components/Sparkline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple wrapper for sparkline 3 | * 4 | * There is no componentWillUpdate mechanism (in case you update data or width). 5 | * Since it is meant to display still data, I kept it simple. 6 | * 7 | * More complex dataviz here: https://github.com/topheman/d3-react-experiments 8 | */ 9 | 10 | import React, { Component } from "react"; 11 | import PropTypes from "prop-types"; 12 | 13 | // temporary hack 14 | // see src/libs/README.md for explanation (why not directly use "@fnando/sparkline") 15 | import { sparkline } from "../libs/@fnando/sparkline/src/sparkline"; 16 | 17 | export default class Sparkline extends Component { 18 | static propTypes = { 19 | data: PropTypes.array.isRequired, 20 | width: PropTypes.number.isRequired, 21 | height: PropTypes.number.isRequired, 22 | strokeWidth: PropTypes.number.isRequired, 23 | onMouseMove: PropTypes.func, 24 | onMouseOut: PropTypes.func 25 | }; 26 | static defaultProps = { 27 | width: 100, 28 | height: 30, 29 | strokeWidth: 3, 30 | onMouseMove: undefined, 31 | onMouseOut: undefined 32 | }; 33 | componentDidMount() { 34 | this.init(); // only init on mount 35 | } 36 | init() { 37 | if (this.node) { 38 | const config = { 39 | onmousemove: this.props.onMouseMove, 40 | onmouseout: this.props.onMouseOut 41 | }; 42 | sparkline(this.node, this.props.data, config); 43 | } 44 | } 45 | render() { 46 | const { 47 | data, // remove data from the props 48 | width, 49 | height, 50 | strokeWidth, 51 | ...remainingProps 52 | } = this.props; 53 | return ( 54 | { 56 | this.node = node; 57 | }} 58 | width={width} 59 | height={height} 60 | strokeWidth={strokeWidth} 61 | {...remainingProps} 62 | /> 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/components/TwitterButton.js: -------------------------------------------------------------------------------- 1 | // inspired by https://github.com/topheman/d3-react-experiments/blob/master/src/components/TwitterButton/TwitterButton.js 2 | 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | 6 | /** 7 | * This component renders directly the iframe of twitter without running external script 8 | * to avoid messing up with react's internal DOM and break react hot loader 9 | * 10 | * @param {String} size 11 | * @param {String} lang 12 | * @param {Boolean} dnt 13 | * @param {String} text 14 | * @param {String} url 15 | * @param {String} hashtags 16 | * @param {String} via 17 | * @param {String} related 18 | * @param {String} buttonTitle 19 | */ 20 | const TwitterButton = props => { 21 | const { 22 | size, 23 | lang, 24 | dnt, 25 | text, 26 | url, 27 | hashtags, 28 | via, 29 | related, 30 | buttonTitle, 31 | style, 32 | ...remainingProps 33 | } = props; 34 | const params = [ 35 | `size=${size}`, 36 | "count=none", 37 | `dnt=${dnt}`, 38 | `lang=${lang}`, 39 | (typeof text !== "undefined" && `text=${encodeURIComponent(text)}`) || 40 | undefined, 41 | (typeof url !== "undefined" && `url=${encodeURIComponent(url)}`) || 42 | undefined, 43 | (typeof hashtags !== "undefined" && 44 | `hashtags=${encodeURIComponent(hashtags)}`) || 45 | undefined, 46 | (typeof via !== "undefined" && `via=${encodeURIComponent(via)}`) || 47 | undefined, 48 | (typeof related !== "undefined" && 49 | `related=${encodeURIComponent(related)}`) || 50 | undefined 51 | ] 52 | .filter(item => item !== undefined) 53 | .join("&"); 54 | const mergedStyles = { 55 | border: 0, 56 | overflow: "hidden", 57 | ...style 58 | }; 59 | return ( 60 | 69 | ); 70 | }; 71 | 72 | TwitterButton.propTypes = { 73 | size: PropTypes.oneOf(["l", "large"]).isRequired, // no default for the moment, only large 74 | lang: PropTypes.string.isRequired, 75 | dnt: PropTypes.bool.isRequired, 76 | text: PropTypes.string, 77 | url: PropTypes.string, 78 | hashtags: PropTypes.string, 79 | via: PropTypes.string, 80 | related: PropTypes.string, 81 | buttonTitle: PropTypes.string.isRequired, 82 | className: PropTypes.string, 83 | style: PropTypes.object 84 | }; 85 | TwitterButton.defaultProps = { 86 | size: "l", 87 | lang: "en", 88 | dnt: false, 89 | buttonTitle: "Twitter Tweet Button", 90 | text: undefined, 91 | url: undefined, 92 | hashtags: undefined, 93 | via: undefined, 94 | related: undefined, 95 | className: undefined, 96 | style: undefined 97 | }; 98 | 99 | export default TwitterButton; 100 | -------------------------------------------------------------------------------- /src/components/Waiting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inspired by https://www.synyx.de/blog/2017-12-14-ux-waiting-komponente/ 3 | * Github https://github.com/bseber/waiting 4 | * 5 | * Don't show the loader until 100ms have passed for better UX 6 | * More about that: https://www.nngroup.com/articles/response-times-3-important-limits/ 7 | */ 8 | 9 | import React from "react"; 10 | import PropTypes from "prop-types"; 11 | 12 | const TIMEOUT = 100; 13 | 14 | export default class Waiting extends React.Component { 15 | static propTypes = { 16 | loader: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), 17 | loading: PropTypes.bool.isRequired, 18 | render: PropTypes.func.isRequired 19 | }; 20 | 21 | static defaultProps = { 22 | loader: () =>
    LOADING ...
    , 23 | loading: true, 24 | render: () => null 25 | }; 26 | 27 | constructor(props) { 28 | super(); 29 | this.state = { 30 | loading: props.loading, 31 | inDecision: props.loading 32 | }; 33 | } 34 | 35 | componentDidMount() { 36 | if (this.state.inDecision) { 37 | this._loadingTimeout = window.setTimeout(() => { 38 | this.setState({ 39 | loading: true, 40 | inDecision: false 41 | }); 42 | }, TIMEOUT); 43 | } 44 | } 45 | 46 | componentWillReceiveProps(nextProps) { 47 | if (nextProps.loading !== this.props.loading) { 48 | window.clearTimeout(this._loadingTimeout); 49 | if (nextProps.loading) { 50 | this.setState({ inDecision: true }); 51 | this._loadingTimeout = window.setTimeout(() => { 52 | this.setState({ 53 | loading: nextProps.loading, 54 | inDecision: false 55 | }); 56 | }, TIMEOUT); 57 | } else { 58 | this.setState({ 59 | loading: false, 60 | inDecision: false 61 | }); 62 | } 63 | } 64 | } 65 | 66 | renderLoader() { 67 | const { loader } = this.props; 68 | return typeof loader === "function" ? loader() : loader; 69 | } 70 | 71 | renderContent() { 72 | if (this.state.inDecision) { 73 | return null; 74 | } else if (this.state.loading) { 75 | return
    {this.renderLoader()}
    ; 76 | } 77 | return this.props.render(); 78 | } 79 | 80 | render() { 81 | return this.renderContent(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/WindowInfos.js: -------------------------------------------------------------------------------- 1 | // inspired by https://github.com/topheman/d3-react-experiments/blob/master/src/components/WindowInfos/Provider.js 2 | 3 | import React, { Component, createContext, forwardRef } from "react"; 4 | import PropTypes from "prop-types"; 5 | import hoistNonReactStatics from "hoist-non-react-statics"; 6 | 7 | import { debounce } from "../utils/helpers"; 8 | 9 | const WindowInfosContext = createContext({}); 10 | 11 | /** Provider part */ 12 | 13 | export class Provider extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | windowWidth: global.width || 960, 18 | windowHeight: global.height || 640 19 | }; 20 | // create the debounced handle resize, to prevent flooding with resize events and pass down some computed width and height to the children 21 | this.debouncedHandleResize = debounce( 22 | () => 23 | this.setState({ 24 | windowWidth: window.innerWidth, 25 | windowHeight: window.innerHeight 26 | }), 27 | props.debounceTime 28 | ); 29 | } 30 | 31 | componentDidMount() { 32 | window.addEventListener("resize", this.debouncedHandleResize); 33 | // update the state once mounted to pass window size to children on the very first render 34 | setTimeout(() => { 35 | this.setState({ 36 | windowWidth: window.innerWidth, 37 | windowHeight: window.innerHeight 38 | }); 39 | }, 0); 40 | } 41 | 42 | componentWillUnmount() { 43 | // cleanup after unmount 44 | window.removeEventListener("resize", this.debouncedHandleResize); 45 | } 46 | 47 | render() { 48 | return ( 49 | 50 | {this.props.children} 51 | 52 | ); 53 | } 54 | } 55 | Provider.propTypes = { 56 | debounceTime: PropTypes.number.isRequired, 57 | children: PropTypes.element.isRequired 58 | }; 59 | Provider.defaultProps = { 60 | debounceTime: 500 61 | }; 62 | 63 | /** Component with render props part */ 64 | 65 | export const ConnectedWindowInfos = ({ render }) => ( 66 | 67 | {props => render(props)} 68 | 69 | ); 70 | ConnectedWindowInfos.propTypes = { 71 | render: PropTypes.func.isRequired 72 | }; 73 | 74 | /** Higher Order Component (HOC) part */ 75 | 76 | export const withWindowInfos = () => Comp => { 77 | // second argument `ref` injected via `forwardRef` 78 | function Wrapper(props, ref) { 79 | return ( 80 | ( 82 | 83 | )} 84 | /> 85 | ); 86 | } 87 | Wrapper.displayName = `withWindowInfos(${Comp.displayName || 88 | Comp.name || 89 | "Component"})`; 90 | const WrapperWithRef = forwardRef(Wrapper); 91 | hoistNonReactStatics(WrapperWithRef, Comp); 92 | WrapperWithRef.WrappedComponent = Comp; 93 | return WrapperWithRef; 94 | }; 95 | -------------------------------------------------------------------------------- /src/components/__tests__/CodeBlock.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "../../testUtils"; 3 | 4 | import CodeBlock from "../CodeBlock"; 5 | 6 | describe("/components/CodeBlock", () => { 7 | describe("render", () => { 8 | it("should pass down remaining props", () => { 9 | const { container } = render( 10 | 16 | ); 17 | expect(container.firstChild).toHaveClass("hello-world"); 18 | expect(container.firstChild.style.color).toBe("red"); 19 | expect(container.firstChild.getAttribute("data-infos")).toBe( 20 | "Hello world" 21 | ); 22 | }); 23 | it("should pass value as child", () => { 24 | const { getByText } = render( 25 | 26 | ); 27 | expect(getByText("some code")).toBeTruthy(); 28 | }); 29 | it("should set className on element according to props.language", () => { 30 | const { getByText } = render( 31 | 32 | ); 33 | expect(getByText("some code")).toHaveClass("language-js"); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/__tests__/Footer.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "../../testUtils"; 3 | 4 | import Footer from "../Footer"; 5 | 6 | describe("/components/Footer", () => { 7 | describe("render", () => { 8 | it("should pass down remainingProps", () => { 9 | const { container } = render( 10 |