├── .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 |
21 | {
22 | this.props.users.map(user => (
23 | -
24 | {user.name}
25 |
26 | see detail
27 |
28 |
29 | ))
30 | }
31 |
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 | | id | {user.id} |
26 | | name | {user.name} |
27 | | email | {user.email} |
28 | | phone | {user.phone} |
29 |
30 |
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 |
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 |
--------------------------------------------------------------------------------