├── 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 |
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 |
23 | {fetching ? 'Fetching...' : 'Fetch'}
24 | {' '}
25 |
26 | Clear
27 |
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 |
23 | Log in
24 |
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 |
43 |
44 | Zen
45 |
46 |
47 | Sign out
48 |
49 |
50 | Github
51 |
52 |
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 |
57 | You need to enable JavaScript to run this app.
58 |
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 | [](https://travis-ci.org/YutHelloWorld/vortex-react)
4 | [](https://github.com/facebook/jest)
5 | [](https://codecov.io/gh/YutHelloWorld/vortex-react)
6 | [](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