├── .gitignore ├── src ├── react │ ├── components │ │ ├── home.js │ │ ├── notFound.js │ │ ├── layout.js │ │ ├── usersList.js │ │ └── user.js │ ├── routes.js │ ├── serverRouter.js │ ├── clientRouter.js │ └── asyncLink.js ├── redux │ ├── reducers │ │ └── index.js │ ├── services │ │ ├── hydrated.js │ │ └── users.js │ └── store │ │ └── index.js ├── api │ ├── routes.js │ └── users.json ├── client.js └── render.js ├── .babelrc ├── README.md ├── ecosystem.json ├── .eslintrc ├── server.js ├── webpack ├── config.server.js └── config.client.js ├── dev_server.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /log 4 | .vscode 5 | .idea 6 | -------------------------------------------------------------------------------- /src/react/components/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |

UNI-React

5 | ); 6 | -------------------------------------------------------------------------------- /src/react/components/notFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
Page not found
5 | ); 6 | -------------------------------------------------------------------------------- /src/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import user from '../services/users'; 3 | import hydrated from '../services/hydrated'; 4 | 5 | 6 | export default combineReducers({ 7 | user, 8 | hydrated, 9 | }); 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "targets": { 5 | "node": "6.11.1" 6 | } 7 | }], 8 | "@babel/preset-react" 9 | ], 10 | "plugins": [ 11 | "syntax-dynamic-import", 12 | "babel-plugin-dynamic-import-node", 13 | "@babel/plugin-transform-modules-commonjs" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/redux/services/hydrated.js: -------------------------------------------------------------------------------- 1 | 2 | const HYDRATED = Symbol('HYDRATED'); 3 | 4 | const initialState = false; 5 | 6 | export default function hydratedReduser(state = initialState, action) { 7 | switch (action.type) { 8 | case HYDRATED: 9 | return true; 10 | default: 11 | return state; 12 | } 13 | } 14 | 15 | export function setHydrated() { 16 | return { type: HYDRATED }; 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UNI-react 2 | 3 | ## Universal hot start React + Redux + Express 4 | 5 | ### Install 6 | 7 | ``` 8 | git clone 9 | npm install 10 | ``` 11 | 12 | ### Developer mode (hot reloading on server and on client) 13 | 14 | ``` 15 | npm run hot 16 | ``` 17 | 18 | ### Production mode 19 | 20 | ``` 21 | npm run build 22 | npm start 23 | ``` 24 | 25 | - [Хабр 1] (https://habrahabr.ru/post/349064/) 26 | - [Хабр 2] (https://habrahabr.ru/post/349136/) 27 | -------------------------------------------------------------------------------- /src/react/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from '../asyncLink'; 3 | 4 | export default ({ children }) => ( // eslint-disable-line react/prop-types 5 |
6 |
7 | 12 |
13 | { children } 14 | 17 |
18 | ); 19 | -------------------------------------------------------------------------------- /src/react/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | path: '/', 4 | exact: true, 5 | componentName: 'components/home', 6 | }, { 7 | path: '/users', 8 | exact: true, 9 | componentName: 'components/usersList', 10 | }, { 11 | path: '/users/page/:page', 12 | exact: true, 13 | componentName: 'components/usersList', 14 | }, { 15 | path: '/users/:id', 16 | exact: true, 17 | componentName: 'components/user', 18 | }, { 19 | path: '*', 20 | exact: false, 21 | componentName: 'components/notFound', 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /src/api/routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const users = require('./users.json'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/users', (req, res) => { 7 | res.send(users.data.map((user, id) => ({ id, name: user[0] }))); 8 | }); 9 | 10 | router.get('/users/:id', (req, res) => { 11 | const id = Number(req.params.id); 12 | const user = users.data[id]; 13 | if (!user) { 14 | return res.status(404).send(); 15 | } 16 | const [name, phone, email, birtday] = user; 17 | return res.send({ id, name, phone, email, birtday }); 18 | }); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /src/react/serverRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import routes from './routes'; 4 | import Layout from './components/layout'; 5 | 6 | export default () => ( 7 | 8 | 9 | { 10 | routes.map((props) => { 11 | props.component = require(`./${props.componentName}`); // eslint-disable-line 12 | if (props.component.default) { // eslint-disable-line 13 | props.component = props.component.default; // eslint-disable-line 14 | } 15 | return ; // eslint-disable-line 16 | }) 17 | } 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /ecosystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "conduit", 5 | "script": "server.js", 6 | "log_date_format": "YYYY-MM-DD HH:mm Z", 7 | "error_file": "./log/node-app.stderr.log", 8 | "out_file": "./log/node-app.stdout.log", 9 | "watch": false, 10 | "merge_logs": true, 11 | "exec_mode": "cluster", 12 | "instances": 0, 13 | "max_memory_restart": "1024M", 14 | "env": { 15 | "COMMON_VARIABLE": "true", 16 | "PORT": "3000", 17 | "HOST": "0.0.0.0", 18 | "NODE_ENV": "production" 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module", 6 | "allowImportExportEverywhere": true 7 | }, 8 | "rules": { 9 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 10 | "object-curly-newline": ["error", { 11 | "ObjectExpression": { "multiline": true }, 12 | "ObjectPattern": { "multiline": true }, 13 | "ImportDeclaration": { "multiline": true }, 14 | "ExportDeclaration": { "multiline": true } 15 | }], 16 | "jsx-a11y/anchor-is-valid": [ "error", { 17 | "components": [ "Link" ], 18 | "specialLink": [ "to" ], 19 | "aspects": [ ] 20 | }] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/redux/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { createLogger } from 'redux-logger'; 4 | import rootReducer from '../reducers'; 5 | 6 | let store; 7 | export default function prepareStore(initialState) { 8 | if (module.hot) { 9 | store = compose( 10 | applyMiddleware(thunk), 11 | applyMiddleware(createLogger({})), 12 | )(createStore)(rootReducer, initialState); 13 | 14 | module.hot.accept('../reducers', () => { 15 | const nextRootReducer = rootReducer; 16 | 17 | store.replaceReducer(nextRootReducer); 18 | }); 19 | } else { 20 | store = compose(applyMiddleware(thunk))(createStore)(rootReducer, initialState); 21 | } 22 | 23 | return store; 24 | } 25 | 26 | export function getStore() { 27 | return store; 28 | } 29 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import { AppContainer } from 'react-hot-loader'; 2 | import React from 'react'; 3 | import { hydrate } from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | import AppRouter from './react/clientRouter'; // eslint-disable-line 7 | import createStore from './redux/store'; 8 | import { setHydrated } from './redux/services/hydrated'; 9 | 10 | const preloadedState = window.__PRELOADED_STATE__; // eslint-disable-line 11 | delete window.__PRELOADED_STATE__; // eslint-disable-line 12 | const store = createStore(preloadedState); 13 | 14 | window.onload= () => store.dispatch(setHydrated()); // eslint-disable-line 15 | 16 | hydrate( 17 | 18 | 19 | 20 | 21 | 22 | 23 | , 24 | document.getElementById('app') // eslint-disable-line 25 | ); 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cookieParser = require('cookie-parser'); 3 | const cookieEncrypter = require('cookie-encrypter'); 4 | const bodyParser = require('body-parser'); 5 | const apicache = require('apicache'); 6 | const api = require('./src/api/routes'); 7 | const render = require('./dist/render.bundle.js'); 8 | const morgan = require('morgan'); 9 | 10 | const port = Number(process.env.PORT) || 3000; 11 | const app = express(); 12 | 13 | const nodeEnv = process.env.NODE_ENV || 'development'; 14 | app.use(morgan('method :url :status :res[content-length] - :response-time ms')); 15 | const cache = apicache.options({ 16 | appendKey: req => req.get('Authorization'), 17 | defaultDuration: 1000, 18 | headerBlacklist: ['Authorization', 'authorization'], 19 | }).middleware; 20 | 21 | app.set('env', nodeEnv); 22 | app.use(cookieParser('change secret value')); 23 | app.use(cookieEncrypter('12345678901234567890123456789012')); 24 | app.use(bodyParser.json()); 25 | app.use('/api', api); 26 | app.use('/static', express.static('dist')); 27 | app.use('/api', api); 28 | app.use('/', cache(1000), render); 29 | 30 | app.listen(port, () => { 31 | console.log(`Listening at ${port}`); 32 | }); 33 | -------------------------------------------------------------------------------- /webpack/config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | const externalFolder = new RegExp(`^${path.resolve(__dirname, '../src')}/(react|redux)/.*$`); 5 | const nodeEnv = process.env.NODE_ENV || 'development'; 6 | const isDevelopment = nodeEnv === 'development'; 7 | 8 | module.exports = { 9 | mode: isDevelopment ? 'development' : 'production', 10 | name: 'server', 11 | devtool: isDevelopment ? 'eval' : false, 12 | entry: './src/render.js', 13 | target: 'node', 14 | bail: !isDevelopment, 15 | externals: [ 16 | nodeExternals(), 17 | function externals(context, request, callback) { 18 | if (request === module.exports.entry 19 | || externalFolder.test(path.resolve(context, request))) { 20 | return callback(); 21 | } 22 | return callback(null, `commonjs2 ${request}`); 23 | }, 24 | ], 25 | output: { 26 | path: path.resolve(__dirname, '../dist'), 27 | filename: 'render.bundle.js', 28 | libraryTarget: 'commonjs2', 29 | }, 30 | module: { 31 | rules: [{ 32 | test: /\.jsx?$/, 33 | exclude: [/node_modules/], 34 | use: 'babel-loader?retainLines=true', 35 | }], 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/react/clientRouter.js: -------------------------------------------------------------------------------- 1 | import { hot } from 'react-hot-loader'; 2 | import React from 'react'; 3 | import { Route, Switch } from 'react-router-dom'; 4 | import Loadable from 'react-loadable'; 5 | import routes from './routes'; 6 | import Layout from './components/layout'; 7 | 8 | export default hot(module)(() => ( 9 | 10 | 11 | { 12 | routes.map((props) => { 13 | props.component = Loadable({ // eslint-disable-line no-param-reassign 14 | loader: () => import(`./${props.componentName}`).then( 15 | (component) => { 16 | if (module.hot) { 17 | // hot(module)(component) 18 | /* module.hot.accept(component, () => { 19 | // if you are using harmony modules ({modules:false}) 20 | //render(AppRouter) 21 | // in all other cases - re-require App manually 22 | render(component) 23 | }); */ 24 | } 25 | return component; 26 | }, 27 | ), 28 | loading: () => null, 29 | delay: 0, 30 | timeout: 10000, 31 | }); 32 | return ; // eslint-disable-line react/prop-types 33 | }) 34 | } 35 | 36 | 37 | )); 38 | -------------------------------------------------------------------------------- /src/react/components/usersList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import AsyncLink from '../asyncLink'; 5 | import { usersAction } from '../../redux/services/users'; 6 | 7 | class UsersList extends React.PureComponent { 8 | static async getInitialProps({ dispatch }) { 9 | await dispatch(usersAction()); 10 | } 11 | 12 | async componentDidMount() { 13 | if (this.props.history.action === 'POP' && this.props.hydrated) { 14 | await UsersList.getInitialProps(this.props); 15 | } 16 | } 17 | 18 | render() { 19 | return ( 20 | 32 | ); 33 | } 34 | } 35 | 36 | UsersList.propTypes = { 37 | users: PropTypes.arrayOf(PropTypes.shape({ 38 | id: PropTypes.number.isRequired, 39 | name: PropTypes.string.isRequired, 40 | })).isRequired, 41 | history: PropTypes.shape().isRequired, 42 | hydrated: PropTypes.bool.isRequired, 43 | }; 44 | 45 | UsersList.defaultProps = { users: [] }; 46 | 47 | export default connect(state => ({ 48 | users: state.user.users, 49 | hydrated: state.hydrated, 50 | }))(UsersList); 51 | -------------------------------------------------------------------------------- /src/react/components/user.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { userAction } from '../../redux/services/users'; 5 | 6 | class User extends React.PureComponent { 7 | static async getInitialProps({ match, dispatch }) { 8 | const id = Number(match.params.id); 9 | await dispatch(userAction({ id })); 10 | } 11 | 12 | async componentDidMount() { 13 | if (this.props.history.action === 'POP' && this.props.hydrated) { 14 | await User.getInitialProps(this.props); 15 | } 16 | } 17 | 18 | render() { 19 | const { user } = this.props; 20 | 21 | return ( 22 | user ? 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
id{user.id}
name{user.name}
email{user.email}
phone{user.phone}
31 | : 32 | null 33 | ); 34 | } 35 | } 36 | 37 | // User.defaultProps = { user: {} }; 38 | 39 | User.propTypes = { 40 | user: PropTypes.shape({ 41 | id: PropTypes.number, 42 | name: PropTypes.string, 43 | email: PropTypes.string, 44 | phone: PropTypes.string, 45 | }).isRequired, 46 | history: PropTypes.shape().isRequired, 47 | hydrated: PropTypes.bool.isRequired, 48 | }; 49 | 50 | export default connect(state => ({ 51 | user: state.user.user, 52 | hydrated: state.hydrated, 53 | }))(User); 54 | -------------------------------------------------------------------------------- /src/redux/services/users.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const USERS_REQUEST = Symbol('USERS_REQUEST'); 4 | const USERS_SUCCESS = Symbol('USERS_SUCCESS'); 5 | const USERS_FAILURE = Symbol('USERS_FAILURE'); 6 | const USER_REQUEST = Symbol('USER_REQUEST'); 7 | const USER_SUCCESS = Symbol('USER_SUCCESS'); 8 | const USER_FAILURE = Symbol('USER_FAILURE'); 9 | 10 | const initialState = { count: 0 }; 11 | 12 | export default function userReducer(state = initialState, action) { 13 | switch (action.type) { 14 | case USERS_REQUEST: 15 | return state; 16 | case USERS_SUCCESS: 17 | return { ...state, users: action.payload }; 18 | case USERS_FAILURE: 19 | return { ...state, users: undefined }; 20 | case USER_REQUEST: 21 | return state; 22 | case USER_SUCCESS: 23 | return { ...state, user: action.payload }; 24 | case USER_FAILURE: 25 | return { ...state, user: undefined }; 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | export function usersAction() { 32 | return (dispatch) => { 33 | dispatch({ type: USERS_REQUEST }); 34 | return axios({ 35 | method: 'get', 36 | baseURL: 'http://localhost:3000/api/', 37 | url: 'users', 38 | withCredentials: true, 39 | }).then( 40 | data => dispatch({ type: USERS_SUCCESS, payload: data.data }), 41 | error => dispatch({ type: USERS_FAILURE, error }), 42 | ); 43 | }; 44 | } 45 | 46 | export function userAction({ id }) { 47 | return (dispatch) => { 48 | dispatch({ type: USER_REQUEST }); 49 | return axios({ 50 | method: 'get', 51 | baseURL: 'http://localhost:3000/api/', 52 | url: `users/${id}`, 53 | withCredentials: true, 54 | }).then( 55 | data => dispatch({ type: USER_SUCCESS, payload: data.data }), 56 | error => dispatch({ type: USER_FAILURE, error }), 57 | ); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/react/asyncLink.js: -------------------------------------------------------------------------------- 1 | import { Link, matchPath } from 'react-router-dom'; 2 | import routes from './routes'; 3 | import { getStore } from '../redux/store'; 4 | 5 | function isModifiedEvent(event) { 6 | return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); 7 | } 8 | 9 | function handleClick(event) { 10 | if (this.props.onClick) this.props.onClick(event); 11 | 12 | if ( 13 | !event.defaultPrevented && // onClick prevented default 14 | event.button === 0 && // ignore everything but left clicks 15 | !this.props.target && // let browser handle "target=_blank" etc. 16 | !isModifiedEvent(event) // ignore clicks with modifier keys 17 | ) { 18 | event.preventDefault(); 19 | const { history } = this.context.router; 20 | const { replace, to } = this.props; 21 | 22 | function locate() { // eslint-disable-line no-inner-declarations 23 | if (replace) { 24 | history.replace(to); 25 | } else { 26 | history.push(to); 27 | } 28 | } 29 | if (this.context.router.history.location.pathname) { 30 | const routeTo = routes.find(route => (matchPath(this.props.to, route) ? route : null)); 31 | const match = matchPath(this.props.to, routeTo); 32 | const store = getStore(); 33 | if (routeTo) { 34 | import(`./${routeTo.componentName}`) 35 | .then(component => component.default || component) 36 | .then(component => (component.getInitialProps ? 37 | component.getInitialProps({ 38 | match, 39 | store, 40 | dispatch: store.dispatch, 41 | }) 42 | : null 43 | )) 44 | .then(() => locate()); 45 | } else { 46 | locate(); 47 | } 48 | } else { 49 | locate(); 50 | } 51 | } 52 | } 53 | 54 | class AsyncLink extends Link { 55 | constructor(...args) { 56 | super(...args); 57 | this.handleClick = handleClick.bind(this); 58 | } 59 | } 60 | 61 | export default AsyncLink; 62 | -------------------------------------------------------------------------------- /dev_server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const cookieParser = require('cookie-parser'); 4 | const cookieEncrypter = require('cookie-encrypter'); 5 | const bodyParser = require('body-parser'); 6 | const webpack = require('webpack'); 7 | const webpackClientDevMiddleware = require('webpack-dev-middleware'); 8 | const webpackClientHotMiddleware = require('webpack-hot-middleware'); 9 | const webpackClientConfig = require('./webpack/config.client'); 10 | const serverConfig = require('./webpack/config.server'); 11 | const api = require('./src/api/routes'); 12 | 13 | const serverCompiler = webpack(serverConfig); 14 | const clientCompiler = webpack(webpackClientConfig); 15 | const port = Number(process.env.PORT) || 3000; 16 | const app = express(); 17 | const nodeEnv = process.env.NODE_ENV || 'development'; 18 | 19 | const serverPath = path.resolve(__dirname, './dist/render.bundle.js'); 20 | let render = require(serverPath); // eslint-disable-line import/no-dynamic-require 21 | 22 | 23 | app.set('env', nodeEnv); 24 | app.use(cookieParser('change secret value')); 25 | app.use(cookieEncrypter('12345678901234567890123456789012')); 26 | app.use(bodyParser.json()); 27 | app.use('/api', api); 28 | 29 | app.use(webpackClientDevMiddleware(clientCompiler, { 30 | publicPath: webpackClientConfig.output.publicPath, 31 | headers: { 'Access-Control-Allow-Origin': '*' }, 32 | stats: { colors: true }, 33 | historyApiFallback: true, 34 | })); 35 | 36 | app.use(webpackClientHotMiddleware(clientCompiler, { 37 | log: console.log, 38 | path: '/__webpack_hmr', 39 | heartbeat: 10000, 40 | })); 41 | 42 | app.use('/api', api); 43 | 44 | app.use('/', (req, res, next) => render(req, res, next)); 45 | 46 | app.listen(port, () => { 47 | console.log(`Listening at ${port}`); 48 | }); 49 | 50 | function clearCache() { 51 | const cacheIds = Object.keys(require.cache); 52 | 53 | cacheIds.forEach((id) => { 54 | if (id === serverPath) { 55 | delete require.cache[id]; 56 | } 57 | }); 58 | } 59 | 60 | function onServerChange(err, stats) { 61 | if (err || (stats.compilation && stats.compilation.errors && stats.compilation.errors.length)) { 62 | console.log('Server bundling error:', err || stats.compilation.errors); 63 | } 64 | clearCache(); 65 | try { 66 | render = require(serverPath); // eslint-disable-line import/no-dynamic-require, global-require 67 | } catch (ex) { 68 | console.log('Error detecded', ex); 69 | } 70 | } 71 | 72 | function watch() { 73 | const compilerOptions = { 74 | aggregateTimeout: 300, 75 | poll: 150, 76 | }; 77 | 78 | serverCompiler.watch(compilerOptions, onServerChange); 79 | } 80 | 81 | watch(); 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "realworld-react-universal-hot", 3 | "version": "0.0.1", 4 | "description": "realworld-react-universal-hot", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "hot": "rm -rf ./dist && mkdir ./dist && NODE_ENV=development webpack -d --config=./webpack/config.server.js --mode=development && NODE_ENV=development webpack -d --config=./webpack/config.client.js --mode=development && node ./dev_server", 9 | "build": "rm -rf ./dist && mkdir ./dist && NODE_ENV=production webpack --config=./webpack/config.server.js --mode=production && NODE_ENV=production webpack -p --config=./webpack/config.client.js --mode=production", 10 | "start": "NODE_ENV=production node ./server", 11 | "lint": "eslint src/ --ext .js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/apapacy/realworld-react-universal-hot.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "redux", 20 | "universal", 21 | "hot" 22 | ], 23 | "author": "Ovcharenko A.V.", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/apapacy/realworld-react-universal-hot/issues" 27 | }, 28 | "homepage": "https://github.com/apapacy/realworld-react-universal-hot#readme", 29 | "devDependencies": { 30 | "@babel/core": "^7.1.0", 31 | "@babel/plugin-transform-runtime": "^7.8.3", 32 | "@babel/preset-env": "^7.1.0", 33 | "@babel/preset-react": "^7.0.0", 34 | "babel-core": "7.0.0-bridge.0", 35 | "babel-eslint": "^8.2.2", 36 | "babel-loader": "^8.0.2", 37 | "babel-plugin-dynamic-import-node": "^1.2.0", 38 | "enzyme": "^3.11.0", 39 | "enzyme-adapter-react-16": "^1.5.0", 40 | "eslint": "^4.18.2", 41 | "eslint-config-airbnb": "^16.1.0", 42 | "eslint-plugin-import": "^2.9.0", 43 | "eslint-plugin-jsx-a11y": "^6.0.3", 44 | "eslint-plugin-react": "^7.7.0", 45 | "jest": "^25.1.0", 46 | "jsdom": "^16.1.0", 47 | "jsdom-global": "^3.0.2", 48 | "react-test-renderer": "^16.12.0", 49 | "react-transform-hmr": "^1.0.4" 50 | }, 51 | "dependencies": { 52 | "@babel/node": "^7.0.0", 53 | "@babel/polyfill": "^7.0.0", 54 | "@babel/runtime": "^7.0.0", 55 | "apicache": "^1.5.3", 56 | "axios": "^0.19.2", 57 | "babel-jest": "^25.1.0", 58 | "body-parser": "^1.18.2", 59 | "cookie-encrypter": "^1.0.1", 60 | "cookie-parser": "^1.4.3", 61 | "express": "^4.17.1", 62 | "fast-async": "^6.3.8", 63 | "from": "^0.1.7", 64 | "import": "0.0.6", 65 | "lodash": "^4.17.15", 66 | "moment": "^2.21.0", 67 | "morgan": "^1.9.0", 68 | "prop-types": "^15.6.1", 69 | "raf": "^3.4.0", 70 | "react": "^16.12.0", 71 | "react-dom": "^16.12.0", 72 | "react-hot-loader": "^4.3.11", 73 | "react-loadable": "^5.3.1", 74 | "react-markdown": "^3.2.2", 75 | "react-redux": "^5.0.7", 76 | "react-router": "^5.1.2", 77 | "react-router-dom": "^4.2.2", 78 | "redux": "^3.7.2", 79 | "redux-logger": "^3.0.6", 80 | "redux-thunk": "^2.2.0", 81 | "webpack": "^4.23.1", 82 | "webpack-cli": "^3.1.2", 83 | "webpack-dev-middleware": "^3.4.0", 84 | "webpack-hot-middleware": "^2.24.3", 85 | "webpack-node-externals": "^1.7.2" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import { matchPath, StaticRouter } from 'react-router-dom'; 4 | import { Provider } from 'react-redux'; 5 | import routes from './react/routes'; 6 | import AppRouter from './react/serverRouter'; 7 | import createStore from './redux/store'; 8 | import stats from '../dist/stats.generated'; 9 | 10 | function assets(name) { 11 | const prefix = '/static/'; 12 | if (name instanceof Array) { 13 | return prefix + name[0]; 14 | } 15 | return prefix + name; 16 | } 17 | 18 | module.exports = (req, res, next) => { 19 | const store = createStore(); 20 | const promises = []; 21 | const componentNames = []; 22 | const componentsPath = []; 23 | routes.some((route) => { 24 | const match = matchPath(req.path, route); 25 | if (match) { 26 | let component = require(`./react/${route.componentName}`); // eslint-disable-line 27 | if (component.default) { 28 | component = component.default; 29 | } 30 | componentNames.push(route.componentName); 31 | componentsPath.push(route.path); 32 | if (typeof component.getInitialProps === 'function') { 33 | promises.push(component.getInitialProps({ 34 | req, 35 | res, 36 | next, 37 | match, 38 | store, 39 | dispatch: store.dispatch, 40 | })); 41 | } 42 | } 43 | return match; 44 | }); 45 | 46 | Promise.all(promises).then((data) => { 47 | if (data[0] && data[0].redirectUrl) { 48 | res.writeHead(301, { Location: data[0].redirectUrl }); 49 | res.end(); 50 | return; 51 | } 52 | 53 | const context = { data }; 54 | const html = ReactDOMServer.renderToString(( 55 | 56 | 57 | 58 | 59 | 60 | )); 61 | 62 | if (componentsPath.length === 0 || componentsPath[0] === '*') { 63 | res.writeHead(404); 64 | } else { 65 | res.writeHead(200); 66 | } 67 | 68 | res.write(` 69 | 70 | 71 | 72 | 73 | Conduit 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 87 |
${html}
88 | 89 | ${componentNames.map(componentName => ``)} 90 | `); 91 | res.end(); 92 | }); 93 | }; 94 | -------------------------------------------------------------------------------- /webpack/config.client.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const routes = require('../src/react/routes'); 4 | 5 | const nodeEnv = process.env.NODE_ENV || 'development'; 6 | const isDevelopment = nodeEnv === 'development'; 7 | 8 | const entry = {}; 9 | 10 | for (let i = 0; i < routes.length; i += 1) { 11 | entry[routes[i].componentName] = [ 12 | '../src/client.js', 13 | ]; 14 | if (isDevelopment) { 15 | entry[routes[i].componentName].unshift('webpack-hot-middleware/client'); 16 | } else { 17 | entry[routes[i].componentName].push(`../src/react/${routes[i].componentName}.js`); 18 | } 19 | } 20 | 21 | module.exports = { 22 | mode: isDevelopment ? 'development' : 'production', 23 | name: 'client', 24 | target: 'web', 25 | cache: isDevelopment, 26 | devtool: isDevelopment ? 'cheap-module-source-map' : 'hidden-source-map', 27 | context: __dirname, 28 | entry, 29 | output: { 30 | path: path.resolve(__dirname, '../dist'), 31 | publicPath: isDevelopment ? '/static/' : '/static/', 32 | filename: isDevelopment ? '[name].bundle.js' : '[name].[hash].bundle.js', 33 | chunkFilename: isDevelopment ? '[name].bundle.js' : '[name].[hash].bundle.js', 34 | }, 35 | module: { 36 | rules: [{ 37 | test: /\.jsx?$/, 38 | exclude: /node_modules/, 39 | loader: require.resolve('babel-loader'), 40 | options: { 41 | cacheDirectory: isDevelopment, 42 | babelrc: false, 43 | presets: [ 44 | ['@babel/preset-env', { 45 | targets: { 46 | browsers: ['>90%'], 47 | }, 48 | exclude: ['transform-async-to-generator', 'transform-regenerator',], 49 | }], 50 | '@babel/preset-react', 51 | ], 52 | plugins: (isDevelopment ? [ 53 | 'react-hot-loader/babel', 54 | ['module:fast-async', { spec: true }], 55 | ['@babel/plugin-transform-runtime', { 56 | corejs: false, 57 | helpers: true, 58 | regenerator: true, 59 | useESModules: false, 60 | }], 61 | 'syntax-dynamic-import', 62 | ] : [ 63 | ['@babel/plugin-transform-runtime', { 64 | corejs: false, 65 | helpers: true, 66 | regenerator: true, 67 | useESModules: false, 68 | }], 69 | 'syntax-dynamic-import', 70 | ]).concat([ 71 | ]), 72 | }, 73 | }], 74 | }, 75 | optimization: { 76 | minimize: !isDevelopment, 77 | runtimeChunk: { name: 'common' }, 78 | splitChunks: { 79 | cacheGroups: { 80 | default: false, 81 | commons: { 82 | test: /\.jsx?$/, 83 | chunks: 'all', 84 | minChunks: 2, 85 | name: 'common', 86 | enforce: true, 87 | maxAsyncRequests: 1, 88 | maxInitialRequests: 1, 89 | }, 90 | }, 91 | }, 92 | }, 93 | plugins: [ 94 | new webpack.optimize.OccurrenceOrderPlugin(), 95 | new webpack.NoEmitOnErrorsPlugin(), 96 | new webpack.NamedModulesPlugin(), 97 | function StatsPlugin() { 98 | this.plugin('done', stats => 99 | require('fs').writeFileSync( // eslint-disable-line no-sync, global-require 100 | path.join(__dirname, '../dist', 'stats.generated.js'), 101 | `module.exports=${JSON.stringify(stats.toJson().assetsByChunkName)};\n`, 102 | )); 103 | }, 104 | ].concat(isDevelopment ? [ 105 | new webpack.HotModuleReplacementPlugin(), 106 | ] : [ 107 | ]), 108 | }; 109 | -------------------------------------------------------------------------------- /src/api/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "cols": [ 3 | "name", 4 | "phone", 5 | "email", 6 | "birtday" 7 | ], 8 | "data": [ 9 | [ 10 | "Blake, Rose E.", 11 | "(07493) 0839350", 12 | "egestas.Aliquam.fringilla@feugiat.com", 13 | "2018-11-27T00:52:13-08:00" 14 | ], 15 | [ 16 | "Bray, Kasper X.", 17 | "(0905) 02494306", 18 | "leo.Morbi.neque@Crassed.com", 19 | "2017-07-22T19:55:50-07:00" 20 | ], 21 | [ 22 | "White, Vaughan A.", 23 | "(039728) 375658", 24 | "fermentum@ipsum.net", 25 | "2017-08-13T22:59:26-07:00" 26 | ], 27 | [ 28 | "Bernard, Basil O.", 29 | "(03051) 0045489", 30 | "a.auctor.non@nonlobortis.co.uk", 31 | "2017-06-18T01:27:40-07:00" 32 | ], 33 | [ 34 | "Sampson, Lacy P.", 35 | "(0917) 45405774", 36 | "cubilia.Curae@Phasellus.co.uk", 37 | "2017-07-13T00:45:33-07:00" 38 | ], 39 | [ 40 | "Roberts, Herrod O.", 41 | "(030155) 231881", 42 | "semper.et.lacinia@velvulputateeu.edu", 43 | "2017-03-02T10:51:56-08:00" 44 | ], 45 | [ 46 | "Stanton, Adria K.", 47 | "(036) 39170288", 48 | "neque.Nullam@eleifendnec.co.uk", 49 | "2017-05-13T17:27:25-07:00" 50 | ], 51 | [ 52 | "Miranda, Hall M.", 53 | "(064) 01845356", 54 | "erat.semper@nullaInteger.org", 55 | "2018-08-14T17:03:36-07:00" 56 | ], 57 | [ 58 | "Richardson, Jason O.", 59 | "(00173) 2249878", 60 | "imperdiet.ullamcorper.Duis@sit.ca", 61 | "2018-03-23T16:09:03-07:00" 62 | ], 63 | [ 64 | "Gallegos, Ursa D.", 65 | "(034869) 476856", 66 | "nisl@id.com", 67 | "2018-05-25T21:19:22-07:00" 68 | ], 69 | [ 70 | "James, Jayme B.", 71 | "(031480) 930420", 72 | "sem.eget@vitaediam.net", 73 | "2018-05-26T15:03:38-07:00" 74 | ], 75 | [ 76 | "Madden, Scarlet P.", 77 | "(0314) 95004901", 78 | "dictum.Proin.eget@velitAliquam.ca", 79 | "2018-04-29T11:53:52-07:00" 80 | ], 81 | [ 82 | "Workman, Lance Q.", 83 | "(067) 80563035", 84 | "Sed@consequatpurusMaecenas.com", 85 | "2017-04-28T20:51:23-07:00" 86 | ], 87 | [ 88 | "Nolan, Uriah I.", 89 | "(031031) 021996", 90 | "Nam.porttitor@eu.com", 91 | "2018-02-27T12:31:12-08:00" 92 | ], 93 | [ 94 | "Suarez, Rebekah T.", 95 | "(0200) 53134668", 96 | "magna@convallisest.org", 97 | "2018-03-23T08:17:00-07:00" 98 | ], 99 | [ 100 | "Huber, Cairo V.", 101 | "(0366) 59048563", 102 | "Sed.nec@felisDonec.org", 103 | "2017-09-07T05:30:44-07:00" 104 | ], 105 | [ 106 | "Walton, Mohammad Y.", 107 | "(097) 00384591", 108 | "Cum@Intincidunt.net", 109 | "2017-07-14T19:10:57-07:00" 110 | ], 111 | [ 112 | "Fuentes, Laith A.", 113 | "(050) 07892177", 114 | "tellus.lorem.eu@Nullatincidunt.co.uk", 115 | "2018-10-12T16:15:18-07:00" 116 | ], 117 | [ 118 | "Sawyer, Ulric Z.", 119 | "(08353) 6534327", 120 | "et@vulputatenisisem.org", 121 | "2017-04-22T07:52:58-07:00" 122 | ], 123 | [ 124 | "Alexander, Jacqueline X.", 125 | "(05484) 5210399", 126 | "magna@ami.com", 127 | "2018-07-19T11:07:52-07:00" 128 | ], 129 | [ 130 | "Nolan, Alexandra W.", 131 | "(038522) 041279", 132 | "mattis.ornare@Nam.ca", 133 | "2018-09-25T04:13:09-07:00" 134 | ], 135 | [ 136 | "Sampson, Nadine Q.", 137 | "(00410) 9809957", 138 | "fringilla@leoVivamus.ca", 139 | "2018-07-10T22:35:24-07:00" 140 | ], 141 | [ 142 | "Crosby, Odessa D.", 143 | "(06985) 3122651", 144 | "aliquet.metus@egestas.org", 145 | "2017-08-03T21:41:45-07:00" 146 | ], 147 | [ 148 | "Mcconnell, Lisandra L.", 149 | "(04561) 7056434", 150 | "Curabitur.ut@egestas.co.uk", 151 | "2018-01-18T07:14:07-08:00" 152 | ], 153 | [ 154 | "Emerson, Clio Z.", 155 | "(023) 88334989", 156 | "mauris.ut@tincidunt.ca", 157 | "2017-04-21T13:05:25-07:00" 158 | ], 159 | [ 160 | "Cain, Octavius R.", 161 | "(036814) 291170", 162 | "non.magna.Nam@lectus.com", 163 | "2017-07-31T00:27:36-07:00" 164 | ], 165 | [ 166 | "Velez, Martina A.", 167 | "(07721) 3535384", 168 | "fames.ac@Etiamlaoreet.ca", 169 | "2017-05-31T11:13:16-07:00" 170 | ], 171 | [ 172 | "Warner, Hasad B.", 173 | "(0550) 09727085", 174 | "et@accumsaninterdumlibero.ca", 175 | "2018-07-20T19:38:47-07:00" 176 | ], 177 | [ 178 | "Young, Jana X.", 179 | "(033304) 011173", 180 | "ipsum.Curabitur.consequat@Proinvelarcu.co.uk", 181 | "2017-03-25T20:51:15-07:00" 182 | ], 183 | [ 184 | "Stark, Tatum N.", 185 | "(031594) 047963", 186 | "Sed@elementum.co.uk", 187 | "2017-03-18T12:53:50-07:00" 188 | ], 189 | [ 190 | "Bass, Idola X.", 191 | "(038) 41759655", 192 | "venenatis.lacus.Etiam@SednequeSed.ca", 193 | "2017-12-26T10:10:31-08:00" 194 | ], 195 | [ 196 | "Wilkerson, Zane N.", 197 | "(054) 20536903", 198 | "odio@molestieorci.edu", 199 | "2018-06-28T03:27:44-07:00" 200 | ], 201 | [ 202 | "York, Silas Q.", 203 | "(045) 26539499", 204 | "luctus.sit@ullamcorpervelitin.com", 205 | "2018-01-08T21:24:05-08:00" 206 | ], 207 | [ 208 | "Pacheco, Jane S.", 209 | "(0237) 93685090", 210 | "Donec@sitamet.edu", 211 | "2018-03-06T19:49:15-08:00" 212 | ], 213 | [ 214 | "Buckley, Shafira Y.", 215 | "(0839) 38823511", 216 | "purus@ipsum.ca", 217 | "2019-01-01T01:31:13-08:00" 218 | ], 219 | [ 220 | "Griffith, Yvette D.", 221 | "(09019) 8547062", 222 | "lobortis.mauris@lacusAliquamrutrum.net", 223 | "2018-04-09T00:06:46-07:00" 224 | ], 225 | [ 226 | "Matthews, Oprah T.", 227 | "(063) 59216165", 228 | "In.at.pede@maurisrhoncus.co.uk", 229 | "2018-05-12T07:33:55-07:00" 230 | ], 231 | [ 232 | "Wood, Mufutau Q.", 233 | "(07575) 5655348", 234 | "feugiat.Lorem.ipsum@etmagnis.edu", 235 | "2017-04-28T22:26:14-07:00" 236 | ], 237 | [ 238 | "Charles, Kaden U.", 239 | "(03601) 2939704", 240 | "viverra.Maecenas@Nullaegetmetus.net", 241 | "2017-04-20T09:16:06-07:00" 242 | ], 243 | [ 244 | "Coleman, Sade T.", 245 | "(01480) 1828030", 246 | "luctus@Proinsed.co.uk", 247 | "2017-03-22T10:33:57-07:00" 248 | ], 249 | [ 250 | "Aguilar, Ivory U.", 251 | "(08916) 5183589", 252 | "Quisque.fringilla@odio.com", 253 | "2018-04-21T05:14:08-07:00" 254 | ], 255 | [ 256 | "Padilla, Zephr X.", 257 | "(0030) 88713719", 258 | "sodales.Mauris.blandit@ametluctus.ca", 259 | "2017-12-04T07:38:54-08:00" 260 | ], 261 | [ 262 | "Navarro, Len G.", 263 | "(031165) 813071", 264 | "vitae.risus@idmagna.net", 265 | "2017-12-25T20:33:19-08:00" 266 | ], 267 | [ 268 | "Hampton, Wanda B.", 269 | "(026) 05557840", 270 | "Proin.sed.turpis@Nunc.net", 271 | "2018-05-01T02:57:42-07:00" 272 | ], 273 | [ 274 | "Park, Dorothy C.", 275 | "(032830) 906192", 276 | "fermentum.convallis@pharetrautpharetra.edu", 277 | "2019-01-23T10:47:57-08:00" 278 | ], 279 | [ 280 | "Hancock, Hall H.", 281 | "(027) 32770894", 282 | "ac@risus.ca", 283 | "2018-04-20T05:44:16-07:00" 284 | ], 285 | [ 286 | "Steele, Thor Y.", 287 | "(07090) 6374831", 288 | "Aenean.sed.pede@non.org", 289 | "2017-08-28T10:42:06-07:00" 290 | ], 291 | [ 292 | "Lindsay, Quentin X.", 293 | "(0559) 68844378", 294 | "posuere@lacusAliquamrutrum.co.uk", 295 | "2018-03-21T10:04:51-07:00" 296 | ], 297 | [ 298 | "Harrison, Priscilla O.", 299 | "(032448) 687594", 300 | "Morbi@fringillaeuismodenim.edu", 301 | "2018-03-28T19:04:45-07:00" 302 | ], 303 | [ 304 | "Bender, Francesca F.", 305 | "(029) 31825372", 306 | "libero.Proin.sed@adipiscingMaurismolestie.org", 307 | "2017-07-02T00:07:43-07:00" 308 | ], 309 | [ 310 | "Goodman, Gary H.", 311 | "(040) 57407330", 312 | "Aenean.gravida@nunc.co.uk", 313 | "2018-01-16T11:19:36-08:00" 314 | ], 315 | [ 316 | "Nolan, Michelle R.", 317 | "(07517) 6853721", 318 | "pulvinar@morbitristique.edu", 319 | "2017-10-22T20:02:38-07:00" 320 | ], 321 | [ 322 | "Thompson, Bell M.", 323 | "(038910) 193904", 324 | "vitae.velit@ante.com", 325 | "2018-06-03T22:58:40-07:00" 326 | ], 327 | [ 328 | "Bolton, Emerald C.", 329 | "(035609) 962842", 330 | "luctus.aliquet.odio@et.edu", 331 | "2017-07-15T19:52:05-07:00" 332 | ], 333 | [ 334 | "Leblanc, Halla G.", 335 | "(0779) 91543670", 336 | "arcu.Curabitur@lorem.com", 337 | "2018-06-12T23:03:15-07:00" 338 | ], 339 | [ 340 | "Munoz, Ingrid P.", 341 | "(0572) 27393453", 342 | "Morbi@risusatfringilla.com", 343 | "2018-03-21T10:58:38-07:00" 344 | ], 345 | [ 346 | "Ruiz, Christopher M.", 347 | "(01633) 5097679", 348 | "nibh.dolor.nonummy@natoque.org", 349 | "2018-04-11T22:48:55-07:00" 350 | ], 351 | [ 352 | "Miller, Ethan K.", 353 | "(03688) 9111675", 354 | "eu.neque.pellentesque@purusmauris.co.uk", 355 | "2018-02-17T16:23:35-08:00" 356 | ], 357 | [ 358 | "Knowles, Isaac E.", 359 | "(064) 36890208", 360 | "imperdiet@Cras.co.uk", 361 | "2018-07-10T15:34:37-07:00" 362 | ], 363 | [ 364 | "Woodard, Simon J.", 365 | "(0465) 18317354", 366 | "interdum.enim.non@non.edu", 367 | "2018-11-10T04:46:17-08:00" 368 | ], 369 | [ 370 | "Martin, Ariana Y.", 371 | "(012) 32044131", 372 | "id@lectusa.com", 373 | "2017-05-11T21:57:37-07:00" 374 | ], 375 | [ 376 | "Ferguson, Piper B.", 377 | "(073) 05711570", 378 | "nisl.sem@fermentum.org", 379 | "2018-08-15T15:27:01-07:00" 380 | ], 381 | [ 382 | "England, Craig L.", 383 | "(0938) 91487056", 384 | "sit.amet@eleifendnecmalesuada.co.uk", 385 | "2018-05-14T16:10:46-07:00" 386 | ], 387 | [ 388 | "Herrera, Maya O.", 389 | "(033160) 981732", 390 | "mollis.non.cursus@Quisquefringillaeuismod.edu", 391 | "2018-04-27T19:11:35-07:00" 392 | ], 393 | [ 394 | "Byrd, Mollie A.", 395 | "(0673) 07922686", 396 | "natoque@augue.ca", 397 | "2018-11-28T18:46:57-08:00" 398 | ], 399 | [ 400 | "Bolton, Leilani K.", 401 | "(0453) 10718341", 402 | "magna@ligulaDonecluctus.ca", 403 | "2017-10-23T02:07:55-07:00" 404 | ], 405 | [ 406 | "Schultz, Stewart D.", 407 | "(0457) 19301246", 408 | "sodales.Mauris@Donecest.co.uk", 409 | "2018-03-27T21:32:39-07:00" 410 | ], 411 | [ 412 | "Patton, Neville G.", 413 | "(04001) 7856563", 414 | "pede.Cras@inmolestie.org", 415 | "2017-12-02T23:17:38-08:00" 416 | ], 417 | [ 418 | "Norman, Erica U.", 419 | "(045) 70953375", 420 | "lorem.Donec.elementum@posuerecubiliaCurae.ca", 421 | "2017-11-22T17:09:53-08:00" 422 | ], 423 | [ 424 | "Coffey, Cecilia Q.", 425 | "(0667) 23030140", 426 | "lacinia@luctusipsumleo.co.uk", 427 | "2017-10-16T18:40:24-07:00" 428 | ], 429 | [ 430 | "Dyer, Urielle F.", 431 | "(0108) 63966724", 432 | "sagittis.augue.eu@enim.com", 433 | "2018-05-01T01:44:40-07:00" 434 | ], 435 | [ 436 | "Finley, Carter V.", 437 | "(06033) 2422861", 438 | "sit@adipiscing.com", 439 | "2017-07-27T06:52:49-07:00" 440 | ], 441 | [ 442 | "Peters, Chancellor G.", 443 | "(0470) 77153459", 444 | "ac@cursus.co.uk", 445 | "2018-10-08T00:29:33-07:00" 446 | ], 447 | [ 448 | "Carney, Hilda D.", 449 | "(039577) 354814", 450 | "lectus.rutrum.urna@etmagnisdis.com", 451 | "2019-01-01T08:43:25-08:00" 452 | ], 453 | [ 454 | "Blankenship, Steven O.", 455 | "(0181) 59302085", 456 | "id.erat.Etiam@Morbinonsapien.com", 457 | "2018-11-04T21:33:55-08:00" 458 | ], 459 | [ 460 | "Thornton, Lucian H.", 461 | "(038799) 467472", 462 | "rutrum@faucibus.org", 463 | "2017-05-19T23:20:48-07:00" 464 | ], 465 | [ 466 | "Chen, Wallace I.", 467 | "(039) 71566608", 468 | "pellentesque@mollis.org", 469 | "2017-08-15T07:11:32-07:00" 470 | ], 471 | [ 472 | "Wilson, Trevor W.", 473 | "(04643) 0928990", 474 | "netus@ullamcorper.ca", 475 | "2018-07-04T21:55:58-07:00" 476 | ], 477 | [ 478 | "Blevins, Ocean K.", 479 | "(064) 06535583", 480 | "dolor.sit.amet@hendrerita.com", 481 | "2017-10-28T00:00:16-07:00" 482 | ], 483 | [ 484 | "Carter, Gary X.", 485 | "(01223) 4827760", 486 | "ultricies.ornare@necanteMaecenas.org", 487 | "2017-09-02T05:48:11-07:00" 488 | ], 489 | [ 490 | "Graves, Harding L.", 491 | "(0368) 43769672", 492 | "malesuada.id@vitaevelitegestas.ca", 493 | "2018-06-30T12:03:27-07:00" 494 | ], 495 | [ 496 | "Buchanan, Noah Y.", 497 | "(0218) 78023153", 498 | "erat.nonummy@dictum.org", 499 | "2018-03-05T06:46:35-08:00" 500 | ], 501 | [ 502 | "Stokes, Cynthia G.", 503 | "(036882) 755750", 504 | "Phasellus.fermentum.convallis@Suspendisse.com", 505 | "2018-07-20T06:39:17-07:00" 506 | ], 507 | [ 508 | "Parsons, Ginger S.", 509 | "(035296) 866620", 510 | "Donec.nibh@lectusconvallisest.net", 511 | "2017-06-29T06:40:19-07:00" 512 | ], 513 | [ 514 | "Wise, Patricia L.", 515 | "(030394) 513880", 516 | "mattis@ametluctusvulputate.edu", 517 | "2018-08-21T23:38:00-07:00" 518 | ], 519 | [ 520 | "Grant, Emi V.", 521 | "(01987) 7636129", 522 | "Curae@augueeutempor.edu", 523 | "2018-05-25T18:06:16-07:00" 524 | ], 525 | [ 526 | "Flowers, Joelle L.", 527 | "(01098) 3248522", 528 | "magna.sed.dui@vulputateposuere.net", 529 | "2017-05-25T16:13:35-07:00" 530 | ], 531 | [ 532 | "Cummings, Dieter L.", 533 | "(099) 98419182", 534 | "sagittis.augue.eu@malesuadamalesuada.co.uk", 535 | "2018-11-15T23:52:09-08:00" 536 | ], 537 | [ 538 | "Schmidt, Dexter Y.", 539 | "(050) 47032741", 540 | "euismod.in@adui.net", 541 | "2017-11-10T02:41:50-08:00" 542 | ], 543 | [ 544 | "Melendez, Maya E.", 545 | "(06838) 1405502", 546 | "a.enim.Suspendisse@gravidamaurisut.edu", 547 | "2017-12-29T18:31:25-08:00" 548 | ], 549 | [ 550 | "Wallace, Arsenio M.", 551 | "(034915) 135458", 552 | "sit.amet.diam@risusodio.edu", 553 | "2017-02-24T20:17:06-08:00" 554 | ], 555 | [ 556 | "Murphy, Odette L.", 557 | "(07064) 5859549", 558 | "nibh.lacinia@Maurisvestibulumneque.ca", 559 | "2017-03-30T09:23:40-07:00" 560 | ], 561 | [ 562 | "Pratt, Victor P.", 563 | "(02376) 3667462", 564 | "Integer.vulputate.risus@nonvestibulum.edu", 565 | "2018-09-18T01:05:02-07:00" 566 | ], 567 | [ 568 | "Burton, Kylan F.", 569 | "(0365) 95428253", 570 | "Cum.sociis.natoque@dolornonummyac.ca", 571 | "2019-01-30T01:44:16-08:00" 572 | ], 573 | [ 574 | "Moore, Chloe Y.", 575 | "(0967) 99520193", 576 | "magna.tellus.faucibus@aliquamadipiscing.net", 577 | "2017-09-30T18:58:52-07:00" 578 | ], 579 | [ 580 | "Mcmahon, Lana H.", 581 | "(037183) 594789", 582 | "in.magna.Phasellus@odioNam.net", 583 | "2017-10-01T21:31:19-07:00" 584 | ], 585 | [ 586 | "Booker, Calista J.", 587 | "(051) 00496072", 588 | "eget@afelisullamcorper.org", 589 | "2019-01-21T02:13:17-08:00" 590 | ], 591 | [ 592 | "Gibson, Ross K.", 593 | "(092) 75208613", 594 | "id.ante@Donecvitae.com", 595 | "2018-06-27T18:36:54-07:00" 596 | ], 597 | [ 598 | "Good, Josiah O.", 599 | "(035681) 236468", 600 | "dolor.vitae.dolor@tellusnon.net", 601 | "2018-08-15T13:23:30-07:00" 602 | ], 603 | [ 604 | "Thompson, Keely V.", 605 | "(00181) 2455456", 606 | "convallis.ligula@scelerisque.edu", 607 | "2018-02-20T12:56:19-08:00" 608 | ] 609 | ] 610 | } 611 | --------------------------------------------------------------------------------