├── config ├── jest │ ├── CSSStub.js │ └── FileStub.js ├── polyfills.js ├── env.js ├── paths.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .travis.yml ├── public ├── favicon.ico └── index.html ├── src ├── components │ ├── frame │ │ ├── frame.css │ │ └── index.js │ ├── favicon.svg │ ├── tab-close.css │ ├── tab-close.js │ ├── star.js │ ├── setting.js │ ├── back.js │ ├── forward.js │ ├── reload.js │ ├── tabs │ │ ├── tabs.css │ │ └── index.js │ ├── tab │ │ ├── index.js │ │ └── tab.css │ └── navigator │ │ ├── input.js │ │ ├── index.js │ │ └── navigator.css ├── reducers │ ├── index.js │ ├── active-tab.js │ ├── is-input-focus.js │ ├── input.js │ └── tabs.js ├── containers │ ├── app.css │ └── app.js ├── index.js ├── util.js └── actions │ └── index.js ├── .gitignore ├── scripts ├── test.js ├── build.js └── start.js ├── LICENSE ├── README.md └── package.json /config/jest/CSSStub.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /config/jest/FileStub.js: -------------------------------------------------------------------------------- 1 | module.exports = "test-file-stub"; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | script: yarn lint 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wujunze/chrome-ui/master/public/favicon.ico -------------------------------------------------------------------------------- /src/components/frame/frame.css: -------------------------------------------------------------------------------- 1 | .content { 2 | flex-grow: 1; 3 | display: flex; 4 | 5 | & iframe { 6 | border: 0; 7 | width: 100% 8 | } 9 | } 10 | 11 | .hidden { 12 | display: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | yarn-error.log 17 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import tabs from './tabs' 3 | import activeTab from './active-tab' 4 | import input from './input' 5 | import isInputFocus from './is-input-focus' 6 | 7 | export default combineReducers({ 8 | tabs, 9 | activeTab, 10 | input, 11 | isInputFocus, 12 | }) 13 | -------------------------------------------------------------------------------- /src/components/tab-close.css: -------------------------------------------------------------------------------- 1 | .close { 2 | width: 12px; 3 | height: 12px; 4 | border-radius: 50%; 5 | z-index: 100; 6 | align-self: center; 7 | margin-left: 2px; 8 | margin-right: 2px; 9 | & path { 10 | fill: #555; 11 | } 12 | &:hover { 13 | background: #e25c4d; 14 | & path { 15 | fill: #fff; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/reducers/active-tab.js: -------------------------------------------------------------------------------- 1 | import { ADD_TAB, SELECT_TAB, CLOSE_TAB } from '../actions' 2 | 3 | export default function activeTab(state = 0, action) { 4 | switch (action.type) { 5 | case ADD_TAB: 6 | case SELECT_TAB: 7 | return action.id 8 | case CLOSE_TAB: 9 | return action.activeId 10 | default: 11 | return state 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/tab-close.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import style from './tab-close.css' 4 | 5 | const TabClose = props => ( 6 | 11 | 12 | 13 | ) 14 | 15 | export default TabClose 16 | -------------------------------------------------------------------------------- /src/components/star.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | 4 | const Star = () => ( 5 | 6 | 7 | 8 | ) 9 | 10 | export default Star 11 | -------------------------------------------------------------------------------- /src/reducers/is-input-focus.js: -------------------------------------------------------------------------------- 1 | import { FOCUS_INPUT, BLUR_INPUT, ADD_TAB, SELECT_TAB, CLOSE_TAB } from '../actions' 2 | 3 | export default function isInputFocus(state = false, action) { 4 | switch (action.type) { 5 | case FOCUS_INPUT: 6 | case ADD_TAB: 7 | return true 8 | case BLUR_INPUT: 9 | case SELECT_TAB: 10 | case CLOSE_TAB: 11 | return false 12 | default: 13 | return state 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/reducers/input.js: -------------------------------------------------------------------------------- 1 | import { CHANGE_INPUT, SELECT_TAB, ADD_TAB, FOCUS_INPUT, CLOSE_TAB } from '../actions' 2 | 3 | export default function input(state = '', action) { 4 | switch (action.type) { 5 | case CHANGE_INPUT: 6 | case FOCUS_INPUT: 7 | return action.value 8 | case SELECT_TAB: 9 | return action.url 10 | case CLOSE_TAB: 11 | return action.url 12 | case ADD_TAB: 13 | return '' 14 | default: 15 | return state 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/setting.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | 4 | const Setting = () => ( 5 | 6 | 7 | 8 | ) 9 | 10 | export default Setting 11 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | process.env.NODE_ENV = 'test'; 3 | process.env.PUBLIC_URL = ''; 4 | 5 | // Load environment variables from .env file. Suppress warnings using silent 6 | // if this file is missing. dotenv will never modify any environment variables 7 | // that have already been set. 8 | // https://github.com/motdotla/dotenv 9 | require('dotenv').config({silent: true}); 10 | 11 | const jest = require('jest'); 12 | const argv = process.argv.slice(2); 13 | 14 | // Watch unless on CI 15 | if (!process.env.CI) { 16 | argv.push('--watch'); 17 | } 18 | 19 | 20 | jest.run(argv); 21 | -------------------------------------------------------------------------------- /src/components/back.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | 4 | const Back = () => ( 5 | 6 | 7 | 8 | ) 9 | 10 | export default Back 11 | -------------------------------------------------------------------------------- /src/components/forward.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | 4 | const Forward = () => ( 5 | 6 | 7 | 8 | ) 9 | 10 | export default Forward 11 | -------------------------------------------------------------------------------- /src/components/reload.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | 4 | const Reload = () => ( 5 | 6 | 7 | 8 | ) 9 | 10 | export default Reload 11 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/containers/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --border-radius: 4px; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; /* Copied from GitHub */ 9 | } 10 | 11 | html, body { 12 | height: 100%; 13 | } 14 | 15 | ul { 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | li { 21 | list-style: none; 22 | } 23 | 24 | :global(#root) { 25 | height: 100%; 26 | padding: 24px; 27 | } 28 | 29 | *, *::before, *::after { 30 | box-sizing: border-box; 31 | } 32 | 33 | .app { 34 | display: flex; 35 | flex-direction: column; 36 | height: 100%; 37 | border-radius: var(--border-radius); 38 | border: 1px solid #c1c1c1; 39 | box-shadow: 0 0 20px #aaa; 40 | } 41 | 42 | .content { 43 | flex-grow: 1; 44 | display: flex; 45 | } 46 | -------------------------------------------------------------------------------- /src/containers/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { pick } from 'lodash' 4 | import style from './app.css' 5 | import Tabs from '../components/tabs' 6 | import Navigator from '../components/navigator' 7 | import Frame from '../components/frame' 8 | 9 | const App = props => ( 10 |
11 | 12 | 13 | 14 |
15 | ) 16 | 17 | const mapStateToProps = state => ({ 18 | tabs: Object.keys(state.tabs).map(id => ({ 19 | id, 20 | ...state.tabs[id], 21 | isActive: state.activeTab === id, 22 | })), 23 | url: state.tabs[state.activeTab].url, 24 | isInputFocus: state.isInputFocus, 25 | input: state.input, 26 | }) 27 | 28 | export default connect(mapStateToProps)(App) 29 | -------------------------------------------------------------------------------- /src/components/frame/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import classNames from 'classnames' 3 | import { completeUrl } from '../../util' 4 | import { loadSuccess } from '../../actions' 5 | import style from './frame.css' 6 | 7 | const handleLoad = ({ dispatch, id }) => () => dispatch(loadSuccess(id)) 8 | 9 | const Frame = ({ tabs, dispatch }) => ( 10 |
11 | {tabs.map(({ id, url, isActive }) => ( 12 | ' 43 | } 44 | 45 | // Send analytic event 46 | export function sendEvent(...args) { 47 | if (window.ga) { 48 | window.ga('send', 'event', ...args) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/navigator/navigator.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --hover: #ccc; 3 | --border: #bbb; 4 | --nav: #f2f2f2; 5 | --icon: #6d6d6d; 6 | --icon-hover: #dfdfdf; 7 | --icon-focus: #aaa; 8 | } 9 | 10 | .nav { 11 | display: flex; 12 | align-items: center; 13 | background: var(--nav); 14 | border-top: 1px solid var(--border); 15 | border-bottom: 1px solid #ddd; 16 | padding-left: 4px; 17 | padding-right: 4px; 18 | height: 38px; 19 | & a { 20 | border-radius: 2px; 21 | margin-left: 2px; 22 | margin-right: 2px; 23 | padding: 4px; 24 | height: 24px; 25 | &:hover { 26 | background: var(--icon-hover); 27 | } 28 | &:focus { 29 | background: var(--icon-focus); 30 | } 31 | } 32 | & svg { 33 | width: 16px; 34 | height: 16px; 35 | & path { 36 | fill: var(--icon); 37 | } 38 | } 39 | & form { 40 | flex-grow: 1; 41 | display: flex; 42 | align-items: center; 43 | background: #fff; 44 | height: 28px; 45 | border-radius: 4px; 46 | margin-left: 4px; 47 | margin-right: 2px; 48 | border: 1px solid #ccc; 49 | &.active { 50 | border: 1px solid #399df7; 51 | height: 26px; 52 | } 53 | & svg { 54 | margin-right: 4px; 55 | } 56 | & input { 57 | display: block; 58 | border: none; 59 | width: 100%; 60 | padding-left: 20px; 61 | font-size: 13.6px; 62 | &:focus { 63 | outline: none; 64 | } 65 | &:-webkit-autofill { 66 | background-color: #fff; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Chrome UI 17 | 18 | 23 | 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | var appDirectory = fs.realpathSync(process.cwd()); 7 | function resolveApp(relativePath) { 8 | return path.resolve(appDirectory, relativePath); 9 | } 10 | 11 | // We support resolving modules according to `NODE_PATH`. 12 | // This lets you use absolute paths in imports inside large monorepos: 13 | // https://github.com/facebookincubator/create-react-app/issues/253. 14 | 15 | // It works similar to `NODE_PATH` in Node itself: 16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 17 | 18 | // We will export `nodePaths` as an array of absolute paths. 19 | // It will then be used by Webpack configs. 20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 21 | 22 | var nodePaths = (process.env.NODE_PATH || '') 23 | .split(process.platform === 'win32' ? ';' : ':') 24 | .filter(Boolean) 25 | .map(resolveApp); 26 | 27 | // config after eject: we're in ./config/ 28 | module.exports = { 29 | appBuild: resolveApp('build'), 30 | appPublic: resolveApp('public'), 31 | appHtml: resolveApp('public/index.html'), 32 | appIndexJs: resolveApp('src/index.js'), 33 | appPackageJson: resolveApp('package.json'), 34 | appSrc: resolveApp('src'), 35 | testsSetup: resolveApp('src/setupTests.js'), 36 | appNodeModules: resolveApp('node_modules'), 37 | ownNodeModules: resolveApp('node_modules'), 38 | nodePaths: nodePaths 39 | }; 40 | 41 | 42 | 43 | // config before publish: we're in ./packages/react-scripts/config/ 44 | if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1) { 45 | module.exports = { 46 | appBuild: resolveOwn('../../../build'), 47 | appPublic: resolveOwn('../template/public'), 48 | appHtml: resolveOwn('../template/public/index.html'), 49 | appIndexJs: resolveOwn('../template/src/index.js'), 50 | appPackageJson: resolveOwn('../package.json'), 51 | appSrc: resolveOwn('../template/src'), 52 | testsSetup: resolveOwn('../template/src/setupTests.js'), 53 | appNodeModules: resolveOwn('../node_modules'), 54 | ownNodeModules: resolveOwn('../node_modules'), 55 | nodePaths: nodePaths 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/tab/tab.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: #dbdbdb; 3 | --hover: #eee; 4 | --border: #bbb; 5 | --nav: #f2f2f2; 6 | } 7 | 8 | .tab { 9 | flex-basis: 218px; 10 | display: flex; 11 | min-width: 0; /* http://stackoverflow.com/questions/34934586/white-space-nowrap-and-flexbox-did-not-work-in-chrome */ 12 | align-items: center; 13 | position: relative; 14 | background-color: var(--background); 15 | border-top: 1px solid var(--border); 16 | margin: 0 5px; 17 | font-size: 0; 18 | transition: background-color .5s; 19 | &:hover { 20 | background: var(--hover); 21 | &::before, &::after { 22 | background: var(--hover); 23 | } 24 | } 25 | &.active { 26 | background: var(--nav); 27 | height: 29px; 28 | border-bottom: 1px solid var(--nav); 29 | transition: all 0s; 30 | &::before, &::after { 31 | z-index: 10; 32 | transition: all 0s; 33 | align-self: flex-start; 34 | height: 28px; 35 | background: var(--nav); 36 | border-bottom: 1px solid var(--nav); 37 | } 38 | } 39 | & .favicon { 40 | z-index: 100; 41 | width: 16px; 42 | height: 16px; 43 | margin-left: 4px; 44 | margin-right: 4px; 45 | user-select: none; 46 | } 47 | & .close { 48 | fill: #000; 49 | width: 12px; 50 | height: 12px; 51 | border-radius: 50%; 52 | &:hover { 53 | fill: #fff; 54 | background: #f00; 55 | } 56 | } 57 | &::before { 58 | content: ''; 59 | position: absolute; 60 | z-index: 0; 61 | left: 0; 62 | width: 16px; 63 | height: 28px; 64 | background-color: var(--background); 65 | border-left: 1px solid var(--border); 66 | border-bottom: 1px solid var(--border); 67 | transform: skewx(-25deg); 68 | transform-origin: left top; 69 | transition: background-color .5s; 70 | } 71 | &::after { 72 | content: ''; 73 | position: absolute; 74 | z-index: 1; 75 | right: 0; 76 | width: 16px; 77 | height: 28px; 78 | background-color: var(--background); 79 | border-right: 1px solid var(--border); 80 | border-bottom: 1px solid var(--border); 81 | transform: skewx(25deg); 82 | transform-origin: right top; 83 | transition: background-color .5s; 84 | } 85 | 86 | & .content { 87 | flex-grow: 1; 88 | padding-left: 2px; 89 | font-size: 12.6px; 90 | line-height: 28px; 91 | cursor: default; 92 | max-width: 200px; 93 | user-select: none; 94 | white-space: nowrap; 95 | overflow: hidden; 96 | text-overflow: clip; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid' 2 | 3 | export const ADD_TAB = 'ADD_TAB' 4 | export const CLOSE_TAB = 'CLOSE_TAB' 5 | export const SELECT_TAB = 'SELECT_TAB' 6 | export const UPDATE_URL = 'UPDATE_TITLE' 7 | export const CHANGE_INPUT = 'CHANGE_INPUT' 8 | export const FOCUS_INPUT = 'FOCUS_INPUT' 9 | export const BLUR_INPUT = 'BLUR_INPUT' 10 | 11 | export const LOAD_START = 'LOAD_START' 12 | export const LOAD_SUCCESS = 'LOAD_SUCCESS' 13 | export const LOAD_FAIL = 'LOAD_FAIL' 14 | 15 | export const addTab = () => { 16 | // This is an ugly hack. 17 | // When new tab is opened, location input should focus. 18 | // It is too complex to do this in React way. 🌧 19 | document.querySelector('#location').focus() 20 | return { 21 | type: ADD_TAB, 22 | id: v4(), 23 | } 24 | } 25 | 26 | export const closeTab = id => (dispatch, getState) => { 27 | const state = getState() 28 | const { tabs } = state 29 | const allIds = Object.keys(tabs) 30 | 31 | // Do not close last tab 32 | if (allIds.length <= 1) { 33 | alert('This is the last tab :)') // eslint-disable-line 34 | return 35 | } 36 | 37 | // When active tab is closed, make right tab active 38 | // If no right, make left active 39 | let activeId = state.activeTab 40 | if (id === state.activeTab) { 41 | const position = allIds.indexOf(id) 42 | if (position < allIds.length - 1) { 43 | activeId = allIds[position + 1] 44 | } else { 45 | activeId = allIds[position - 1] 46 | } 47 | } 48 | 49 | dispatch({ 50 | type: CLOSE_TAB, 51 | id, 52 | activeId, 53 | url: tabs[activeId].url, 54 | }) 55 | } 56 | 57 | export const selectTab = id => (dispatch, getState) => dispatch({ 58 | type: SELECT_TAB, 59 | id, 60 | url: getState().tabs[id].url, 61 | }) 62 | 63 | export const updateUrl = ({ id, url }) => ({ 64 | type: UPDATE_URL, 65 | id, 66 | url, 67 | }) 68 | 69 | export const changeInput = value => ({ 70 | type: CHANGE_INPUT, 71 | value, 72 | }) 73 | 74 | export const focusInput = value => ({ 75 | type: FOCUS_INPUT, 76 | value, 77 | }) 78 | 79 | export const blurInput = () => ({ 80 | type: BLUR_INPUT, 81 | }) 82 | 83 | const loadStart = ({ id, url }) => ({ 84 | type: LOAD_START, 85 | id, 86 | url, 87 | }) 88 | 89 | export const loadSuccess = id => ({ 90 | type: LOAD_SUCCESS, 91 | id, 92 | }) 93 | 94 | export const load = () => (dispatch, getState) => { 95 | // Ugly hack either 96 | // Make loaction input blur after submit 97 | document.querySelector('#location').blur() 98 | 99 | const state = getState() 100 | const id = state.activeTab 101 | const url = state.input 102 | dispatch(loadStart({ id, url })) 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-ui", 3 | "version": "1.0.0", 4 | "author": "pd4d10 ", 5 | "homepage": "https://pd4d10.github.io/chrome-ui/", 6 | "devDependencies": { 7 | "autoprefixer": "6.5.1", 8 | "babel-core": "6.17.0", 9 | "babel-eslint": "7.0.0", 10 | "babel-jest": "16.0.0", 11 | "babel-loader": "6.2.5", 12 | "babel-preset-react-app": "^1.0.0", 13 | "case-sensitive-paths-webpack-plugin": "1.1.4", 14 | "chalk": "1.1.3", 15 | "connect-history-api-fallback": "1.3.0", 16 | "cross-spawn": "4.0.2", 17 | "css-loader": "0.25.0", 18 | "detect-port": "1.0.1", 19 | "dotenv": "2.0.0", 20 | "eslint": "^3.9.1", 21 | "eslint-config-airbnb": "^13.0.0", 22 | "eslint-loader": "1.6.0", 23 | "eslint-plugin-flowtype": "2.21.0", 24 | "eslint-plugin-import": "^2.1.0", 25 | "eslint-plugin-jsx-a11y": "2.2.3", 26 | "eslint-plugin-react": "^6.6.0", 27 | "extract-text-webpack-plugin": "1.0.1", 28 | "file-loader": "0.9.0", 29 | "filesize": "3.3.0", 30 | "find-cache-dir": "0.1.1", 31 | "fs-extra": "0.30.0", 32 | "gh-pages": "^0.12.0", 33 | "gzip-size": "3.0.0", 34 | "html-webpack-plugin": "2.24.0", 35 | "http-proxy-middleware": "0.17.2", 36 | "jest": "16.0.2", 37 | "json-loader": "0.5.4", 38 | "object-assign": "4.1.0", 39 | "path-exists": "2.1.0", 40 | "postcss-cssnext": "^2.9.0", 41 | "postcss-import": "^9.0.0", 42 | "postcss-loader": "1.0.0", 43 | "promise": "7.1.1", 44 | "react-dev-utils": "^0.3.0", 45 | "recursive-readdir": "2.1.0", 46 | "rimraf": "2.5.4", 47 | "strip-ansi": "3.0.1", 48 | "style-loader": "0.13.1", 49 | "url-loader": "0.5.7", 50 | "webpack": "1.13.2", 51 | "webpack-dev-server": "1.16.2", 52 | "webpack-manifest-plugin": "1.1.0", 53 | "whatwg-fetch": "1.0.0" 54 | }, 55 | "dependencies": { 56 | "classnames": "^2.2.5", 57 | "history": "^4.5.0", 58 | "lodash": "^4.17.4", 59 | "react": "^15.4.0", 60 | "react-dom": "^15.4.0", 61 | "react-redux": "^4.4.6", 62 | "redux": "^3.6.0", 63 | "redux-thunk": "^2.1.0", 64 | "seamless-immutable": "^7.0.1", 65 | "uuid": "^3.0.1" 66 | }, 67 | "scripts": { 68 | "start": "node scripts/start.js", 69 | "build": "node scripts/build.js", 70 | "test": "node scripts/test.js --env=jsdom", 71 | "lint": "eslint src/*", 72 | "deploy": "gh-pages -d build" 73 | }, 74 | "jest": { 75 | "moduleFileExtensions": [ 76 | "jsx", 77 | "js", 78 | "json" 79 | ], 80 | "moduleNameMapper": { 81 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/FileStub.js", 82 | "^.+\\.css$": "/config/jest/CSSStub.js" 83 | }, 84 | "setupFiles": [ 85 | "/config/polyfills.js" 86 | ], 87 | "testPathIgnorePatterns": [ 88 | "/(build|docs|node_modules)/" 89 | ], 90 | "testEnvironment": "node" 91 | }, 92 | "babel": { 93 | "presets": [ 94 | "react-app" 95 | ] 96 | }, 97 | "eslintConfig": { 98 | "extends": "airbnb", 99 | "env": { 100 | "browser": true 101 | }, 102 | "rules": { 103 | "semi": [ 104 | "error", 105 | "never" 106 | ], 107 | "react/jsx-filename-extension": "off" 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // Do this as the first thing so that any code reading it knows the right env. 3 | process.env.NODE_ENV = 'production'; 4 | 5 | // Load environment variables from .env file. Suppress warnings using silent 6 | // if this file is missing. dotenv will never modify any environment variables 7 | // that have already been set. 8 | // https://github.com/motdotla/dotenv 9 | require('dotenv').config({silent: true}); 10 | 11 | var chalk = require('chalk'); 12 | var fs = require('fs-extra'); 13 | var path = require('path'); 14 | var filesize = require('filesize'); 15 | var gzipSize = require('gzip-size').sync; 16 | var rimrafSync = require('rimraf').sync; 17 | var webpack = require('webpack'); 18 | var config = require('../config/webpack.config.prod'); 19 | var paths = require('../config/paths'); 20 | var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 21 | var recursive = require('recursive-readdir'); 22 | var stripAnsi = require('strip-ansi'); 23 | 24 | // Warn and crash if required files are missing 25 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 26 | process.exit(1); 27 | } 28 | 29 | // Input: /User/dan/app/build/static/js/main.82be8.js 30 | // Output: /static/js/main.js 31 | function removeFileNameHash(fileName) { 32 | return fileName 33 | .replace(paths.appBuild, '') 34 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); 35 | } 36 | 37 | // Input: 1024, 2048 38 | // Output: "(+1 KB)" 39 | function getDifferenceLabel(currentSize, previousSize) { 40 | var FIFTY_KILOBYTES = 1024 * 50; 41 | var difference = currentSize - previousSize; 42 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; 43 | if (difference >= FIFTY_KILOBYTES) { 44 | return chalk.red('+' + fileSize); 45 | } else if (difference < FIFTY_KILOBYTES && difference > 0) { 46 | return chalk.yellow('+' + fileSize); 47 | } else if (difference < 0) { 48 | return chalk.green(fileSize); 49 | } else { 50 | return ''; 51 | } 52 | } 53 | 54 | // First, read the current file sizes in build directory. 55 | // This lets us display how much they changed later. 56 | recursive(paths.appBuild, (err, fileNames) => { 57 | var previousSizeMap = (fileNames || []) 58 | .filter(fileName => /\.(js|css)$/.test(fileName)) 59 | .reduce((memo, fileName) => { 60 | var contents = fs.readFileSync(fileName); 61 | var key = removeFileNameHash(fileName); 62 | memo[key] = gzipSize(contents); 63 | return memo; 64 | }, {}); 65 | 66 | // Remove all content but keep the directory so that 67 | // if you're in it, you don't end up in Trash 68 | rimrafSync(paths.appBuild + '/*'); 69 | 70 | // Start the webpack build 71 | build(previousSizeMap); 72 | 73 | // Merge with the public folder 74 | copyPublicFolder(); 75 | }); 76 | 77 | // Print a detailed summary of build files. 78 | function printFileSizes(stats, previousSizeMap) { 79 | var assets = stats.toJson().assets 80 | .filter(asset => /\.(js|css)$/.test(asset.name)) 81 | .map(asset => { 82 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); 83 | var size = gzipSize(fileContents); 84 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)]; 85 | var difference = getDifferenceLabel(size, previousSize); 86 | return { 87 | folder: path.join('build', path.dirname(asset.name)), 88 | name: path.basename(asset.name), 89 | size: size, 90 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '') 91 | }; 92 | }); 93 | assets.sort((a, b) => b.size - a.size); 94 | var longestSizeLabelLength = Math.max.apply(null, 95 | assets.map(a => stripAnsi(a.sizeLabel).length) 96 | ); 97 | assets.forEach(asset => { 98 | var sizeLabel = asset.sizeLabel; 99 | var sizeLength = stripAnsi(sizeLabel).length; 100 | if (sizeLength < longestSizeLabelLength) { 101 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); 102 | sizeLabel += rightPadding; 103 | } 104 | console.log( 105 | ' ' + sizeLabel + 106 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) 107 | ); 108 | }); 109 | } 110 | 111 | // Print out errors 112 | function printErrors(summary, errors) { 113 | console.log(chalk.red(summary)); 114 | console.log(); 115 | errors.forEach(err => { 116 | console.log(err.message || err); 117 | console.log(); 118 | }); 119 | } 120 | 121 | // Create the production build and print the deployment instructions. 122 | function build(previousSizeMap) { 123 | console.log('Creating an optimized production build...'); 124 | webpack(config).run((err, stats) => { 125 | if (err) { 126 | printErrors('Failed to compile.', [err]); 127 | process.exit(1); 128 | } 129 | 130 | if (stats.compilation.errors.length) { 131 | printErrors('Failed to compile.', stats.compilation.errors); 132 | process.exit(1); 133 | } 134 | 135 | console.log(chalk.green('Compiled successfully.')); 136 | console.log(); 137 | 138 | console.log('File sizes after gzip:'); 139 | console.log(); 140 | printFileSizes(stats, previousSizeMap); 141 | console.log(); 142 | 143 | var openCommand = process.platform === 'win32' ? 'start' : 'open'; 144 | var homepagePath = require(paths.appPackageJson).homepage; 145 | var publicPath = config.output.publicPath; 146 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { 147 | // "homepage": "http://user.github.io/project" 148 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 149 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 150 | console.log(); 151 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 152 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); 153 | console.log(); 154 | console.log(' ' + chalk.cyan('npm') + ' install --save-dev gh-pages'); 155 | console.log(); 156 | console.log('Add the following script in your ' + chalk.cyan('package.json') + '.'); 157 | console.log(); 158 | console.log(' ' + chalk.dim('// ...')); 159 | console.log(' ' + chalk.yellow('"scripts"') + ': {'); 160 | console.log(' ' + chalk.dim('// ...')); 161 | console.log(' ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"gh-pages -d build"')); 162 | console.log(' }'); 163 | console.log(); 164 | console.log('Then run:'); 165 | console.log(); 166 | console.log(' ' + chalk.cyan('npm') + ' run deploy'); 167 | console.log(); 168 | } else if (publicPath !== '/') { 169 | // "homepage": "http://mywebsite.com/project" 170 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 171 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 172 | console.log(); 173 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 174 | console.log(); 175 | } else { 176 | // no homepage or "homepage": "http://mywebsite.com" 177 | console.log('The project was built assuming it is hosted at the server root.'); 178 | if (homepagePath) { 179 | // "homepage": "http://mywebsite.com" 180 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 181 | console.log(); 182 | } else { 183 | // no homepage 184 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); 185 | console.log('For example, add this to build it for GitHub Pages:') 186 | console.log(); 187 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); 188 | console.log(); 189 | } 190 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 191 | console.log('You may also serve it locally with a static server:') 192 | console.log(); 193 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); 194 | console.log(' ' + chalk.cyan('pushstate-server') + ' build'); 195 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); 196 | console.log(); 197 | } 198 | }); 199 | } 200 | 201 | function copyPublicFolder() { 202 | fs.copySync(paths.appPublic, paths.appBuild, { 203 | dereference: true, 204 | filter: file => file !== paths.appHtml 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const autoprefixer = require('autoprefixer') 3 | const webpack = require('webpack') 4 | const findCacheDir = require('find-cache-dir') 5 | const HtmlWebpackPlugin = require('html-webpack-plugin') 6 | const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin') 7 | const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin') 8 | const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin') 9 | const getClientEnvironment = require('./env') 10 | const paths = require('./paths') 11 | 12 | // Webpack uses `publicPath` to determine where the app is being served from. 13 | // In development, we always serve from the root. This makes config easier. 14 | const publicPath = '/' 15 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 16 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 17 | // Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 18 | const publicUrl = '' 19 | // Get environment variables to inject into our app. 20 | const env = getClientEnvironment(publicUrl) 21 | 22 | // This is the development configuration. 23 | // It is focused on developer experience and fast rebuilds. 24 | // The production configuration is different and lives in a separate file. 25 | module.exports = { 26 | // This makes the bundle appear split into separate modules in the devtools. 27 | // We don't use source maps here because they can be confusing: 28 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875 29 | // You may want 'cheap-module-source-map' instead if you prefer source maps. 30 | devtool: 'eval', 31 | // These are the "entry points" to our application. 32 | // This means they will be the "root" imports that are included in JS bundle. 33 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 34 | entry: [ 35 | // Include an alternative client for WebpackDevServer. A client's job is to 36 | // connect to WebpackDevServer by a socket and get notified about changes. 37 | // When you save a file, the client will either apply hot updates (in case 38 | // of CSS changes), or refresh the page (in case of JS changes). When you 39 | // make a syntax error, this client will display a syntax error overlay. 40 | // Note: instead of the default WebpackDevServer client, we use a custom one 41 | // to bring better experience for Create React App users. You can replace 42 | // the line below with these two lines if you prefer the stock client: 43 | // require.resolve('webpack-dev-server/client') + '?/', 44 | // require.resolve('webpack/hot/dev-server'), 45 | require.resolve('react-dev-utils/webpackHotDevClient'), 46 | // We ship a few polyfills by default: 47 | require.resolve('./polyfills'), 48 | // Finally, this is your app's code: 49 | paths.appIndexJs, 50 | // We include the app code last so that if there is a runtime error during 51 | // initialization, it doesn't blow up the WebpackDevServer client, and 52 | // changing JS code would still trigger a refresh. 53 | ], 54 | output: { 55 | // Next line is not used in dev but WebpackDevServer crashes without it: 56 | path: paths.appBuild, 57 | // Add /* filename */ comments to generated require()s in the output. 58 | pathinfo: true, 59 | // This does not produce a real file. It's just the virtual path that is 60 | // served by WebpackDevServer in development. This is the JS bundle 61 | // containing code from all our entry points, and the Webpack runtime. 62 | filename: 'static/js/bundle.js', 63 | // This is the URL that app is served from. We use "/" in development. 64 | publicPath, 65 | }, 66 | resolve: { 67 | // This allows you to set a fallback for where Webpack should look for modules. 68 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. 69 | // We use `fallback` instead of `root` because we want `node_modules` to "win" 70 | // if there any conflicts. This matches Node resolution mechanism. 71 | // https://github.com/facebookincubator/create-react-app/issues/253 72 | fallback: paths.nodePaths, 73 | // These are the reasonable defaults supported by the Node ecosystem. 74 | // We also include JSX as a common component filename extension to support 75 | // some tools, although we do not recommend using it, see: 76 | // https://github.com/facebookincubator/create-react-app/issues/290 77 | extensions: ['.js', '.json', '.jsx', ''], 78 | alias: { 79 | // Support React Native Web 80 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 81 | 'react-native': 'react-native-web', 82 | }, 83 | }, 84 | 85 | module: { 86 | // First, run the linter. 87 | // It's important to do this before Babel processes the JS. 88 | preLoaders: [ 89 | { 90 | test: /\.(js|jsx)$/, 91 | loader: 'eslint', 92 | include: paths.appSrc, 93 | }, 94 | ], 95 | loaders: [ 96 | // Process JS with Babel. 97 | { 98 | test: /\.(js|jsx)$/, 99 | include: paths.appSrc, 100 | loader: 'babel', 101 | query: { 102 | 103 | // This is a feature of `babel-loader` for webpack (not Babel itself). 104 | // It enables caching results in ./node_modules/.cache/react-scripts/ 105 | // directory for faster rebuilds. We use findCacheDir() because of: 106 | // https://github.com/facebookincubator/create-react-app/issues/483 107 | cacheDirectory: findCacheDir({ 108 | name: 'react-scripts', 109 | }), 110 | }, 111 | }, 112 | // "postcss" loader applies autoprefixer to our CSS. 113 | // "css" loader resolves paths in CSS and adds assets as dependencies. 114 | // "style" loader turns CSS into JS modules that inject