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} -
- {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} + + {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]' } }] }, ],