diff --git a/.babelrc b/.babelrc
index d6b15d8..97bb45c 100644
--- a/.babelrc
+++ b/.babelrc
@@ -4,14 +4,18 @@
"targets": {
"uglify": true
},
+ "loose": true,
"modules": false,
- "useBuiltIns": true
+ "useBuiltIns": "entry"
}],
"react",
"stage-1"
],
"plugins": [
- "react-loadable/babel"
+ "react-loadable/babel",
+ ["styled-components", {
+ "ssr": true
+ }]
],
"env": {
"test": {
@@ -23,7 +27,11 @@
},
"production": {
"plugins": [
- "transform-react-remove-prop-types"
+ "transform-react-remove-prop-types",
+ ["styled-components", {
+ "displayName": false,
+ "ssr": true
+ }]
]
}
}
diff --git a/.eslintignore b/.eslintignore
index 4a5d465..e6cdaa6 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,6 +1,9 @@
.idea
.vscode
+es
+lib
build
coverage
node_modules
npm-debug.log*
+lerna-debug.log*
diff --git a/.gitignore b/.gitignore
index 4a5d465..e6cdaa6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
.idea
.vscode
+es
+lib
build
coverage
node_modules
npm-debug.log*
+lerna-debug.log*
diff --git a/.stylelintignore b/.stylelintignore
index 4a5d465..e6cdaa6 100644
--- a/.stylelintignore
+++ b/.stylelintignore
@@ -1,6 +1,9 @@
.idea
.vscode
+es
+lib
build
coverage
node_modules
npm-debug.log*
+lerna-debug.log*
diff --git a/.stylelintrc b/.stylelintrc
index d2ea289..33a4989 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -1,3 +1,12 @@
{
- "extends": "stylelint-config-standard",
+ "processors": ["stylelint-processor-styled-components"],
+ "extends": [
+ "stylelint-config-standard",
+ "stylelint-config-styled-components"
+ ],
+ "rules": {
+ "indentation": null,
+ "comment-empty-line-before": null
+ },
+ "syntax": "scss"
}
diff --git a/client/components/Flex/Col.js b/client/components/Flex/Col.js
deleted file mode 100644
index 5b98a94..0000000
--- a/client/components/Flex/Col.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cn from 'classnames';
-
-const Col = ({ className, tag, children, ...restProps }) =>
- React.createElement(tag, {
- className: cn('col', {
- [`col--${restProps.size}`]: restProps.size,
- [`col--offset-${restProps.offset}`]: restProps.offset,
- 'col--first': restProps.first,
- 'col--last': restProps.last,
- 'col--reverse': restProps.reverse,
- }, className),
- }, children);
-
-Col.propTypes = {
- className: PropTypes.string,
- tag: PropTypes.string,
- size: PropTypes.number,
- offset: PropTypes.number,
- first: PropTypes.bool,
- last: PropTypes.bool,
- reverse: PropTypes.bool,
- children: PropTypes.node.isRequired,
-};
-
-Col.defaultProps = {
- className: '',
- tag: 'div',
- size: 0,
- offset: 0,
- first: false,
- last: false,
- reverse: false,
-};
-
-export default Col;
diff --git a/client/components/Flex/Flex.js b/client/components/Flex/Flex.js
deleted file mode 100644
index 3e3e86a..0000000
--- a/client/components/Flex/Flex.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import Grid from './Grid';
-import Row from './Row';
-import Col from './Col';
-import './flex.css';
-
-export default {
- Grid,
- Row,
- Col,
-};
diff --git a/client/components/Flex/Grid.js b/client/components/Flex/Grid.js
deleted file mode 100644
index be59f9d..0000000
--- a/client/components/Flex/Grid.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cn from 'classnames';
-
-const Grid = ({ className, tag, fluid, children, ...restProps }) =>
- React.createElement(tag, {
- className: cn({
- container: !fluid,
- 'container-fluid': fluid,
- }, className),
- ...restProps,
- }, children);
-
-Grid.propTypes = {
- className: PropTypes.string,
- tag: PropTypes.string,
- fluid: PropTypes.bool,
- children: PropTypes.node.isRequired,
-};
-
-Grid.defaultProps = {
- className: '',
- tag: 'div',
- fluid: false,
-};
-
-export default Grid;
diff --git a/client/components/Flex/Row.js b/client/components/Flex/Row.js
deleted file mode 100644
index abf9963..0000000
--- a/client/components/Flex/Row.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cn from 'classnames';
-
-const Row = ({ className, tag, children, ...restProps }) =>
- React.createElement(tag, {
- className: cn('row', {
- 'row--start': restProps.start,
- 'row--center': restProps.center,
- 'row--end': restProps.end,
- 'row--top': restProps.top,
- 'row--middle': restProps.middle,
- 'row--bottom': restProps.bottom,
- 'row--around': restProps.around,
- 'row--between': restProps.between,
- 'row--reverse': restProps.reverse,
- }, className),
- }, children);
-
-Row.propTypes = {
- className: PropTypes.string,
- tag: PropTypes.string,
- start: PropTypes.bool,
- center: PropTypes.bool,
- end: PropTypes.bool,
- top: PropTypes.bool,
- middle: PropTypes.bool,
- bottom: PropTypes.bool,
- around: PropTypes.bool,
- between: PropTypes.bool,
- reverse: PropTypes.bool,
- children: PropTypes.node.isRequired,
-};
-
-Row.defaultProps = {
- className: '',
- tag: 'div',
- start: false,
- center: false,
- end: false,
- top: false,
- middle: false,
- bottom: false,
- around: false,
- between: false,
- reverse: false,
-};
-
-export default Row;
diff --git a/client/components/Flex/flex.css b/client/components/Flex/flex.css
deleted file mode 100644
index 7e10b24..0000000
--- a/client/components/Flex/flex.css
+++ /dev/null
@@ -1,200 +0,0 @@
-.container {
- margin-right: auto;
- margin-left: auto;
-}
-
-.container-fluid {
- margin-right: auto;
- margin-left: auto;
- padding-left: 16px;
- padding-right: 16px;
-}
-
-.row {
- box-sizing: border-box;
- display: flex;
- flex: 0 1 auto;
- flex-flow: row wrap;
- margin-right: -8px;
- margin-left: -8px;
-}
-
-.row--start {
- justify-content: flex-start;
- text-align: start;
-}
-
-.row--center {
- justify-content: center;
- text-align: center;
-}
-
-.row--end {
- justify-content: flex-end;
- text-align: end;
-}
-
-.row--top {
- align-items: flex-start;
-}
-
-.row--middle {
- align-items: center;
-}
-
-.row--bottom {
- align-items: flex-end;
-}
-
-.row--around {
- justify-content: space-around;
-}
-
-.row--between {
- justify-content: space-between;
-}
-
-.row--reverse {
- flex-direction: row-reverse;
-}
-
-.col,
-.col--1,
-.col--2,
-.col--3,
-.col--4,
-.col--5,
-.col--6,
-.col--7,
-.col--8,
-.col--9,
-.col--10,
-.col--11,
-.col--12 {
- box-sizing: border-box;
- flex: 0 0 auto;
- padding-right: 8px;
- padding-left: 8px;
-}
-
-.col {
- flex-grow: 1;
- flex-basis: 0;
- max-width: 100%;
-}
-
-.col--1 {
- flex-basis: 8.333%;
- max-width: 8.333%;
-}
-
-.col--2 {
- flex-basis: 16.667%;
- max-width: 16.667%;
-}
-
-.col--3 {
- flex-basis: 25%;
- max-width: 25%;
-}
-
-.col--4 {
- flex-basis: 33.333%;
- max-width: 33.333%;
-}
-
-.col--5 {
- flex-basis: 41.667%;
- max-width: 41.667%;
-}
-
-.col--6 {
- flex-basis: 50%;
- max-width: 50%;
-}
-
-.col--7 {
- flex-basis: 58.333%;
- max-width: 58.333%;
-}
-
-.col--8 {
- flex-basis: 66.667%;
- max-width: 66.667%;
-}
-
-.col--9 {
- flex-basis: 75%;
- max-width: 75%;
-}
-
-.col--10 {
- flex-basis: 83.333%;
- max-width: 83.333%;
-}
-
-.col--11 {
- flex-basis: 91.667%;
- max-width: 91.667%;
-}
-
-.col--12 {
- flex-basis: 100%;
- max-width: 100%;
-}
-
-.col--offset-1 {
- margin-left: 8.333%;
-}
-
-.col--offset-2 {
- margin-left: 16.667%;
-}
-
-.col--offset-3 {
- margin-left: 25%;
-}
-
-.col--offset-4 {
- margin-left: 33.333%;
-}
-
-.col--offset-5 {
- margin-left: 41.667%;
-}
-
-.col--offset-6 {
- margin-left: 50%;
-}
-
-.col--offset-7 {
- margin-left: 58.333%;
-}
-
-.col--offset-8 {
- margin-left: 66.667%;
-}
-
-.col--offset-9 {
- margin-left: 75%;
-}
-
-.col--offset-10 {
- margin-left: 83.333%;
-}
-
-.col--offset-11 {
- margin-left: 91.667%;
-}
-
-.col--first {
- order: -1;
-}
-
-.col--last {
- order: 1;
-}
-
-.col--reverse {
- flex-direction: column-reverse;
-}
diff --git a/client/components/LoaderHOC/LoaderHOC.js b/client/components/LoaderHOC/LoaderHOC.js
deleted file mode 100644
index 4981478..0000000
--- a/client/components/LoaderHOC/LoaderHOC.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { Component } from 'react';
-import isEmpty from 'lodash/isEmpty';
-import moment from 'moment';
-import './loaderHOC.css';
-
-const LoaderHOC = (property) =>
- (WrappedComponent) =>
- class extends Component {
- componentDidMount() {
- this.startTime = moment();
- }
-
- componentWillUpdate() {
- this.endTime = moment();
- }
-
- render() {
- const additionalProps = {
- loadTime: this.endTime ? `${this.endTime.diff(this.startTime, 'ms')}ms` : null,
- };
-
- return isEmpty(this.props[property]) ? (
-
- ) : (
-
- );
- }
- };
-
-export default LoaderHOC;
diff --git a/client/components/LoaderHOC/loaderHOC.css b/client/components/LoaderHOC/loaderHOC.css
deleted file mode 100644
index 7e1bf42..0000000
--- a/client/components/LoaderHOC/loaderHOC.css
+++ /dev/null
@@ -1,63 +0,0 @@
-@import '../../vendor/styles/variables.css';
-
-.sk-cube-grid {
- width: 30px;
- height: 30px;
- margin: 100px auto;
-}
-
-.sk-cube-grid .sk-cube {
- width: 33%;
- height: 33%;
- background-color: var(--color-primary);
- float: left;
- animation: sk-cubeGridScaleDelay 1.5s infinite ease-in-out;
-}
-
-.sk-cube-grid .sk-cube1 {
- animation-delay: 0.2s;
-}
-
-.sk-cube-grid .sk-cube2 {
- animation-delay: 0.3s;
-}
-
-.sk-cube-grid .sk-cube3 {
- animation-delay: 0.4s;
-}
-
-.sk-cube-grid .sk-cube4 {
- animation-delay: 0.1s;
-}
-
-.sk-cube-grid .sk-cube5 {
- animation-delay: 0.2s;
-}
-
-.sk-cube-grid .sk-cube6 {
- animation-delay: 0.3s;
-}
-
-.sk-cube-grid .sk-cube7 {
- animation-delay: 0s;
-}
-
-.sk-cube-grid .sk-cube8 {
- animation-delay: 0.1s;
-}
-
-.sk-cube-grid .sk-cube9 {
- animation-delay: 0.2s;
-}
-
-@keyframes sk-cubeGridScaleDelay {
- 0%,
- 70%,
- 100% {
- transform: scale3d(1, 1, 1);
- }
-
- 35% {
- transform: scale3d(0, 0, 1);
- }
-}
diff --git a/client/index.js b/client/index.js
deleted file mode 100644
index 2828feb..0000000
--- a/client/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { Provider } from 'react-redux';
-import { Router, browserHistory } from 'react-router';
-import { ReduxAsyncConnect } from 'redux-connect';
-import configureStore from './store/configureStore';
-import routes from './routes';
-
-const store = configureStore(window.__INITIAL_STATE__);
-
-ReactDOM.render(
-
- }
- onUpdate={() => window.scrollTo(0, 0)}
- />
- ,
- document.getElementById('root'),
-);
diff --git a/client/routes.js b/client/routes.js
deleted file mode 100644
index b6a405b..0000000
--- a/client/routes.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import React from 'react';
-import { Route, IndexRoute } from 'react-router';
-import NotFoundPage from './components/NotFound/NotFound';
-import Wrapper from './views/Wrapper/Wrapper';
-import importCss from './utils/importCss';
-
-export const loadRoute = {
- LandingPage: () => Promise.all([
- import('./views/LandingPage/LandingPage' /* webpackChunkName: 'LandingPage' */),
- importCss('LandingPage'),
- ]),
-};
-
-export default (
-
-
- loadRoute.LandingPage()
- .then(([module]) => cb(null, module.default))}
- />
-
-
-
-
-);
diff --git a/client/services/user/userActionCreators.js b/client/services/user/userActionCreators.js
deleted file mode 100644
index 435c26d..0000000
--- a/client/services/user/userActionCreators.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import * as userTypes from './userTypes';
-
-export const getAll = () => (dispatch, getState, { api }) =>
- dispatch({
- type: userTypes.GET_ALL,
- promise: api.get('/api', {
- results: 3,
- inc: 'name,location,picture',
- }),
- });
-
-export const getOne = () => (dispatch, getState, { api }) =>
- dispatch({
- type: userTypes.GET_ONE,
- promise: api.get('/api', {
- results: 1,
- inc: 'name,location,picture',
- }),
- });
diff --git a/client/services/user/userModel.js b/client/services/user/userModel.js
deleted file mode 100644
index 6790857..0000000
--- a/client/services/user/userModel.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const userProto = {
- get introduce() {
- return `Hey, my name is ${this.name}`;
- },
-};
-
-export const makeUser = (name) => {
- const user = Object.create(userProto);
- user.name = name;
- return user;
-};
-
-export const normalize = (users) => ({
- byId: users.reduce((obj, user, index) => ({ ...obj, [index]: user }), {}),
- ids: users.map((user, index) => index),
-});
-
-export const getAll = (ids, byId) =>
- ids.map((userId) => byId[userId]);
diff --git a/client/services/user/userReducer.js b/client/services/user/userReducer.js
deleted file mode 100644
index c60e99a..0000000
--- a/client/services/user/userReducer.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { handle } from 'redux-pack';
-import * as userTypes from './userTypes';
-import * as userModel from './userModel';
-
-export const initialState = {
- byId: {},
- ids: [],
- isLoading: false,
-};
-
-export default (state = initialState, action) => {
- const { type, payload } = action;
-
- switch (type) {
- case userTypes.GET_ALL: return handle(state, action, {
- start: (s) => ({
- ...s,
- isLoading: true,
- }),
- success: (s) => ({
- ...s,
- ...userModel.normalize(payload.results),
- }),
- failure: (s) => ({
- ...s,
- error: payload.error,
- }),
- finish: (s) => ({
- ...s,
- isLoading: false,
- }),
- });
-
- default:
- return state;
- }
-};
diff --git a/client/store/configureStore.js b/client/store/configureStore.js
deleted file mode 100644
index 80f4c16..0000000
--- a/client/store/configureStore.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { createStore, compose, applyMiddleware } from 'redux';
-import reduxThunk from 'redux-thunk';
-import { middleware as reduxPack } from 'redux-pack';
-import api from '../utils/api';
-import rootReducer from './rootReducer';
-
-const middlewares = [
- reduxThunk.withExtraArgument({ api }),
- reduxPack,
-];
-
-const storeEnhancers = [
- applyMiddleware(...middlewares),
- __BROWSER__ && __LOCAL__ && window.devToolsExtension ? window.devToolsExtension() : (f) => f,
-];
-
-export default (initialState) => createStore(
- rootReducer,
- initialState,
- compose(...storeEnhancers),
-);
diff --git a/client/store/rootReducer.js b/client/store/rootReducer.js
deleted file mode 100644
index 708f7a1..0000000
--- a/client/store/rootReducer.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { combineReducers } from 'redux';
-import { reducer as reduxAsyncConnectReducer } from 'redux-connect';
-import userReducer from '../services/user/userReducer';
-
-export default combineReducers({
- reduxAsyncConnect: reduxAsyncConnectReducer,
- user: userReducer,
-});
diff --git a/client/utils/api.js b/client/utils/api.js
deleted file mode 100644
index 0e97431..0000000
--- a/client/utils/api.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import fetch from 'isomorphic-fetch';
-import queryString from 'query-string';
-import config from '../../config';
-
-const fireRequest = async (method, url, data) => {
- const fullUrl = `${config.apiUrl}${url}`;
- const options = {
- method,
- body: JSON.stringify(data),
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json',
- },
- };
-
- const response = await fetch(fullUrl, options);
- const json = await response.json();
- return response.ok ? json : Promise.reject(json);
-};
-
-export default {
- get(url, query) {
- const qs = queryString.stringify(query, { arrayFormat: 'index' });
- return fireRequest('GET', `${url}?${qs}`);
- },
-
- post(url, data) {
- return fireRequest('POST', url, data);
- },
-
- put(url, data) {
- return fireRequest('PUT', url, data);
- },
-
- delete(url) {
- return fireRequest('DELETE', url);
- },
-};
diff --git a/client/utils/importCss.js b/client/utils/importCss.js
deleted file mode 100644
index ccc53fd..0000000
--- a/client/utils/importCss.js
+++ /dev/null
@@ -1,38 +0,0 @@
-export default (chunkName) => {
- if (!__BROWSER__) {
- return Promise.resolve();
- } else if (!(chunkName in window.__ASSETS_MANIFEST__)) {
- return Promise.reject(`chunk not found: ${chunkName}`);
- } else if (!window.__ASSETS_MANIFEST__[chunkName].css) {
- return Promise.resolve(`chunk css does not exist: ${chunkName}`);
- } else if (document.getElementById(`${chunkName}.css`)) {
- return Promise.resolve(`css chunk already loaded: ${chunkName}`);
- }
-
- const head = document.getElementsByTagName('head')[0];
- const link = document.createElement('link');
- link.href = window.__ASSETS_MANIFEST__[chunkName].css;
- link.id = `${chunkName}.css`;
- link.rel = 'stylesheet';
-
- return new Promise((resolve, reject) => {
- let timeout;
-
- link.onload = () => {
- link.onload = null;
- link.onerror = null;
- clearTimeout(timeout);
- resolve(`css chunk loaded: ${chunkName}`);
- };
-
- link.onerror = () => {
- link.onload = null;
- link.onerror = null;
- clearTimeout(timeout);
- reject(new Error(`could not load css chunk: ${chunkName}`));
- };
-
- timeout = setTimeout(link.onerror, 30000);
- head.appendChild(link);
- });
-};
diff --git a/client/utils/pluralize.js b/client/utils/pluralize.js
deleted file mode 100644
index b0ce1f8..0000000
--- a/client/utils/pluralize.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export default (amount, text, suffix = 's') =>
- +amount > 1 ? `${text}${suffix}` : text;
diff --git a/client/vendor/styles/styles.css b/client/vendor/styles/styles.css
deleted file mode 100644
index f7718e0..0000000
--- a/client/vendor/styles/styles.css
+++ /dev/null
@@ -1,51 +0,0 @@
-@import 'normalize.css';
-@import './utility.css';
-@import './variables.css';
-
-html {
- box-sizing: border-box;
- font-size: 62.5%;
-}
-
-body {
- background: #fff;
- color: var(--color-tertiary);
- font-size: 1.4em;
- font-weight: 400;
- font-family: system-ui, sans-serif;
-}
-
-*,
-*::after,
-*::before {
- box-sizing: inherit;
- -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-}
-
-a {
- color: inherit;
- text-decoration: none;
-}
-
-ul {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-ol {
- list-style: decimal inside;
- padding: 0;
- margin: 0;
-}
-
-p,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- padding: 0;
- margin: 0;
-}
diff --git a/client/vendor/styles/utility.css b/client/vendor/styles/utility.css
deleted file mode 100644
index ef3035e..0000000
--- a/client/vendor/styles/utility.css
+++ /dev/null
@@ -1,19 +0,0 @@
-@import './variables.css';
-
-.hide {
- display: none;
-}
-
-.text {
- &-center {
- text-align: center;
- }
-
- &-left {
- text-align: left;
- }
-
- &-right {
- text-align: right;
- }
-}
diff --git a/client/vendor/styles/variables.css b/client/vendor/styles/variables.css
deleted file mode 100644
index e16448c..0000000
--- a/client/vendor/styles/variables.css
+++ /dev/null
@@ -1,13 +0,0 @@
-:root {
- /* colors */
- --color-primary: #5500eb;
- --color-secondary: #4a4a4a;
- --color-tertiary: #9b9b9b;
- --color-quaternary: #e0e0e0;
- --color-quinary: #f1f1f1;
- --color-error: #d00;
- --color-link: #0d0;
-
- /* shadows */
- --shadow-primary: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
-}
diff --git a/client/views/LandingPage/LandingPage.js b/client/views/LandingPage/LandingPage.js
deleted file mode 100644
index 5bb8bd4..0000000
--- a/client/views/LandingPage/LandingPage.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import isEmpty from 'lodash/isEmpty';
-import { asyncConnect } from 'redux-connect';
-import { bindActionCreators, compose } from 'redux';
-import * as userActionCreators from '../../services/user/userActionCreators';
-import UsersListAsync from './UserList/UserListAsync';
-import './landingPage.css';
-
-class LandingPage extends React.Component {
- componentDidMount() {
- const { userActions } = this.props;
- userActions.getAll();
- }
-
- render() {
- return (
-
-
PWA
-
An opinionated progressive web app boilerplate
-
-
- );
- }
-}
-
-LandingPage.propTypes = {
- userActions: PropTypes.object.isRequired,
-};
-
-const beforeRouteEnter = [{
- promise: ({ store: { dispatch, getState } }) => {
- const promise = isEmpty(getState().user.ids)
- ? dispatch(userActionCreators.getAll()) : null;
- return __BROWSER__ ? null : promise;
- },
-}];
-
-const mapDispatchToProps = (dispatch) => ({
- userActions: bindActionCreators(userActionCreators, dispatch),
-});
-
-export default compose(
- asyncConnect(beforeRouteEnter, null, mapDispatchToProps),
-)(LandingPage);
diff --git a/client/views/LandingPage/UserList/UserList.js b/client/views/LandingPage/UserList/UserList.js
deleted file mode 100644
index 5e3efda..0000000
--- a/client/views/LandingPage/UserList/UserList.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { compose } from 'redux';
-import { connect } from 'react-redux';
-import cn from 'classnames';
-import Flex from '../../../components/Flex/Flex';
-import LoaderHOC from '../../../components/LoaderHOC/LoaderHOC';
-import * as userModel from '../../../services/user/userModel';
-import './userList.css';
-
-class UsersList extends Component {
- state = {
- active: 1,
- };
-
- showUser = (index) => () => {
- this.setState({ active: index });
- }
-
- render() {
- const { users, loadTime } = this.props;
- const { active } = this.state;
-
- return users.length ? (
-
- {
- loadTime ? (
- Took: {loadTime}
- ) : null
- }
-
- {
- users.map(({ name, picture }, i) => (
-
-
-
- {name.first}
-
-
- ))
- }
-
- {users[active].location.street}
-
- ) : null;
- }
-}
-
-UsersList.propTypes = {
- users: PropTypes.array.isRequired,
- loadTime: PropTypes.string,
-};
-
-UsersList.defaultProps = {
- loadTime: '',
-};
-
-const mapStateToProps = (state) => ({
- users: userModel.getAll(state.user.ids, state.user.byId),
-});
-
-export default compose(
- connect(mapStateToProps),
- LoaderHOC('users'),
-)(UsersList);
diff --git a/client/views/LandingPage/UserList/UserListAsync.js b/client/views/LandingPage/UserList/UserListAsync.js
deleted file mode 100644
index 828c55e..0000000
--- a/client/views/LandingPage/UserList/UserListAsync.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import Loadable from 'react-loadable';
-import importCss from '../../../utils/importCss';
-
-export default Loadable({
- loader: () => {
- importCss('UserList');
- return import('./UserList' /* webpackChunkName: 'UserList' */);
- },
- // provide better UX by using a skeleton screen here instead of just text
- loading: () => Loading the UserList component...
,
-});
diff --git a/client/views/LandingPage/UserList/userList.css b/client/views/LandingPage/UserList/userList.css
deleted file mode 100644
index 77cc0dd..0000000
--- a/client/views/LandingPage/UserList/userList.css
+++ /dev/null
@@ -1,33 +0,0 @@
-@import '../../../vendor/styles/variables.css';
-
-.user {
- margin-top: 50px;
-
- &__list {
- margin: 16px 0;
- }
-
- &__img {
- border-radius: 50%;
- border: 3px solid transparent;
- padding: 4px;
- cursor: pointer;
- transition: all 0.2s ease-in-out;
-
- &--active {
- border-color: var(--color-primary);
- }
- }
-
- &__name {
- visibility: hidden;
- color: var(--color-secondary);
- margin-top: 8px;
- text-transform: uppercase;
- transition: all 0.2s ease-in-out;
-
- &--visible {
- visibility: visible;
- }
- }
-}
diff --git a/client/views/LandingPage/landingPage.css b/client/views/LandingPage/landingPage.css
deleted file mode 100644
index ccefb72..0000000
--- a/client/views/LandingPage/landingPage.css
+++ /dev/null
@@ -1,6 +0,0 @@
-@import '../../vendor/styles/variables.css';
-
-.landing-page {
- margin-top: 12px;
- text-align: center;
-}
diff --git a/client/views/Wrapper/Wrapper.js b/client/views/Wrapper/Wrapper.js
deleted file mode 100644
index 6a114bc..0000000
--- a/client/views/Wrapper/Wrapper.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import Helmet from 'react-helmet';
-import performanceMark from '../../utils/performanceMark';
-import './wrapper.css';
-
-class Wrapper extends Component {
- componentDidMount() {
- performanceMark('firstInteraction');
- }
-
- render() {
- const { children } = this.props;
-
- return (
-
-
- {children}
-
- );
- }
-}
-
-Wrapper.propTypes = {
- children: PropTypes.element.isRequired,
-};
-
-export default Wrapper;
diff --git a/client/views/Wrapper/wrapper.css b/client/views/Wrapper/wrapper.css
deleted file mode 100644
index e8ff585..0000000
--- a/client/views/Wrapper/wrapper.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.wrapper {
- min-width: 320px;
-}
diff --git a/config/development.js b/config/development.js
deleted file mode 100644
index ff8b4c5..0000000
--- a/config/development.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/config/index.js b/config/index.js
deleted file mode 100644
index 9deee9a..0000000
--- a/config/index.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import defaultConfig from './default';
-import localConfig from './local';
-import developmentConfig from './development';
-import stagingConfig from './staging';
-import productionConfig from './production';
-
-const config = {
- local: {
- ...defaultConfig,
- ...localConfig,
- },
- development: {
- ...defaultConfig,
- ...developmentConfig,
- },
- staging: {
- ...defaultConfig,
- ...stagingConfig,
- },
- production: {
- ...defaultConfig,
- ...productionConfig,
- },
-};
-
-export default config[__PWA_ENV__];
diff --git a/config/local.js b/config/local.js
deleted file mode 100644
index ff8b4c5..0000000
--- a/config/local.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/package.json b/package.json
index d3e4d05..65ad0cb 100644
--- a/package.json
+++ b/package.json
@@ -13,12 +13,11 @@
"boilerplate",
"jest"
],
- "main": "server/index.js",
"scripts": {
"start": "npm run local",
"stop": "pm2 delete pm2.json",
"prelocal": "mkdir -p ./build/server/ && touch ./build/server/index.js",
- "local": "PWA_ENV=local PWA_PUBLIC_PATH=http://localhost:8080/build/client/ PWA_SSR=false NODE_ENV=development PORT=8000 webpack-dashboard -- run-p local:*",
+ "local": "PWA_ENV=local PWA_PUBLIC_PATH=http://localhost:8080/build/client/ PWA_SSR=true NODE_ENV=development PORT=8000 webpack-dashboard -- run-p local:*",
"local:client": "webpack-dev-server --config ./webpack.client.js --hot",
"local:server": "webpack --config ./webpack.server.js --watch",
"local:serve": "pm2 start pm2.json --only pwa-local",
@@ -36,84 +35,84 @@
"build:server": "webpack --config ./webpack.server.js --progress",
"lint": "run-p lint:*",
"lint:eslint": "eslint .",
- "lint:stylelint": "stylelint '**/*.css'",
+ "lint:stylelint": "stylelint '**/*.js'",
"test": "NODE_ENV=test jest",
"precommit": "lint-staged && npm run test",
"pm2": "pm2"
},
"lint-staged": {
- "*.js": ["eslint"],
- "*.css": ["stylelint"]
+ "*.js": [
+ "eslint",
+ "stylelint"
+ ]
},
"license": "MIT",
"devDependencies": {
"babel-core": "6.26.0",
- "babel-eslint": "8.0.3",
- "babel-jest": "21.2.0",
- "babel-loader": "7.1.2",
- "babel-plugin-transform-react-remove-prop-types": "0.4.10",
+ "babel-eslint": "8.2.2",
+ "babel-jest": "22.4.1",
+ "babel-loader": "7.1.3",
+ "babel-plugin-styled-components": "1.5.1",
+ "babel-plugin-transform-react-remove-prop-types": "0.4.13",
"babel-preset-env": "1.6.1",
"babel-preset-react": "6.24.1",
"babel-preset-stage-1": "6.24.1",
- "css-loader": "0.28.7",
- "eslint": "4.13.1",
+ "eslint": "4.18.2",
"eslint-config-airbnb": "16.1.0",
- "eslint-plugin-import": "2.8.0",
- "eslint-plugin-jsx-a11y": "6.0.2",
- "eslint-plugin-react": "7.5.1",
- "file-loader": "1.1.5",
+ "eslint-plugin-import": "2.9.0",
+ "eslint-plugin-jsx-a11y": "6.0.3",
+ "eslint-plugin-react": "7.7.0",
+ "file-loader": "1.1.11",
"husky": "0.14.3",
- "jest": "21.2.1",
- "lint-staged": "6.0.0",
+ "jest": "22.4.2",
+ "lint-staged": "7.0.0",
"npm-run-all": "4.1.2",
- "pm2": "2.8.0",
- "postcss-cssnext": "2.11.0",
- "postcss-import": "10.0.0",
- "postcss-loader": "1.3.3",
- "postcss-url": "7.0.0",
+ "pm2": "2.10.1",
"rimraf": "2.6.2",
- "style-loader": "0.19.0",
- "stylelint": "8.3.1",
- "stylelint-config-standard": "18.0.0",
- "url-loader": "0.6.2",
- "webpack-dev-server": "2.9.7"
+ "style-loader": "0.20.2",
+ "stylelint": "9.1.1",
+ "stylelint-config-standard": "18.2.0",
+ "stylelint-config-styled-components": "0.1.1",
+ "stylelint-processor-styled-components": "1.3.0",
+ "url-loader": "1.0.1",
+ "webpack-dev-server": "2.11.1"
},
"dependencies": {
"assets-webpack-plugin": "3.5.1",
"babel-polyfill": "6.26.0",
- "classnames": "2.2.5",
- "clean-webpack-plugin": "0.1.17",
- "compression": "1.7.1",
+ "clean-webpack-plugin": "0.1.18",
+ "compression": "1.7.2",
"connect-slashes": "1.3.1",
- "copy-webpack-plugin": "4.2.3",
+ "copy-webpack-plugin": "4.5.0",
"express": "4.16.2",
- "extract-css-chunks-webpack-plugin": "2.0.18",
- "helmet": "3.9.0",
+ "helmet": "3.12.0",
"isomorphic-fetch": "2.2.1",
- "lodash": "4.17.4",
- "moment": "2.19.4",
+ "lodash": "4.17.5",
+ "moment": "2.21.0",
"morgan": "1.9.0",
- "nock": "9.1.4",
- "normalize.css": "7.0.0",
+ "nock": "9.2.3",
"preact": "8.2.7",
- "preact-compat": "3.17.0",
- "prop-types": "15.6.0",
- "query-string": "5.0.1",
- "react": "15.6.1",
- "react-dom": "15.6.1",
+ "preact-compat": "3.18.0",
+ "prop-types": "15.6.1",
+ "query-string": "5.1.0",
+ "raw-loader": "0.5.1",
+ "react": "16.2.0",
+ "react-dom": "16.2.0",
"react-helmet": "5.2.0",
"react-loadable": "5.3.1",
- "react-redux": "5.0.6",
- "react-router": "3.0.2",
+ "react-redux": "5.0.7",
+ "react-router-config": "1.0.0-beta.4",
+ "react-router-dom": "4.2.2",
"redux": "3.7.2",
- "redux-connect": "5.1.0",
- "redux-mock-store": "1.3.0",
+ "redux-mock-store": "1.5.1",
"redux-pack": "0.1.5",
"redux-thunk": "2.2.0",
+ "styled-components": "3.1.6",
+ "styled-normalize": "4.0.0",
"sw-precache-webpack-plugin": "0.11.4",
"webpack": "3.6.0",
- "webpack-bundle-analyzer": "2.9.1",
- "webpack-dashboard": "1.0.2",
+ "webpack-bundle-analyzer": "2.11.1",
+ "webpack-dashboard": "1.1.1",
"webpack-node-externals": "1.6.0"
},
"jest": {
diff --git a/postcss.config.js b/postcss.config.js
deleted file mode 100644
index 3b0281a..0000000
--- a/postcss.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
- plugins: {
- 'postcss-import': {},
- 'postcss-url': {},
- 'postcss-cssnext': {},
- },
-};
diff --git a/server/middlewares/renderMiddleware/renderMiddleware.js b/server/middlewares/renderMiddleware/renderMiddleware.js
deleted file mode 100644
index 4c3619d..0000000
--- a/server/middlewares/renderMiddleware/renderMiddleware.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React from 'react';
-import Helmet from 'react-helmet';
-import Loadable from 'react-loadable';
-import { renderToString } from 'react-dom/server';
-import { match } from 'react-router';
-import { Provider } from 'react-redux';
-import { ReduxAsyncConnect, loadOnServer } from 'redux-connect';
-import configureStore from '../../../client/store/configureStore';
-import routes from '../../../client/routes';
-import html from './html';
-
-const PWA_SSR = process.env.PWA_SSR === 'true';
-
-const serverRenderedChunks = async (req, res, renderProps) => {
- const route = renderProps.routes[renderProps.routes.length - 1];
- const store = configureStore();
- const chunks = [];
-
- res.set('Content-Type', 'text/html');
-
- const earlyChunk = html.earlyChunk(route);
- res.write(earlyChunk);
- res.flush();
-
- if (PWA_SSR) await loadOnServer({ ...renderProps, store });
-
- const lateChunk = html.lateChunk(
- PWA_SSR ? renderToString(
- chunks.push(name.replace(/.*\//, ''))}>
-
-
-
- ,
- ) : '',
- Helmet.renderStatic(),
- store.getState(),
- route,
- chunks,
- );
-
- res.end(lateChunk);
-};
-
-export default (req, res) => {
- match({
- routes,
- location: req.originalUrl,
- }, (error, redirectLocation, renderProps) => {
- if (error) {
- return res.status(500).send(error.message);
- } else if (redirectLocation) {
- return res.redirect(302, redirectLocation.pathname + redirectLocation.search);
- } else if (renderProps) {
- return serverRenderedChunks(req, res, renderProps);
- }
- return res.status(404).send('404: Not Found');
- });
-};
diff --git a/src/client.js b/src/client.js
new file mode 100644
index 0000000..c98a462
--- /dev/null
+++ b/src/client.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+import { BrowserRouter } from 'react-router-dom';
+import { renderRoutes } from 'react-router-config';
+import createStore from './store/createStore';
+import routes from './routes';
+
+const store = createStore(window.__INITIAL_STATE__);
+
+ReactDOM.render(
+
+
+ {renderRoutes(routes)}
+
+ ,
+ document.getElementById('root'),
+);
diff --git a/config/default.js b/src/config/development.js
similarity index 100%
rename from config/default.js
rename to src/config/development.js
diff --git a/src/config/index.js b/src/config/index.js
new file mode 100644
index 0000000..7bce68f
--- /dev/null
+++ b/src/config/index.js
@@ -0,0 +1,13 @@
+import local from './local';
+import development from './development';
+import staging from './staging';
+import production from './production';
+
+const config = {
+ local,
+ development,
+ staging,
+ production,
+};
+
+export default config[__PWA_ENV__];
diff --git a/src/config/local.js b/src/config/local.js
new file mode 100644
index 0000000..f81cd65
--- /dev/null
+++ b/src/config/local.js
@@ -0,0 +1,3 @@
+export default {
+ apiUrl: 'https://randomuser.me',
+};
diff --git a/config/production.js b/src/config/production.js
similarity index 100%
rename from config/production.js
rename to src/config/production.js
diff --git a/config/staging.js b/src/config/staging.js
similarity index 100%
rename from config/staging.js
rename to src/config/staging.js
diff --git a/src/core/Flex/Flex.js b/src/core/Flex/Flex.js
new file mode 100644
index 0000000..7dafb59
--- /dev/null
+++ b/src/core/Flex/Flex.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+
+const Flex = styled(({
+ alignContent,
+ alignItems,
+ alignSelf,
+ children,
+ component,
+ display,
+ flex,
+ flexBasis,
+ flexDirection,
+ flexGrow,
+ flexShrink,
+ flexWrap,
+ justifyContent,
+ order,
+ ...props
+}) => React.createElement(component, props, children))`
+ ${(p) => p.alignContent ? `align-content: ${p.alignContent};` : ''}
+ ${(p) => p.alignSelf ? `align-self: ${p.alignSelf};` : ''}
+ ${(p) => p.alignItems ? `align-items: ${p.alignItems};` : ''}
+ ${(p) => p.display ? `display: ${p.display};` : ''}
+ ${(p) => p.flex ? `flex: ${p.flex};` : ''}
+ ${(p) => p.flexBasis ? `flex-basis: ${p.flexBasis};` : ''}
+ ${(p) => p.flexDirection ? `flex-direction: ${p.flexDirection};` : ''}
+ ${(p) => p.flexGrow ? `flex-grow: ${p.flexGrow};` : ''}
+ ${(p) => p.flexShrink ? `flex-shrink: ${p.flexShrink};` : ''}
+ ${(p) => p.flexWrap ? `flex-wrap: ${p.flexWrap};` : ''}
+ ${(p) => p.justifyContent ? `justify-content: ${p.justifyContent};` : ''}
+ ${(p) => p.order ? `order: ${p.order};` : ''}
+`;
+
+Flex.propTypes = {
+ alignContent: PropTypes.oneOf(['center', 'flex-end', 'flex-start', 'space-around', 'space-between', 'stretch']),
+ alignItems: PropTypes.oneOf(['baseline', 'center', 'flex-end', 'flex-start', 'stretch']),
+ alignSelf: PropTypes.oneOf(['baseline', 'center', 'flex-end', 'flex-start', 'stretch']),
+ children: PropTypes.node,
+ display: PropTypes.oneOf(['flex', 'inline-flex']),
+ component: PropTypes.oneOf(['article', 'aside', 'div', 'figure', 'footer', 'header', 'main', 'nav', 'section']),
+ flex: PropTypes.string,
+ flexBasis: PropTypes.string,
+ flexDirection: PropTypes.oneOf(['column-reverse', 'column', 'row-reverse', 'row']),
+ flexGrow: PropTypes.number,
+ flexShrink: PropTypes.number,
+ flexWrap: PropTypes.oneOf(['nowrap', 'wrap-reverse', 'wrap']),
+ justifyContent: PropTypes.oneOf(['center', 'flex-end', 'flex-start', 'space-around', 'space-between']),
+ order: PropTypes.number,
+};
+
+Flex.defaultProps = {
+ component: 'div',
+ display: 'flex',
+};
+
+export default Flex;
diff --git a/src/core/Flex/index.js b/src/core/Flex/index.js
new file mode 100644
index 0000000..5ee4d4e
--- /dev/null
+++ b/src/core/Flex/index.js
@@ -0,0 +1 @@
+export default from './Flex';
diff --git a/client/components/NotFound/404.png b/src/core/NotFound/404.png
similarity index 100%
rename from client/components/NotFound/404.png
rename to src/core/NotFound/404.png
diff --git a/client/components/NotFound/NotFound.js b/src/core/NotFound/NotFound.js
similarity index 100%
rename from client/components/NotFound/NotFound.js
rename to src/core/NotFound/NotFound.js
diff --git a/src/core/NotFound/index.js b/src/core/NotFound/index.js
new file mode 100644
index 0000000..50068c5
--- /dev/null
+++ b/src/core/NotFound/index.js
@@ -0,0 +1 @@
+export default from './NotFound';
diff --git a/src/core/Spacer/Spacer.js b/src/core/Spacer/Spacer.js
new file mode 100644
index 0000000..bb81ab2
--- /dev/null
+++ b/src/core/Spacer/Spacer.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+
+const Spacer = styled(({
+ children,
+ ...props
+}) => React.cloneElement(children, props))`
+ ${(p) => p.margin || p.margin === 0 ? `margin: ${p.theme.px(p.margin)} !important;` : ''}
+ ${(p) => p.padding || p.padding === 0 ? `padding: ${p.theme.px(p.padding)} !important;` : ''}
+ ${(p) => p.maxWidth ? `max-width: ${p.maxWidth} !important;` : ''}
+ ${(p) => p.width ? `width: ${p.width} !important;` : ''}
+ ${(p) => p.minWidth ? `min-width: ${p.minWidth} !important;` : ''}
+ ${(p) => p.minHeight ? `min-height: ${p.minHeight} !important;` : ''}
+ ${(p) => p.height ? `height: ${p.height} !important;` : ''}
+ ${(p) => p.maxHeight ? `max-height: ${p.maxHeight} !important;` : ''}
+`;
+
+Spacer.propTypes = {
+ margin: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
+ ]),
+ padding: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
+ ]),
+ maxWidth: PropTypes.string,
+ width: PropTypes.string,
+ minWidth: PropTypes.string,
+ minHeight: PropTypes.string,
+ height: PropTypes.string,
+ maxHeight: PropTypes.string,
+};
+
+export default Spacer;
diff --git a/src/core/Spacer/index.js b/src/core/Spacer/index.js
new file mode 100644
index 0000000..3f66c97
--- /dev/null
+++ b/src/core/Spacer/index.js
@@ -0,0 +1 @@
+export default from './Spacer';
diff --git a/src/core/Text/Text.js b/src/core/Text/Text.js
new file mode 100644
index 0000000..52f5730
--- /dev/null
+++ b/src/core/Text/Text.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+
+const Text = styled(({
+ component,
+ children,
+ ...props
+}) => React.createElement(component, props, children))`
+ ${(p) => p.color ? `color: ${p.theme.color[p.color]};` : ''}
+ ${(p) => p.size ? `font-size: ${p.theme.fontSize[p.size]};` : ''}
+ ${(p) => p.weight ? `font-weight: ${p.theme.fontWeight[p.weight]};` : ''}
+ ${(p) => p.family ? `font-family: ${p.theme.fontFamily[p.family]};` : ''}
+ ${(p) => p.truncate ? `
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ ` : ''}
+`;
+
+Text.propTypes = {
+ component: PropTypes.node,
+ color: PropTypes.string,
+ size: PropTypes.string,
+ weight: PropTypes.string,
+ family: PropTypes.string,
+ truncate: PropTypes.bool,
+};
+
+Text.defaultProps = {
+ component: 'p',
+};
+
+export default Text;
diff --git a/src/core/Text/index.js b/src/core/Text/index.js
new file mode 100644
index 0000000..b960764
--- /dev/null
+++ b/src/core/Text/index.js
@@ -0,0 +1 @@
+export default from './Text';
diff --git a/src/core/Wrapper/Wrapper.js b/src/core/Wrapper/Wrapper.js
new file mode 100644
index 0000000..71b02ee
--- /dev/null
+++ b/src/core/Wrapper/Wrapper.js
@@ -0,0 +1,33 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { ThemeProvider } from 'styled-components';
+import Helmet from 'react-helmet';
+import { renderRoutes } from 'react-router-config';
+import theme from '../theme';
+import performanceMark from '../../utils/performanceMark';
+import './styles';
+
+class Wrapper extends Component {
+ componentDidMount() {
+ performanceMark('first-interaction');
+ }
+
+ render() {
+ const { route } = this.props;
+
+ return (
+
+
+
+ {renderRoutes(route.routes)}
+
+
+ );
+ }
+}
+
+Wrapper.propTypes = {
+ route: PropTypes.object.isRequired,
+};
+
+export default Wrapper;
diff --git a/src/core/Wrapper/index.js b/src/core/Wrapper/index.js
new file mode 100644
index 0000000..5b9663f
--- /dev/null
+++ b/src/core/Wrapper/index.js
@@ -0,0 +1 @@
+export default from './Wrapper';
diff --git a/src/core/Wrapper/styles.js b/src/core/Wrapper/styles.js
new file mode 100644
index 0000000..f99d700
--- /dev/null
+++ b/src/core/Wrapper/styles.js
@@ -0,0 +1,18 @@
+/* eslint-disable no-unused-expressions */
+import styledNormalize from 'styled-normalize';
+import { injectGlobal } from 'styled-components';
+import theme, { injectBaseStyles } from '../../core/theme';
+
+injectGlobal`${styledNormalize}`;
+injectBaseStyles(theme);
+injectGlobal`
+ html {
+ min-width: 320px;
+ }
+
+ *,
+ *::after,
+ *::before {
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+ }
+`;
diff --git a/src/core/theme/index.js b/src/core/theme/index.js
new file mode 100644
index 0000000..7aa23cd
--- /dev/null
+++ b/src/core/theme/index.js
@@ -0,0 +1,2 @@
+export default from './theme';
+export injectBaseStyles from './injectBaseStyles';
diff --git a/src/core/theme/injectBaseStyles.js b/src/core/theme/injectBaseStyles.js
new file mode 100644
index 0000000..e7dc015
--- /dev/null
+++ b/src/core/theme/injectBaseStyles.js
@@ -0,0 +1,46 @@
+import { injectGlobal } from 'styled-components';
+
+export default (theme) => injectGlobal`
+ html {
+ box-sizing: border-box;
+ }
+
+ body {
+ background: ${theme.color.greyLighter};
+ color: ${theme.color.greyDarker};
+ font-size: ${theme.fontSize.s};
+ font-weight: ${theme.fontWeight.normal};
+ font-family: ${theme.fontFamily.roboto}, system-ui, sans-serif;
+ }
+
+ *,
+ *::after,
+ *::before {
+ box-sizing: inherit;
+ }
+
+ p,
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ ul,
+ ol {
+ padding: 0;
+ margin: 0;
+ }
+
+ p {
+ line-height: 1.5;
+ }
+
+ ul {
+ list-style: none;
+ }
+
+ ol {
+ list-style: decimal inside;
+ }
+`;
diff --git a/src/core/theme/theme.js b/src/core/theme/theme.js
new file mode 100644
index 0000000..b27706a
--- /dev/null
+++ b/src/core/theme/theme.js
@@ -0,0 +1,90 @@
+const theme = {};
+
+theme.borderRadius = '2px';
+
+theme.boxShadow = [];
+theme.boxShadow[0] = 'none';
+theme.boxShadow[1] = '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(114, 113, 113, 0.08)';
+theme.boxShadow[2] = '0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 3px 6px 0 rgba(174, 174, 174, 0.16)';
+theme.boxShadow[3] = '0 10px 20px 0 rgba(0, 0, 0, 0.12), 0 6px 6px 0 rgba(0, 0, 0, 0.16)';
+theme.boxShadow[4] = '0 14px 28px 0 rgba(0, 0, 0, 0.12), 0 10px 10px 0 rgba(0, 0, 0, 0.16)';
+theme.boxShadow[5] = '0 19px 38px 0 rgba(0, 0, 0, 0.16), 0 15px 12px 0 rgba(0, 0, 0, 0.16)';
+
+theme.color = {};
+theme.color.greenLighter = '#e6f7ed';
+theme.color.greenLight = '#6ed396';
+theme.color.green = '#0eb550';
+theme.color.greenDark = '#00893d';
+theme.color.blueLighter = '#eeeffc';
+theme.color.blueLight = '#9aa4f2';
+theme.color.blue = '#5768e9';
+theme.color.blueDark = '#4451b6';
+theme.color.yellowLighter = '#fff0d6';
+theme.color.yellowLight = '#ffc866';
+theme.color.yellow = '#ffa400';
+theme.color.yellowDark = '#cc8300';
+theme.color.redLighter = '#ffeff1';
+theme.color.redLight = '#ffa3ab';
+theme.color.red = '#ff6673';
+theme.color.redDark = '#cc525c';
+theme.color.lagoonLighter = '#e5f1f3';
+theme.color.lagoonLight = '#66afb8';
+theme.color.lagoon = '#007989';
+theme.color.lagoonDark = '#004c56';
+theme.color.tealLighter = '#eaf3f1';
+theme.color.tealLight = '#a1d5ca';
+theme.color.teal = '#44ac95';
+theme.color.tealDark = '#2d907a';
+theme.color.chillLighter = '#f2f8f7';
+theme.color.chillLight = '#d4e8e4';
+theme.color.chill = '#bcdcd6';
+theme.color.chillDark = '#9ab5b0';
+theme.color.white = '#ffffff';
+theme.color.greyLighter = '#f1f1f1';
+theme.color.greyLight = '#dedede';
+theme.color.grey = '#aeaeae';
+theme.color.greyDark = '#727171';
+theme.color.greyDarker = '#4a4a4a';
+theme.color.black = '#000000';
+theme.color.translucent = 'rgba(0, 0, 0, 0.1)';
+theme.color.transparent = 'rgba(0, 0, 0, 0)';
+theme.color.primaryLighter = theme.color.greenLighter;
+theme.color.primaryLight = theme.color.greenLight;
+theme.color.primary = theme.color.green;
+theme.color.primaryDark = theme.color.greenDark;
+theme.color.accentLighter = theme.color.white;
+theme.color.accentLight = theme.color.white;
+theme.color.accent = theme.color.white;
+theme.color.accentDark = theme.color.white;
+
+theme.fontFamily = {};
+theme.fontFamily.roboto = 'Roboto';
+theme.fontFamily.averta = 'Averta';
+
+theme.fontSize = {};
+theme.fontSize.xxxxl = '32px';
+theme.fontSize.xxxl = '28px';
+theme.fontSize.xxl = '24px';
+theme.fontSize.xl = '20px';
+theme.fontSize.l = '18px';
+theme.fontSize.m = '16px';
+theme.fontSize.s = '14px';
+theme.fontSize.xs = '12px';
+theme.fontSize.xxs = '10px';
+
+theme.fontWeight = {};
+theme.fontWeight.regular = 400;
+theme.fontWeight.medium = 500;
+theme.fontWeight.semibold = 600;
+theme.fontWeight.bold = 700;
+
+theme.pxScale = 8;
+
+theme.px = (value) => {
+ const values = [].concat(value);
+ return values
+ .map((v) => typeof v === 'string' ? v : `${v * theme.pxScale}px`)
+ .join(' ');
+};
+
+export default theme;
diff --git a/src/home/HomePage/HomePage.js b/src/home/HomePage/HomePage.js
new file mode 100644
index 0000000..e73b0a9
--- /dev/null
+++ b/src/home/HomePage/HomePage.js
@@ -0,0 +1,50 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import isEmpty from 'lodash/isEmpty';
+import { connect } from 'react-redux';
+import { bindActionCreators, compose } from 'redux';
+import * as userActionCreators from '../../user/userActionCreators';
+import UsersList from '../../user/UserList';
+import Spacer from '../../core/Spacer';
+
+class HomePage extends React.Component {
+ static componentWillServerRender = ({ store }) =>
+ isEmpty(store.getState().$user.ids)
+ ? store.dispatch(userActionCreators.getAll()) : null
+
+ componentDidMount() {
+ const { $user, userActions } = this.props;
+ if (isEmpty($user.ids)) {
+ userActions.getAll();
+ }
+ }
+
+ render() {
+ return (
+
+
+
PWA
+
An opinionated progressive web app boilerplate
+
+
+
+ );
+ }
+}
+
+HomePage.propTypes = {
+ $user: PropTypes.object.isRequired,
+ userActions: PropTypes.object.isRequired,
+};
+
+const mapStateToProps = (state) => ({
+ $user: state.$user,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ userActions: bindActionCreators(userActionCreators, dispatch),
+});
+
+export default compose(
+ connect(mapStateToProps, mapDispatchToProps),
+)(HomePage);
diff --git a/src/home/HomePage/index.js b/src/home/HomePage/index.js
new file mode 100644
index 0000000..5732525
--- /dev/null
+++ b/src/home/HomePage/index.js
@@ -0,0 +1,6 @@
+import Loadable from 'react-loadable';
+
+export default Loadable({
+ loader: () => import('./HomePage' /* webpackChunkName: 'HomePage' */),
+ loading: () => null,
+});
diff --git a/client/manifest.json b/src/manifest.json
similarity index 100%
rename from client/manifest.json
rename to src/manifest.json
diff --git a/client/offline/offline.html b/src/offline/offline.html
similarity index 100%
rename from client/offline/offline.html
rename to src/offline/offline.html
diff --git a/client/offline/offline.js b/src/offline/offline.js
similarity index 100%
rename from client/offline/offline.js
rename to src/offline/offline.js
diff --git a/src/render/execComponentWillServerRender.js b/src/render/execComponentWillServerRender.js
new file mode 100644
index 0000000..23383b6
--- /dev/null
+++ b/src/render/execComponentWillServerRender.js
@@ -0,0 +1,24 @@
+export default (branches, ctx) => {
+ const promises = branches.map(async (branch) => {
+ let { component } = branch.route;
+ const context = {
+ match: branch.match,
+ ...ctx,
+ };
+
+ if (component.preload) {
+ const loadedComponent = await component.preload();
+ component = loadedComponent.default;
+ }
+
+ if (component.componentWillServerRender) {
+ return Promise
+ .resolve(component.componentWillServerRender(context))
+ .catch(() => {});
+ }
+
+ return Promise.resolve();
+ });
+
+ return Promise.all(promises);
+};
diff --git a/server/middlewares/renderMiddleware/fragments.js b/src/render/fragments.js
similarity index 64%
rename from server/middlewares/renderMiddleware/fragments.js
rename to src/render/fragments.js
index 3b571a0..a1a208b 100644
--- a/server/middlewares/renderMiddleware/fragments.js
+++ b/src/render/fragments.js
@@ -1,17 +1,7 @@
/* eslint-disable max-len, import/no-unresolved */
-import fs from 'fs';
import assetsManifest from '../../build/client/assetsManifest.json';
-export const assets = Object.keys(assetsManifest)
- .reduce((obj, entry) => ({
- ...obj,
- [entry]: {
- ...assetsManifest[entry],
- styles: assetsManifest[entry].css
- ? fs.readFileSync(`build/client/css/${assetsManifest[entry].css.split('/').pop()}`, 'utf8')
- : undefined,
- },
- }), {});
+export const assets = assetsManifest;
export const scripts = {
serviceWorker: `
diff --git a/server/middlewares/renderMiddleware/html.js b/src/render/html.js
similarity index 85%
rename from server/middlewares/renderMiddleware/html.js
rename to src/render/html.js
index c4f99c1..4f7c6aa 100644
--- a/server/middlewares/renderMiddleware/html.js
+++ b/src/render/html.js
@@ -18,12 +18,9 @@ export default {
${!assets[route.name] ? '' : ``}`;
},
- lateChunk(app, head, initialState, route, chunks) {
+ lateChunk(app, styles, head, initialState, route, chunks) {
return `
- ${__LOCAL__ ? '' : ``}
- ${__LOCAL__ ? '' : ``}
- ${__LOCAL__ || !assets[route.name] ? '' : ``}
- ${__LOCAL__ ? '' : chunks.reduce((s, name) => `${s}`, '')}
+ ${__LOCAL__ ? '' : styles}
${__LOCAL__ ? '' : ''}
diff --git a/src/render/renderExpressMiddleware.js b/src/render/renderExpressMiddleware.js
new file mode 100644
index 0000000..7d56277
--- /dev/null
+++ b/src/render/renderExpressMiddleware.js
@@ -0,0 +1,55 @@
+import React from 'react';
+import Helmet from 'react-helmet';
+import Loadable from 'react-loadable';
+import { matchRoutes, renderRoutes } from 'react-router-config';
+import { renderToString } from 'react-dom/server';
+import { StaticRouter } from 'react-router-dom';
+import { Provider } from 'react-redux';
+import { ServerStyleSheet } from 'styled-components';
+import createStore from '../store/createStore';
+import routes from '../routes';
+import execComponentWillServerRender from './execComponentWillServerRender';
+import html from './html';
+
+const PWA_SSR = process.env.PWA_SSR === 'true';
+
+export default async (req, res) => {
+ const location = req.originalUrl || req.url;
+ const branches = matchRoutes(routes, location);
+ const branch = branches[branches.length - 1];
+ const sheet = new ServerStyleSheet();
+ const store = createStore();
+ const context = {};
+ const chunks = [];
+
+ res.set('Content-Type', 'text/html');
+
+ const earlyChunk = html.earlyChunk(branch.route);
+ res.write(earlyChunk);
+ res.flush();
+
+ if (PWA_SSR) {
+ await execComponentWillServerRender(branches, { req, res, store });
+ }
+
+ const app = PWA_SSR ? renderToString(sheet.collectStyles(
+ chunks.push(name.replace(/.*\//, ''))}>
+
+
+ {renderRoutes(routes)}
+
+
+ ,
+ )) : '';
+
+ const lateChunk = html.lateChunk(
+ app,
+ sheet.getStyleTags(),
+ Helmet.renderStatic(),
+ store.getState(),
+ branch.route,
+ chunks,
+ );
+
+ res.end(lateChunk);
+};
diff --git a/src/routes.js b/src/routes.js
new file mode 100644
index 0000000..d03f81b
--- /dev/null
+++ b/src/routes.js
@@ -0,0 +1,16 @@
+import Wrapper from './core/Wrapper';
+import NotFound from './core/NotFound';
+import HomePage from './home/HomePage';
+
+export default [{
+ component: Wrapper,
+ routes: [{
+ path: '/',
+ exact: true,
+ name: 'HomePage',
+ component: HomePage,
+ }, {
+ name: 'NotFound',
+ component: NotFound,
+ }],
+}];
diff --git a/server/index.js b/src/server.js
similarity index 73%
rename from server/index.js
rename to src/server.js
index 02bd0cd..d730b81 100644
--- a/server/index.js
+++ b/src/server.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
import 'babel-polyfill';
import express from 'express';
import helmet from 'helmet';
@@ -5,22 +6,22 @@ import compression from 'compression';
import morgan from 'morgan';
import slashes from 'connect-slashes';
import Loadable from 'react-loadable';
-import renderMiddleware from './middlewares/renderMiddleware/renderMiddleware';
+import renderExpressMiddleware from './render/renderExpressMiddleware';
+
+const { PORT } = process.env;
const app = express();
app.use(helmet({ dnsPrefetchControl: false }));
app.use(compression());
app.use(morgan(__LOCAL__ ? 'dev' : 'combined'));
-app.use('/build/client', express.static('build/client'));
app.use('/serviceWorker.js', express.static('build/client/serviceWorker.js'));
app.use('/manifest.json', express.static('build/client/manifest.json'));
+app.use('/build/client', express.static('build/client'));
app.use(slashes(true));
-app.use(renderMiddleware);
+app.use(renderExpressMiddleware);
-const PORT = process.env.PORT || 8000;
Loadable.preloadAll().then(() => {
app.listen(PORT, () => {
- // eslint-disable-next-line
- console.info(`pwa is running as ${__PWA_ENV__} on port ${PORT}`);
+ console.log(`pwa is running as ${__PWA_ENV__} on port ${PORT}`);
});
});
diff --git a/src/store/createStore.js b/src/store/createStore.js
new file mode 100644
index 0000000..8210b69
--- /dev/null
+++ b/src/store/createStore.js
@@ -0,0 +1,23 @@
+import { createStore, compose, applyMiddleware } from 'redux';
+import reduxThunk from 'redux-thunk';
+import { middleware as reduxPack } from 'redux-pack';
+import { makeRequest } from '../utils/request';
+import config from '../config';
+import rootReducer from './rootReducer';
+
+const middlewares = [
+ reduxThunk.withExtraArgument({ request: makeRequest(config.apiUrl) }),
+ reduxPack,
+].filter(Boolean);
+
+const storeEnhancers = [
+ applyMiddleware(...middlewares),
+ __BROWSER__ && __LOCAL__ && window.devToolsExtension && window.devToolsExtension(),
+].filter(Boolean);
+
+export default (initialState) =>
+ createStore(
+ rootReducer,
+ initialState,
+ compose(...storeEnhancers),
+ );
diff --git a/src/store/rootReducer.js b/src/store/rootReducer.js
new file mode 100644
index 0000000..ad25a23
--- /dev/null
+++ b/src/store/rootReducer.js
@@ -0,0 +1,6 @@
+import { combineReducers } from 'redux';
+import * as userReducer from '../user/userReducer';
+
+export default combineReducers({
+ $user: userReducer.reducer,
+});
diff --git a/src/user/UserList/UserList.js b/src/user/UserList/UserList.js
new file mode 100644
index 0000000..955d15e
--- /dev/null
+++ b/src/user/UserList/UserList.js
@@ -0,0 +1,68 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import Text from '../../core/Text';
+import Flex from '../../core/Flex';
+import Spacer from '../../core/Spacer';
+
+const Image = styled.img`
+ cursor: pointer;
+ transition: all 0.2s ease-in-out;
+ border-radius: 50%;
+ border: 3px solid transparent;
+ border-color: ${(p) => p.isActive ? p.theme.color.primary : ''};
+`;
+
+class UsersList extends Component {
+ state = {
+ active: 1,
+ };
+
+ showUser = (index) => () => {
+ this.setState({ active: index });
+ }
+
+ render() {
+ const { users } = this.props;
+ const { active } = this.state;
+
+ return users.length ? (
+
+
+
+ {
+ users.map(({ name, picture }, i) => (
+
+
+
+
+ {name.first}
+
+ ))
+ }
+
+ {users[active].location.street}
+
+
+ ) : null;
+ }
+}
+
+UsersList.propTypes = {
+ users: PropTypes.array.isRequired,
+};
+
+const mapStateToProps = (state) => ({
+ users: Object.values(state.$user.byId),
+});
+
+export default compose(
+ connect(mapStateToProps),
+)(UsersList);
diff --git a/src/user/UserList/index.js b/src/user/UserList/index.js
new file mode 100644
index 0000000..34210e5
--- /dev/null
+++ b/src/user/UserList/index.js
@@ -0,0 +1,7 @@
+import React from 'react';
+import Loadable from 'react-loadable';
+
+export default Loadable({
+ loader: () => import('./UserList' /* webpackChunkName: 'UserList' */),
+ loading: () => Loading users...
,
+});
diff --git a/src/user/userActionCreators.js b/src/user/userActionCreators.js
new file mode 100644
index 0000000..36b24b0
--- /dev/null
+++ b/src/user/userActionCreators.js
@@ -0,0 +1,19 @@
+import * as userActionTypes from './userActionTypes';
+
+export const getAll = () => (dispatch, getState, { request }) =>
+ dispatch({
+ type: userActionTypes.GET_ALL,
+ promise: request.get('/api', {
+ results: 3,
+ inc: 'name,location,picture',
+ }),
+ });
+
+export const getOne = () => (dispatch, getState, { request }) =>
+ dispatch({
+ type: userActionTypes.GET_ONE,
+ promise: request.get('/api', {
+ results: 1,
+ inc: 'name,location,picture',
+ }),
+ });
diff --git a/client/services/user/userTypes.js b/src/user/userActionTypes.js
similarity index 100%
rename from client/services/user/userTypes.js
rename to src/user/userActionTypes.js
diff --git a/src/user/userHelpers.js b/src/user/userHelpers.js
new file mode 100644
index 0000000..76e37c9
--- /dev/null
+++ b/src/user/userHelpers.js
@@ -0,0 +1,17 @@
+export const makeUser = (props) => {
+ const user = {};
+
+ user.name = props.name;
+ user.picture = props.picture;
+ user.location = props.location;
+
+ return user;
+};
+
+export const normalize = (users) => ({
+ byId: users.reduce((obj, user, index) => ({
+ ...obj,
+ [index]: makeUser(user),
+ }), {}),
+ ids: users.map((user, index) => index),
+});
diff --git a/src/user/userReducer.js b/src/user/userReducer.js
new file mode 100644
index 0000000..0c794a6
--- /dev/null
+++ b/src/user/userReducer.js
@@ -0,0 +1,29 @@
+import { handle } from 'redux-pack';
+import * as userActionTypes from './userActionTypes';
+import * as userHelpers from './userHelpers';
+
+export const initialState = {
+ byId: {},
+ ids: [],
+};
+
+export const reducer = (state = initialState, action) => {
+ const { type, payload } = action;
+
+ switch (type) {
+ case userActionTypes.GET_ALL:
+ return handle(state, action, {
+ success: (s) => ({
+ ...s,
+ ...userHelpers.normalize(payload.results),
+ }),
+ failure: (s) => ({
+ ...s,
+ error: payload.error,
+ }),
+ });
+
+ default:
+ return state;
+ }
+};
diff --git a/client/services/user/userReducer.test.js b/src/user/userReducer.test.js
similarity index 62%
rename from client/services/user/userReducer.test.js
rename to src/user/userReducer.test.js
index 85de038..7964926 100644
--- a/client/services/user/userReducer.test.js
+++ b/src/user/userReducer.test.js
@@ -1,10 +1,10 @@
import nock from 'nock';
import { LIFECYCLE } from 'redux-pack';
-import * as testHelpers from '../../utils/testHelpers';
-import * as userTypes from './userTypes';
+import * as testHelpers from '../utils/testHelpers';
+import * as userActionTypes from './userActionTypes';
import * as userActionCreators from './userActionCreators';
-import * as userModel from './userModel';
-import reducer, { initialState } from './userReducer';
+import * as userHelpers from './userHelpers';
+import * as userReducer from './userReducer';
describe('user/userActionCreators', () => {
const store = testHelpers.mockStore();
@@ -14,7 +14,7 @@ describe('user/userActionCreators', () => {
store.clearActions();
});
- it(`dispatches ${userTypes.GET_ALL}`, async () => {
+ it(`dispatches ${userActionTypes.GET_ALL}`, async () => {
const apiResult = { results: [{}, {}, {}] };
nock('https://randomuser.me')
@@ -24,10 +24,10 @@ describe('user/userActionCreators', () => {
const expectedActions = [
testHelpers.makeReduxPackAction(LIFECYCLE.START, {
- type: userTypes.GET_ALL,
+ type: userActionTypes.GET_ALL,
}),
testHelpers.makeReduxPackAction(LIFECYCLE.SUCCESS, {
- type: userTypes.GET_ALL,
+ type: userActionTypes.GET_ALL,
payload: apiResult,
meta: { startPayload: undefined },
}),
@@ -42,35 +42,35 @@ describe('user/userActionCreators', () => {
describe('user/userReducer', () => {
it('returns intialState', () => {
- const finalState = reducer(undefined, {});
- const expectedState = initialState;
+ const finalState = userReducer.reducer(undefined, {});
+ const expectedState = userReducer.initialState;
expect(finalState).toEqual(expectedState);
});
- it(`sets user on ${userTypes.GET_ALL}:success`, () => {
+ it(`sets user on ${userActionTypes.GET_ALL}:success`, () => {
const apiResult = [{}, {}, {}];
- const finalState = reducer(
- initialState,
+ const finalState = userReducer.reducer(
+ userReducer.initialState,
testHelpers.makeReduxPackAction(LIFECYCLE.SUCCESS, {
- type: userTypes.GET_ALL,
+ type: userActionTypes.GET_ALL,
payload: { results: apiResult },
}),
);
const expectedState = {
- ...initialState,
- ...userModel.normalize(apiResult),
+ ...userReducer.initialState,
+ ...userHelpers.normalize(apiResult),
};
expect(finalState).toEqual(expectedState);
});
- it(`sets error on ${userTypes.GET_ALL}:failure`, () => {
- const finalState = reducer(
- initialState,
+ it(`sets error on ${userActionTypes.GET_ALL}:failure`, () => {
+ const finalState = userReducer.reducer(
+ userReducer.initialState,
testHelpers.makeReduxPackAction(LIFECYCLE.FAILURE, {
- type: userTypes.GET_ALL,
+ type: userActionTypes.GET_ALL,
payload: {
error: {},
},
@@ -78,7 +78,7 @@ describe('user/userReducer', () => {
);
const expectedState = {
- ...initialState,
+ ...userReducer.initialState,
error: {},
};
diff --git a/client/utils/performanceMark.js b/src/utils/performanceMark.js
similarity index 100%
rename from client/utils/performanceMark.js
rename to src/utils/performanceMark.js
diff --git a/src/utils/request.js b/src/utils/request.js
new file mode 100644
index 0000000..02ede95
--- /dev/null
+++ b/src/utils/request.js
@@ -0,0 +1,51 @@
+/* eslint-disable no-param-reassign */
+import fetch from 'isomorphic-fetch';
+import queryString from 'query-string';
+
+const request = async (url, data, opts) => {
+ let fullUrl = url;
+
+ if (opts.method === 'GET' && data) {
+ const query = queryString.stringify(data, { arrayFormat: 'index' });
+ fullUrl = `${url}?${query}`;
+ }
+
+ const options = {
+ method: opts.method,
+ body: opts.method !== 'GET' ? JSON.stringify(data) : null,
+ credentials: opts.credentials || 'same-origin',
+ headers: opts.headers || {
+ 'Content-Type': 'application/json',
+ },
+ };
+
+ const response = await fetch(fullUrl, options);
+ const json = await response.json();
+ return response.ok ? json : Promise.reject(json);
+};
+
+export const makeRequest = (baseUrl, options = {}) => ({
+ raw: request,
+
+ get(url, data, opts = options) {
+ opts.method = 'GET';
+ return request(`${baseUrl}${url}`, data, opts);
+ },
+
+ post(url, data, opts = options) {
+ opts.method = 'POST';
+ return request(`${baseUrl}${url}`, data, opts);
+ },
+
+ put(url, data, opts = options) {
+ opts.method = 'PUT';
+ return request(`${baseUrl}${url}`, data, opts);
+ },
+
+ delete(url, data, opts = options) {
+ opts.method = 'DELETE';
+ return request(url, data, opts);
+ },
+});
+
+export default request;
diff --git a/client/utils/testHelpers.js b/src/utils/testHelpers.js
similarity index 78%
rename from client/utils/testHelpers.js
rename to src/utils/testHelpers.js
index 4dfffa0..9b954b8 100644
--- a/client/utils/testHelpers.js
+++ b/src/utils/testHelpers.js
@@ -1,7 +1,8 @@
import configureMockStore from 'redux-mock-store';
import reduxThunk from 'redux-thunk';
import { middleware as reduxPack, KEY } from 'redux-pack';
-import api from './api';
+import config from '../config';
+import { makeRequest } from './request';
export const makeReduxPackAction = (lifecycle, { type, payload, meta = {} }) => ({
type,
@@ -21,6 +22,6 @@ export const removeReduxPackTransaction = (action) => ({
});
export const mockStore = configureMockStore([
- reduxThunk.withExtraArgument({ api }),
+ reduxThunk.withExtraArgument({ request: makeRequest(config.apiUrl) }),
reduxPack,
]);
diff --git a/client/vendor/modules/modules.js b/src/vendor.js
similarity index 75%
rename from client/vendor/modules/modules.js
rename to src/vendor.js
index 0e29832..e2b46fe 100644
--- a/client/vendor/modules/modules.js
+++ b/src/vendor.js
@@ -1,5 +1,4 @@
import 'babel-polyfill';
-import 'classnames';
import 'isomorphic-fetch';
import 'lodash/isEmpty';
import 'moment';
@@ -8,8 +7,9 @@ import 'preact-compat';
import 'query-string';
import 'react-helmet';
import 'react-redux';
-import 'react-router';
+import 'react-router-config';
+import 'react-router-dom';
import 'redux';
-import 'redux-connect';
import 'redux-pack';
import 'redux-thunk';
+import 'styled-components';
diff --git a/webpack.client.js b/webpack.client.js
index a8c851e..d76e3f0 100644
--- a/webpack.client.js
+++ b/webpack.client.js
@@ -4,7 +4,6 @@ const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const AssetsPlugin = require('assets-webpack-plugin');
-const ExtractCssChunks = require('extract-css-chunks-webpack-plugin');
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const DashboardPlugin = require('webpack-dashboard/plugin');
@@ -17,8 +16,8 @@ module.exports = {
cache: !isProd,
entry: {
- main: './client/index.js',
- vendor: ['./client/vendor/modules/modules.js', './client/vendor/styles/styles.css'],
+ main: './src/client.js',
+ vendor: './src/vendor.js',
},
output: {
@@ -32,18 +31,19 @@ module.exports = {
alias: {
react: 'preact-compat',
'react-dom': 'preact-compat',
+ 'react-dom/server': 'preact-compat/server',
},
},
module: {
rules: isProd ? [
{ test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] },
- { test: /\.css$/, loader: ExtractCssChunks.extract({ use: [{ loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader'] }) },
+ { test: /\.css$/, use: ['raw-loader'] },
{ test: /\.(gif|png|jpe?g|svg|ico)$/i, use: [{ loader: 'file-loader', options: { name: 'images/[name].[hash:8].[ext]' } }] },
{ test: /\.(woff(2)?|ttf|otf|eot)(\?[a-z0-9=&.]+)?$/, use: [{ loader: 'url-loader', options: { limit: 1000, name: 'fonts/[name].[hash:8].[ext]' } }] },
] : [
{ test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] },
- { test: /\.css$/, use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader'] },
+ { test: /\.css$/, use: ['raw-loader'] },
{ test: /\.(gif|png|jpe?g|svg|ico)$/i, use: [{ loader: 'file-loader', options: { name: 'images/[name].[ext]' } }] },
{ test: /\.(woff(2)?|ttf|otf|eot)(\?[a-z0-9=&.]+)?$/, use: [{ loader: 'url-loader', options: { limit: 1000, name: 'fonts/[name].[ext]' } }] },
],
@@ -94,10 +94,9 @@ module.exports = {
screw_ie8: true,
},
}),
- new ExtractCssChunks('css/[name].[contenthash:8].css'),
new CopyWebpackPlugin([
- { from: './client/manifest.json' },
- { from: './client/offline', to: 'offline/[name].00000001.[ext]' },
+ { from: './src/manifest.json' },
+ { from: './src/offline', to: 'offline/[name].00000001.[ext]' },
], { copyUnmodified: true }),
new SWPrecacheWebpackPlugin({
cacheId: 'pwa',
diff --git a/webpack.server.js b/webpack.server.js
index 4f36c23..c066694 100644
--- a/webpack.server.js
+++ b/webpack.server.js
@@ -9,12 +9,12 @@ const __PWA_PUBLIC_PATH__ = process.env.PWA_PUBLIC_PATH;
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
- entry: './server/index.js',
+ entry: './src/server.js',
target: 'node',
externals: [
- nodeExternals({ whitelist: [/\.css$/] }),
+ nodeExternals(),
/assetsManifest.json/,
],
@@ -30,18 +30,19 @@ module.exports = {
alias: {
react: 'preact-compat',
'react-dom': 'preact-compat',
+ 'react-dom/server': 'preact-compat/server',
},
},
module: {
rules: isProd ? [
{ test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] },
- { test: /\.css$/, use: ['css-loader/locals', 'postcss-loader'] },
+ { test: /\.css$/, use: ['raw-loader'] },
{ test: /\.(gif|png|jpe?g|svg|ico)$/i, use: [{ loader: 'file-loader', options: { name: 'images/[name].[hash:8].[ext]' } }] },
{ test: /\.(woff(2)?|ttf|otf|eot)(\?[a-z0-9=&.]+)?$/, use: [{ loader: 'url-loader', options: { limit: 1000, name: 'fonts/[name].[hash:8].[ext]' } }] },
] : [
{ test: /\.js$/, exclude: /node_modules/, use: ['babel-loader'] },
- { test: /\.css$/, use: ['css-loader/locals', 'postcss-loader'] },
+ { test: /\.css$/, use: ['raw-loader'] },
{ test: /\.(gif|png|jpe?g|svg|ico)$/i, use: [{ loader: 'file-loader', options: { name: 'images/[name].[ext]' } }] },
{ test: /\.(woff(2)?|ttf|otf|eot)(\?[a-z0-9=&.]+)?$/, use: [{ loader: 'url-loader', options: { limit: 1000, name: 'fonts/[name].[ext]' } }] },
],