├── src ├── assets │ ├── sass │ │ ├── _layout.scss │ │ └── index.scss │ └── img │ │ └── sky.jpg ├── __tests__ │ ├── App.js │ ├── registerServiceWorker.js │ ├── HomeView.js │ ├── CustomHome.js │ ├── AsyncLoad.js │ ├── Login.js │ ├── fakeAuth.js │ ├── PrivateRoute.js │ ├── ZenPage.js │ ├── Navbar.js │ ├── Loading.js │ └── zen.js ├── setupTests.js ├── components │ ├── AsyncLoad.js │ ├── HomeView.js │ ├── PrivateRoute.js │ ├── Loading.js │ ├── Zen.js │ └── Navbar.js ├── index.js ├── routes │ ├── App.js │ ├── Layout.js │ └── CustomHome.js ├── containers │ ├── ZenContainer.js │ └── Login.js ├── store │ ├── reducers.js │ ├── fakeAuth.js │ └── createStore.js ├── modules │ └── zen.js └── utils │ └── registerServiceWorker.js ├── public ├── favicon.ico ├── manifest.json ├── 404.html └── index.html ├── .travis.yml ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── README-zh.md /src/assets/sass/_layout.scss: -------------------------------------------------------------------------------- 1 | .page-layout__viewport { 2 | padding-top: 4rem; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YutHelloWorld/vortex-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YutHelloWorld/vortex-react/HEAD/src/assets/img/sky.jpg -------------------------------------------------------------------------------- /src/__tests__/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from '../routes/App'; 4 | 5 | it('App', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | const localStorageMock = { 2 | getItem: jest.fn(), 3 | setItem: jest.fn(), 4 | clear: jest.fn() 5 | }; 6 | global.localStorage = localStorageMock; 7 | global.requestAnimationFrame = function(callback) { 8 | setTimeout(callback, 0); 9 | }; 10 | -------------------------------------------------------------------------------- /src/__tests__/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | import register, { unregister } from '../utils/registerServiceWorker'; 2 | 3 | describe('registerServiceWorker.js', () => { 4 | it('not throw', () => { 5 | expect(register).not.toThrow(); 6 | expect(unregister).not.toThrow(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/AsyncLoad.js: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable' 2 | import Loading from './Loading' 3 | 4 | export default function AsyncLoad(opts) { 5 | return Loadable({ 6 | ...opts, 7 | loading: Loading, 8 | delay: 200, 9 | timeout: 2000, 10 | }) 11 | }; 12 | -------------------------------------------------------------------------------- /src/__tests__/HomeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import HomeView from '../components/HomeView'; 4 | 5 | it('HomeView renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/HomeView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import sky from '../assets/img/sky.jpg'; 3 | 4 | function HomeView() { 5 | return ( 6 |
7 | sky 8 |
9 | ); 10 | } 11 | 12 | export default HomeView; 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'bootstrap/dist/css/bootstrap.css'; 4 | import './assets/sass/index.css'; 5 | import App from './routes/App'; 6 | import registerServiceWorker from './utils/registerServiceWorker'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | registerServiceWorker(); 10 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/sass/index.scss: -------------------------------------------------------------------------------- 1 | // Some best-practice CSS that's useful for most apps 2 | // Just remove them if they're not what you want 3 | @import './layout'; 4 | 5 | html { 6 | box-sizing: border-box; 7 | } 8 | 9 | html, 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | height: 100%; 14 | } 15 | 16 | *, 17 | *:before, 18 | *:after { 19 | box-sizing: inherit; 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | cache: 5 | yarn: true 6 | directories: 7 | - node_modules 8 | script: 9 | - yarn add --force node-sass 10 | - yarn build 11 | - yarn coverage 12 | after_script: 13 | - yarn codecov 14 | deploy: 15 | provider: pages 16 | skip_cleanup: true 17 | github_token: $GITHUB_TOKEN # Set in travis-ci.org dashboard 18 | local_dir: build 19 | on: 20 | branch: master -------------------------------------------------------------------------------- /src/__tests__/CustomHome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import { CustomHome } from '../routes/CustomHome'; 5 | 6 | it('(Layout) CustomHome', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render( 9 | 10 | {}} /> 11 | , 12 | div 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /.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 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # css 24 | src/**/*.css 25 | 26 | # chrome 27 | chrome 28 | 29 | #snapshots 30 | __snapshots__ -------------------------------------------------------------------------------- /src/routes/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Route } from 'react-router-dom'; 3 | import { Provider } from 'react-redux'; 4 | import Layout from './Layout'; 5 | import store from '../store/createStore'; 6 | 7 | function App() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | export default App; 18 | -------------------------------------------------------------------------------- /src/__tests__/AsyncLoad.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import AsyncLoad from '../components/AsyncLoad'; 4 | 5 | it('(HoC) AsyncLoad', () => { 6 | const div = document.createElement('div'); 7 | function Component() { 8 | return
hello
; 9 | } 10 | 11 | const AsyncHome = AsyncLoad({ 12 | loader: () => 13 | import(/* webpackChunkName: "zen" */ '../components/HomeView.js') 14 | }); 15 | 16 | ReactDOM.render(, div); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import { Login } from '../containers/Login'; 5 | 6 | it('(Component) Login', () => { 7 | const props = { 8 | signInWithCb() {}, 9 | location: {}, 10 | logged: false 11 | }; 12 | const div = document.createElement('div'); 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | div 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/fakeAuth.js: -------------------------------------------------------------------------------- 1 | import reducer, { signIn, signOut, initialState } from '../store/fakeAuth'; 2 | 3 | describe('(Reducer) fakeAuth', () => { 4 | it('(Action) signIn', () => { 5 | localStorage.clear(); 6 | const state = reducer(initialState, signIn()); 7 | expect(state).toBe(true); 8 | }); 9 | 10 | it('(Action) signOut', () => { 11 | localStorage.setItem('logged', 'true'); 12 | const state = reducer(initialState, signOut()); 13 | expect(state).toBe(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/containers/ZenContainer.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import reducer, { fetchZen, clearZen } from '../modules/zen'; 3 | import { injectReducer } from '../store/reducers'; 4 | import store from '../store/createStore'; 5 | import Zen from '../components/Zen'; 6 | 7 | const mapDispatchToProps = { 8 | fetchZen, 9 | clearZen 10 | }; 11 | 12 | const mapStateToProps = state => ({ 13 | zen: state.zen 14 | }); 15 | 16 | injectReducer(store, { key: 'zen', reducer }); 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(Zen); 19 | -------------------------------------------------------------------------------- /src/store/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import fakeAuthReducer from './fakeAuth'; 3 | 4 | export const makeRootReducer = asyncReducers => { 5 | return combineReducers({ 6 | logged: fakeAuthReducer, 7 | ...asyncReducers 8 | }); 9 | }; 10 | 11 | export const injectReducer = (store, { key, reducer }) => { 12 | if (Object.hasOwnProperty.call(store.asyncReducers, key)) return; 13 | 14 | store.asyncReducers[key] = reducer; 15 | store.replaceReducer(makeRootReducer(store.asyncReducers)); 16 | }; 17 | 18 | export default makeRootReducer; 19 | -------------------------------------------------------------------------------- /src/__tests__/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter, Route } from 'react-router-dom'; 4 | import PrivateRoute from '../components/PrivateRoute'; 5 | 6 | describe('(Component) PrivateRoute', () => { 7 | it('isAuthenticated', () => { 8 | const div = document.createElement('div'); 9 | function Component() { 10 | return
hello
; 11 | } 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | div 17 | ); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/ZenPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Zen from '../components/Zen'; 4 | 5 | describe('(Component) Zen', () => { 6 | it('fetching', () => { 7 | const props = { 8 | fetchZen() {}, 9 | clearZen() {}, 10 | zen: { 11 | fetching: true, 12 | text: [] 13 | } 14 | }; 15 | const div = document.createElement('div'); 16 | ReactDOM.render(, div); 17 | }); 18 | 19 | it('fetched', () => { 20 | const props = { 21 | fetchZen() {}, 22 | clearZen() {}, 23 | zen: { 24 | fetching: false, 25 | text: [{ id: 0, text: 'hello' }] 26 | } 27 | }; 28 | const div = document.createElement('div'); 29 | ReactDOM.render(, div); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/routes/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Switch, Route } from 'react-router-dom'; 4 | import { connect } from 'react-redux'; 5 | 6 | import CustomHome from './CustomHome'; 7 | import Login from '../containers/Login'; 8 | import PrivateRoute from '../components/PrivateRoute'; 9 | 10 | const propTypes = { 11 | logged: PropTypes.bool.isRequired 12 | }; 13 | 14 | function Layout({ logged }) { 15 | return ( 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | Layout.propTypes = propTypes; 24 | 25 | const mapStateToProps = state => ({ 26 | logged: state.logged 27 | }); 28 | 29 | export default connect(mapStateToProps)(Layout); 30 | -------------------------------------------------------------------------------- /src/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | 5 | const propTypes = { 6 | isAuthenticated: PropTypes.bool, 7 | component: PropTypes.func.isRequired 8 | }; 9 | 10 | function PrivateRoute({ 11 | component: Component, 12 | isAuthenticated = false, 13 | ...rest 14 | }) { 15 | return ( 16 | 19 | isAuthenticated ? ( 20 | 21 | ) : ( 22 | 28 | )} 29 | /> 30 | ); 31 | } 32 | 33 | PrivateRoute.propTypes = propTypes; 34 | 35 | export default PrivateRoute; 36 | -------------------------------------------------------------------------------- /src/__tests__/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import renderer from 'react-test-renderer'; 4 | import { HashRouter } from 'react-router-dom'; 5 | import Navbar from '../components/Navbar'; 6 | 7 | it('(Layout) Navbar', () => { 8 | const component = renderer.create( 9 | 10 | {}} /> 11 | 12 | ); 13 | let tree = component.toJSON(); 14 | expect(tree).toMatchSnapshot(); 15 | 16 | // manually trigger the callback 17 | tree.children[2].children[0].children[0].props.onMouseOver(); 18 | // re-rendering 19 | tree = component.toJSON(); 20 | expect(tree).toMatchSnapshot(); 21 | 22 | // manually trigger the callback 23 | tree.children[1].props.onClick(); 24 | // re-rendering 25 | tree = component.toJSON(); 26 | expect(tree).toMatchSnapshot(); 27 | }); 28 | -------------------------------------------------------------------------------- /src/__tests__/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Loading from '../components/Loading'; 4 | 5 | describe('(Component) Loading', () => { 6 | it('renders loading without timed out or pasted delay', () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render(, div); 9 | }); 10 | 11 | it('renders timed out', () => { 12 | const div = document.createElement('div'); 13 | ReactDOM.render(, div); 14 | }); 15 | 16 | it('renders a loading screen', () => { 17 | const div = document.createElement('div'); 18 | ReactDOM.render(, div); 19 | }); 20 | 21 | it('renders a failed to load error', () => { 22 | const div = document.createElement('div'); 23 | ReactDOM.render(, div); 24 | }); 25 | 26 | it('renders without any props', () => { 27 | const div = document.createElement('div'); 28 | ReactDOM.render(, div); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/store/fakeAuth.js: -------------------------------------------------------------------------------- 1 | const SIGN_IN = 'SIGN_IN'; 2 | const SIGN_OUT = 'SIGN_OUT'; 3 | 4 | export function signIn() { 5 | return { 6 | type: SIGN_IN 7 | }; 8 | } 9 | 10 | export function signInWithCb() { 11 | return dispatch => { 12 | dispatch(signIn()); 13 | localStorage.setItem('logged', 'true'); 14 | }; 15 | } 16 | 17 | export function signOut() { 18 | return { 19 | type: SIGN_OUT 20 | }; 21 | } 22 | 23 | export function signOutWithCb(e) { 24 | return dispatch => { 25 | e.preventDefault(); 26 | dispatch(signOut()); 27 | localStorage.setItem('logged', 'false'); 28 | }; 29 | } 30 | 31 | const ACTION_HANDLERS = { 32 | [SIGN_IN]: state => true, 33 | [SIGN_OUT]: state => false 34 | }; 35 | 36 | export const initialState = !( 37 | localStorage.getItem('logged') == null || 38 | localStorage.getItem('logged') === 'false' 39 | ); 40 | 41 | export default function(state = initialState, action) { 42 | const handler = ACTION_HANDLERS[action.type]; 43 | 44 | return handler ? handler(state, action) : state; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { bool } from 'prop-types' 3 | import Spinner from 'react-spinkit' 4 | 5 | const propTypes = { 6 | isLoading: bool, 7 | timedOut: bool, 8 | pastDelay: bool, 9 | error: bool 10 | } 11 | function Loading(props) { 12 | if (props.isLoading) { 13 | // While our other component is loading... 14 | if (props.timedOut) { 15 | // In case we've timed out loading our other component. 16 | return 'Loader timed out!' 17 | } else if (props.pastDelay) { 18 | // Display a loading screen after a set delay. 19 | return 20 | } else { 21 | // Don't flash "Loading..." when we don't need to. 22 | return null 23 | } 24 | } else if (props.error) { 25 | // If we aren't loading, maybe 26 | return 'Error! Component failed to load' 27 | } else { 28 | // This case shouldn't happen... but we'll return null anyways. 29 | return null 30 | } 31 | } 32 | 33 | Loading.propTypes = propTypes 34 | 35 | export default Loading 36 | -------------------------------------------------------------------------------- /src/__tests__/zen.js: -------------------------------------------------------------------------------- 1 | import reducer, { requestZen, receiveZen, clearZen } from '../modules/zen'; 2 | import update, { updateChain } from 'immutability-helper-x'; 3 | 4 | describe('(Redux Module) Zen', () => { 5 | describe('(Reducer)', () => { 6 | it('(Action) requestZen', () => { 7 | let state = { 8 | fetching: false, 9 | text: [] 10 | }; 11 | state = reducer(state, requestZen()); 12 | expect(state).toEqual({ 13 | fetching: true, 14 | text: [] 15 | }); 16 | }); 17 | 18 | it('(Action) receiveZen', () => { 19 | let state = { 20 | fetching: true, 21 | text: [] 22 | }; 23 | state = reducer(state, receiveZen('hello')); 24 | expect(state).toEqual({ 25 | fetching: false, 26 | text: [{ id: 0, text: 'hello' }] 27 | }); 28 | }); 29 | 30 | it('(Action) clearZen', () => { 31 | let state = { 32 | text: [] 33 | }; 34 | state = reducer(state, clearZen()); 35 | expect(state).toEqual({ 36 | text: [] 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/Zen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Spinner from 'react-spinkit'; 4 | import { Button } from 'reactstrap'; 5 | 6 | const propTypes = { 7 | fetchZen: PropTypes.func.isRequired, 8 | clearZen: PropTypes.func.isRequired, 9 | zen: PropTypes.shape({ 10 | fetching: PropTypes.bool.isRequired, 11 | text: PropTypes.array 12 | }).isRequired 13 | }; 14 | 15 | function Zen({ fetchZen, clearZen, zen: { fetching, text } }) { 16 | return ( 17 |
18 | {fetching && ( 19 | 20 | )} 21 |
22 | {' '} 25 | 28 |
29 |
30 |
{text.map(item =>

{item.text}

)}
31 |
32 | ); 33 | } 34 | 35 | Zen.propTypes = propTypes; 36 | 37 | export default Zen; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sven 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/containers/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Redirect } from 'react-router-dom'; 4 | import { Button, Container } from 'reactstrap'; 5 | import { connect } from 'react-redux'; 6 | 7 | import { signInWithCb } from '../store/fakeAuth'; 8 | 9 | const propTypes = { 10 | signInWithCb: PropTypes.func.isRequired, 11 | location: PropTypes.object.isRequired, 12 | logged: PropTypes.bool.isRequired 13 | }; 14 | 15 | export function Login({ location, logged, signInWithCb }) { 16 | const { from } = location.state || { from: { pathname: '/' } }; 17 | return logged ? ( 18 | 19 | ) : ( 20 | 21 |

You must log in to view the page at {from.pathname}

22 | 25 |
26 | ); 27 | } 28 | 29 | Login.propTypes = propTypes; 30 | 31 | const mapStateToProps = state => ({ 32 | logged: state.logged 33 | }); 34 | 35 | const mapDispatchToProps = { 36 | signInWithCb 37 | }; 38 | 39 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 40 | -------------------------------------------------------------------------------- /src/store/createStore.js: -------------------------------------------------------------------------------- 1 | import { 2 | applyMiddleware, 3 | compose, 4 | createStore as createReduxStore 5 | } from 'redux'; 6 | import thunk from 'redux-thunk'; 7 | import makeRootReducer from './reducers'; 8 | 9 | const createStore = (initialState = {}) => { 10 | /** 11 | |-------------------------------------------------- 12 | | Middleware Configuration 13 | |-------------------------------------------------- 14 | */ 15 | const middleware = [thunk]; 16 | 17 | /** 18 | |-------------------------------------------------- 19 | | Store Enhancers 20 | |-------------------------------------------------- 21 | */ 22 | const enhancers = []; 23 | let composeEnhancers = compose; 24 | 25 | if (process.env.NODE_ENV === 'development') { 26 | if (typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ === 'function') { 27 | composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__; 28 | } 29 | } 30 | 31 | /** 32 | |-------------------------------------------------- 33 | | Store Instantiation and HMR Setup 34 | |-------------------------------------------------- 35 | */ 36 | const store = createReduxStore( 37 | makeRootReducer(), 38 | initialState, 39 | composeEnhancers(applyMiddleware(...middleware), ...enhancers) 40 | ); 41 | store.asyncReducers = {}; 42 | 43 | return store; 44 | }; 45 | 46 | export default createStore(); 47 | -------------------------------------------------------------------------------- /src/routes/CustomHome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { func } from 'prop-types'; 3 | import { Switch, Route } from 'react-router-dom'; 4 | import { Container } from 'reactstrap'; 5 | import { connect } from 'react-redux'; 6 | 7 | import Navbar from '../components/Navbar'; 8 | import AsyncLoad from '../components/AsyncLoad'; 9 | import { signOutWithCb } from '../store/fakeAuth'; 10 | 11 | export const AsyncZen = AsyncLoad({ 12 | loader: () => 13 | import(/* webpackChunkName: "zen" */ '../containers/ZenContainer.js') 14 | }); 15 | 16 | const AsyncHome = AsyncLoad({ 17 | loader: () => 18 | import(/* webpackChunkName: "home" */ '../components/HomeView.js') 19 | }); 20 | 21 | const propTypes = { 22 | signOut: func.isRequired 23 | }; 24 | 25 | export function CustomHome({ signOut }) { 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | ); 37 | } 38 | 39 | CustomHome.propTypes = propTypes; 40 | 41 | const mapStateToProps = state => ({}); 42 | 43 | const mapDispatchToProps = { 44 | signOut: signOutWithCb 45 | }; 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(CustomHome); 48 | -------------------------------------------------------------------------------- /src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { func } from 'prop-types'; 3 | import { 4 | Collapse, 5 | Navbar, 6 | NavbarToggler, 7 | NavbarBrand, 8 | Nav, 9 | NavLink 10 | } from 'reactstrap'; 11 | import { NavLink as Link } from 'react-router-dom'; 12 | 13 | import { AsyncZen } from '../routes/CustomHome'; 14 | 15 | class CustomNavbar extends Component { 16 | static propTypes = { 17 | signOut: func.isRequired 18 | }; 19 | 20 | state = { 21 | isOpen: false 22 | }; 23 | 24 | toggle = () => { 25 | this.setState({ 26 | isOpen: !this.state.isOpen 27 | }); 28 | }; 29 | 30 | onMouseOver() { 31 | AsyncZen.preload(); 32 | } 33 | 34 | render() { 35 | return ( 36 | 37 | 38 | Vortex React 39 | 40 | 41 | 42 | 53 | 54 | 55 | ); 56 | } 57 | } 58 | 59 | export default CustomNavbar; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vortex-react", 3 | "version": "2.0.0", 4 | "private": true, 5 | "homepage": "https://YutHelloWorld.github.io/vortex-react", 6 | "proxy": { 7 | "/zen": { 8 | "target": "https://api.github.com", 9 | "changeOrigin": true 10 | } 11 | }, 12 | "dependencies": { 13 | "axios": "^0.16.2", 14 | "bootstrap": "4.0.0-beta", 15 | "codecov": "^2.3.1", 16 | "husky": "^0.14.3", 17 | "immutability-helper-x": "^1.0.5", 18 | "lint-staged": "^4.2.3", 19 | "node-sass-chokidar": "^0.0.3", 20 | "npm-run-all": "^4.1.1", 21 | "prettier": "^1.7.4", 22 | "prop-types": "^15.6.0", 23 | "react": "^16.0.0", 24 | "react-dom": "^16.0.0", 25 | "react-loadable": "^5.2.2", 26 | "react-redux": "^5.0.6", 27 | "react-router-dom": "^4.2.2", 28 | "react-scripts": "1.0.14", 29 | "react-spinkit": "^3.0.0", 30 | "react-test-renderer": "^16.0.0", 31 | "react-transition-group": "^1.1.2", 32 | "reactstrap": "^5.0.0-alpha.3", 33 | "redux": "^3.7.2", 34 | "redux-thunk": "^2.2.0", 35 | "source-map-explorer": "^1.5.0" 36 | }, 37 | "lint-staged": { 38 | "src/**/*.{js,jsx,json,css}": [ 39 | "prettier --single-quote --write", 40 | "git add" 41 | ] 42 | }, 43 | "scripts": { 44 | "precommit": "lint-staged", 45 | "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", 46 | "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", 47 | "analyze": "source-map-explorer build/static/js/main.*", 48 | "start-js": "react-scripts start", 49 | "start": "npm-run-all -p watch-css start-js", 50 | "build": "npm run build-css && react-scripts build", 51 | "test": "react-scripts test --env=jsdom", 52 | "coverage": "npm test -- -u --coverage", 53 | "codecov": "codecov", 54 | "eject": "react-scripts eject" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/modules/zen.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import update, { updateChain } from 'immutability-helper-x'; 3 | 4 | /** 5 | |-------------------------------------------------- 6 | | Constants 7 | |-------------------------------------------------- 8 | */ 9 | const RECEIVE_ZEN = 'RECEIVE_ZEN'; 10 | const REQUEST_ZEN = 'REQUEST_ZEN'; 11 | const CLEAR_ZEN = 'CLEAR_ZEN'; 12 | 13 | /** 14 | |-------------------------------------------------- 15 | | Actions 16 | |-------------------------------------------------- 17 | */ 18 | 19 | export const requestZen = () => ({ 20 | type: REQUEST_ZEN 21 | }); 22 | 23 | let availableId = 0; 24 | export const receiveZen = value => ({ 25 | type: RECEIVE_ZEN, 26 | payload: { 27 | text: value, 28 | id: availableId++ 29 | } 30 | }); 31 | 32 | export const clearZen = () => ({ 33 | type: CLEAR_ZEN 34 | }); 35 | 36 | export function fetchZen() { 37 | return async (dispatch, getState) => { 38 | if (getState().zen.fetching) return; 39 | 40 | dispatch(requestZen()); 41 | const { data } = await axios.get('https://api.github.com/zen'); 42 | dispatch(receiveZen(data)); 43 | }; 44 | } 45 | 46 | export const actions = { 47 | requestZen, 48 | receiveZen, 49 | clearZen, 50 | fetchZen 51 | }; 52 | 53 | /** 54 | |-------------------------------------------------- 55 | | Action Handlers 56 | |-------------------------------------------------- 57 | */ 58 | const ACTION_HANDLERS = { 59 | [REQUEST_ZEN]: state => { 60 | // return ({ ...state, fetching: true }) 61 | return update.$set(state, 'fetching', true); 62 | }, 63 | [RECEIVE_ZEN]: (state, action) => { 64 | // return ({ ...state, fetching: false, text: state.text.concat(action.payload) }) 65 | return updateChain(state) 66 | .$set('fetching', false) 67 | .$push('text', [action.payload]) 68 | .value(); 69 | }, 70 | [CLEAR_ZEN]: state => { 71 | // return ({ ...state, text: [] }) 72 | return update.$set(state, 'text', []); 73 | } 74 | }; 75 | 76 | /** 77 | |-------------------------------------------------- 78 | | Reducer 79 | |-------------------------------------------------- 80 | */ 81 | const initialState = { 82 | fetching: false, 83 | text: [] 84 | }; 85 | 86 | export default function(state = initialState, action) { 87 | const handler = ACTION_HANDLERS[action.type]; 88 | 89 | return handler ? handler(state, action) : state; 90 | } 91 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Vortex 23 | 24 | 53 | 54 | 55 | 56 | 59 |
60 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/utils/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vortex-react 2 | 3 | [![Build Status](https://travis-ci.org/YutHelloWorld/vortex-react.svg?branch=master)](https://travis-ci.org/YutHelloWorld/vortex-react) 4 | [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 5 | [![codecov](https://codecov.io/gh/YutHelloWorld/vortex-react/branch/master/graph/badge.svg)](https://codecov.io/gh/YutHelloWorld/vortex-react) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 7 | 8 | Get start with [React](https://facebook.github.io/react/), [Redux](http://redux.js.org/), [RR4](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-dom) 🚀 9 | 10 | > Release 2.0.0 was build with create-react-app 11 | 12 | 👉 [Online](https://yuthelloworld.github.io/vortex-react) 13 | 14 | [中文](https://github.com/YutHelloWorld/vortex-react/blob/master/README-zh.md) 15 | 16 |
17 | Table of Contents 18 | 19 | * [Feature](#feature) 20 | * [Get Start](#get-start) 21 | + [Installation](#installation) 22 | + [Running](#running) 23 | + [Scripts](#scripts) 24 | * [Project Structure](#project-structure) 25 | + [Files Structure](#files-structure) 26 | + [Data Flow](#data-flow) 27 | + [Logic](#logic) 28 | * [Contribution](#contribution) 29 | 30 |
31 | 32 | --- 33 | 34 | ## Feature 35 | 36 | - React 37 | - ES6 38 | - Redux 39 | - React-Router-Dom 40 | - Reactstrap + Bootstrap\^4.0.0-alpha.6 (UI) 41 | - Sass 42 | 43 | --- 44 | 45 | ## Get Start 46 | 47 | Before the start, we recommend you read these documentation. 48 | 49 | - [React](https://facebook.github.io/react/) 50 | - [Redux](https://github.com/reactjs/redux) 51 | - [React-Router-Dom](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-dom) 52 | - [ES6](http://babeljs.io/learn-es2015/) 53 | 54 | You can try ES6 and JSX in [Babel REPL](http://babeljs.io/repl/). 55 | 56 | > We recommend node 6.x + npm 5.x + yarn ^0.27.5。 57 | 58 | ### Installation 59 | 60 | ```bash 61 | git clone https://github.com/yuthelloworld/vortex-react.git 62 | cd 63 | yarn # Install project dependencies (or `npm install`) 64 | ``` 65 | 66 | ### Running 67 | 68 | ```bash 69 | yarn start # Start the development server (or `npm start`) 70 | ``` 71 | 72 | ### Scripts 73 | 74 | | `yarn