ff-stream-web

git clone git://archive.git.mtrnord.blog/MTRNord/ff-stream-web.git
Log | Files | Refs | README | LICENSE

commit 448121f2f275e8842e0d40874098b94ac928b79e
parent 886124e4cda0f35b4ce6eaf1372648de896fdbc6
Author: Konstantin Tarkus <koistya@gmail.com>
Date:   Fri, 23 Sep 2016 10:45:57 +0300

Introduce the new App and Layout components. Upgrade `history` module. (#869)

* Introduce the new App and Layout components. Upgrade `history` module.

* Some clean up and farther improvements

Diffstat:
MCHANGELOG.md | 3+++
Mdocs/getting-started.md | 2+-
Mdocs/testing-your-application.md | 13++++++++-----
Mpackage.json | 37+++++++++++++++++++------------------
Msrc/client.js | 75+++++++++++++++++++++++++++++++++++++++++++--------------------------------
Asrc/components/App.js | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/components/App/App.js | 66------------------------------------------------------------------
Dsrc/components/App/App.test.js | 30------------------------------
Dsrc/components/App/package.json | 6------
Msrc/components/Html.js | 9+++++++++
Rsrc/components/App/App.css -> src/components/Layout/Layout.css | 0
Asrc/components/Layout/Layout.js | 32++++++++++++++++++++++++++++++++
Asrc/components/Layout/Layout.test.js | 33+++++++++++++++++++++++++++++++++
Asrc/components/Layout/package.json | 6++++++
Msrc/components/Link/Link.js | 25++++++++-----------------
Dsrc/core/history.js | 16----------------
Msrc/routes/contact/Contact.js | 17+++++++++++------
Msrc/routes/content/Content.js | 13++++++++-----
Msrc/routes/error/ErrorPage.js | 17+++++++++++------
Msrc/routes/home/Home.js | 33++++++++++++++++++---------------
Msrc/routes/index.js | 39++++++++++++++-------------------------
Msrc/routes/login/Login.js | 189+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/routes/notFound/NotFound.js | 17+++++++++++------
Msrc/routes/register/Register.js | 13++++++++-----
Msrc/server.js | 27+++++++++++++++++++++------
25 files changed, 418 insertions(+), 357 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### [Unreleased][unreleased] +- Split the `App` component into `App` setting context variables and `Layout` setting general look and feel of the app (BREAKING CHANGE) +- Upgrade `history` npm module to v4.x, update `Link` component (BREAKING CHANGE) +- Remove `core/createHistory.js` in favor of initializing a new history instance inside `server.js` and `client.js` (BREAKING CHANGE) - Remove Jade dependency in favor of React-based templates: `src/views/index.jade => src/components/Html` (BREAKING CHANGE) [#711](https://github.com/kriasoft/react-starter-kit/pull/711) - Update `isomorphic-style-loader` to `v1.0.0`, it adds comparability with ES2015+ decorators. diff --git a/docs/getting-started.md b/docs/getting-started.md @@ -129,7 +129,7 @@ $ npm run test:watch # Launch unit test runner and start watching for changes ``` By default, [Mocha](https://mochajs.org/) test runner is looking for test files -matching the `src/**/*.test.js` pattern. Take a look at `src/components/App/App.test.js` +matching the `src/**/*.test.js` pattern. Take a look at `src/components/Layout/Layout.test.js` as an example. To deploy the app, run: diff --git a/docs/testing-your-application.md b/docs/testing-your-application.md @@ -34,21 +34,24 @@ npm test ### Basic example To help you on your way RSK comes with the following -[basic test case](https://github.com/kriasoft/react-starter-kit/blob/master/src/components/App/App.test.js) +[basic test case](https://github.com/kriasoft/react-starter-kit/blob/master/src/components/Layout/Layout.test.js) you can use as a starting point: ```js import React from 'react'; import { expect } from 'chai'; import { shallow } from 'enzyme'; -import App from './App'; +import App from '../App'; +import Layout from './Layout'; -describe('App', () => { +describe('Layout', () => { it('renders children correctly', () => { const wrapper = shallow( <App context={{ insertCss: () => {} }}> - <div className="child" /> + <Layout> + <div className="child" /> + </Layout> </App> ); @@ -58,7 +61,7 @@ describe('App', () => { }); ``` -### React-intl example +### React-intl exampleß React-intl users MUST render/wrap components inside an IntlProvider like the example below: diff --git a/package.json b/package.json @@ -12,7 +12,7 @@ "classnames": "2.2.5", "cookie-parser": "1.4.3", "core-js": "2.4.1", - "eventemitter3": "1.2.0", + "eventemitter3": "2.0.0", "express": "4.14.0", "express-graphql": "0.5.4", "express-jwt": "5.0.0", @@ -21,18 +21,19 @@ "front-matter": "2.1.0", "graphiql": "0.7.8", "graphql": "0.7.0", - "history": "3.0.0", + "history": "4.2.0", "isomorphic-style-loader": "1.0.0", "jsonwebtoken": "7.1.9", - "markdown-it": "7.0.1", - "node-fetch": "1.6.0", + "markdown-it": "8.0.0", + "node-fetch": "1.6.1", "normalize.css": "4.2.0", "passport": "0.3.2", "passport-facebook": "2.1.1", "pretty-error": "2.0.0", - "react": "15.3.1", - "react-dom": "15.3.1", - "sequelize": "3.24.2", + "query-string": "4.2.3", + "react": "15.3.2", + "react-dom": "15.3.2", + "sequelize": "3.24.3", "source-map-support": "0.4.2", "sqlite3": "3.1.4", "universal-router": "1.2.2", @@ -40,13 +41,13 @@ }, "devDependencies": { "assets-webpack-plugin": "^3.4.0", - "autoprefixer": "^6.4.0", + "autoprefixer": "^6.4.1", "babel-cli": "^6.14.0", "babel-core": "^6.14.0", "babel-eslint": "^6.1.2", "babel-loader": "^6.2.5", "babel-plugin-react-transform": "^2.0.2", - "babel-plugin-rewire": "^1.0.0-rc-7", + "babel-plugin-rewire": "^1.0.0", "babel-plugin-transform-react-constant-elements": "^6.9.1", "babel-plugin-transform-react-inline-elements": "^6.8.0", "babel-plugin-transform-react-remove-prop-types": "^0.2.9", @@ -58,22 +59,22 @@ "babel-register": "^6.14.0", "babel-template": "^6.15.0", "babel-types": "^6.15.0", - "browser-sync": "^2.14.3", + "browser-sync": "^2.16.0", "chai": "^3.5.0", "css-loader": "^0.25.0", "del": "^2.2.2", "enzyme": "^2.4.1", - "eslint": "^3.4.0", + "eslint": "^3.5.0", "eslint-config-airbnb": "^11.1.0", "eslint-loader": "^1.5.0", - "eslint-plugin-import": "^1.14.0", + "eslint-plugin-import": "^1.15.0", "eslint-plugin-jsx-a11y": "^2.2.2", - "eslint-plugin-react": "^6.2.1", + "eslint-plugin-react": "^6.3.0", "extend": "^3.0.0", "file-loader": "^0.9.0", "gaze": "^1.1.1", "git-repository": "^0.1.4", - "glob": "^7.0.6", + "glob": "^7.1.0", "json-loader": "^0.5.4", "mkdirp": "^0.5.1", "mocha": "^3.0.2", @@ -95,13 +96,13 @@ "postcss-selector-matches": "^2.0.5", "postcss-selector-not": "^2.0.0", "raw-loader": "^0.5.1", - "react-addons-test-utils": "15.3.1", + "react-addons-test-utils": "15.3.2", "react-transform-catch-errors": "^1.0.2", "react-transform-hmr": "^1.0.4", - "redbox-react": "^1.3.0", + "redbox-react": "^1.3.1", "sinon": "^2.0.0-pre.2", - "stylelint": "^7.2.0", - "stylelint-config-standard": "^13.0.0", + "stylelint": "^7.3.1", + "stylelint-config-standard": "^13.0.2", "url-loader": "^0.5.7", "webpack": "^1.13.2", "webpack-hot-middleware": "^2.12.2", diff --git a/src/client.js b/src/client.js @@ -8,11 +8,13 @@ */ import 'babel-polyfill'; +import React from 'react'; import ReactDOM from 'react-dom'; import FastClick from 'fastclick'; import UniversalRouter from 'universal-router'; -import { readState, saveState } from 'history/lib/DOMStateStorage'; -import history from './core/history'; +import queryString from 'query-string'; +import createBrowserHistory from 'history/createBrowserHistory'; +import App from './components/App'; import { addEventListener, removeEventListener, @@ -20,12 +22,18 @@ import { windowScrollY, } from './core/DOMUtils'; +// Global (context) variables that can be easily accessed from any React component +// https://facebook.github.io/react/docs/context.html const context = { + // Navigation manager, e.g. history.push('/home') + // https://github.com/mjackson/history + history: createBrowserHistory(), + // Enables critical path CSS rendering + // https://github.com/kriasoft/isomorphic-style-loader insertCss: (...styles) => { - const removeCss = styles.map(style => style._insertCss()); // eslint-disable-line no-underscore-dangle, max-len - return () => { - removeCss.forEach(f => f()); - }; + // eslint-disable-next-line no-underscore-dangle + const removeCss = styles.map(x => x._insertCss()); + return () => { removeCss.forEach(f => f()); }; }, }; @@ -55,22 +63,25 @@ function updateCustomMeta(name, value) { // eslint-disable-line no-unused-vars } // Restore the scroll position if it was saved into the state -function restoreScrollPosition({ state, hash }) { - if (state && state.scrollY !== undefined) { - window.scrollTo(state.scrollX, state.scrollY); - return; - } - - const targetHash = hash && hash.substr(1); - if (targetHash) { - const target = document.getElementById(targetHash); - if (target) { - window.scrollTo(0, windowScrollY() + target.getBoundingClientRect().top); - return; +let locationStates = {}; +function restoreScrollPosition({ key, hash }) { + let scrollX = 0; + let scrollY = 0; + const state = locationStates[key]; + if (state) { + scrollX = state.scrollX; + scrollY = state.scrollY; + } else { + const targetHash = hash && hash.substr(1); + if (targetHash) { + const target = document.getElementById(targetHash); + if (target) { + scrollY = windowScrollY() + target.getBoundingClientRect().top; + } } } - window.scrollTo(0, 0); + window.scrollTo(scrollX, scrollY); } let onRenderComplete = function initialRenderComplete() { @@ -102,7 +113,7 @@ function render(route, location) { return new Promise((resolve, reject) => { try { ReactDOM.render( - route.component, + <App context={context}>{route.component}</App>, container, onRenderComplete.bind(undefined, route, location) ); @@ -115,27 +126,26 @@ function render(route, location) { // Make taps on links and buttons work fast on mobiles FastClick.attach(document.body); -let currentLocation = history.getCurrentLocation(); +let currentLocation = context.history.location; let routes = require('./routes').default; // Re-render the app when window.location changes async function onLocationChange(location) { // Save the page scroll position into the current location's state - if (currentLocation.key) { - saveState(currentLocation.key, { - ...readState(currentLocation.key), - scrollX: windowScrollX(), - scrollY: windowScrollY(), - }); + locationStates[currentLocation.key] = { + scrollX: windowScrollX(), + scrollY: windowScrollY(), + }; + if (history.action === 'PUSH') { + delete locationStates[location.key]; } currentLocation = location; try { const route = await UniversalRouter.resolve(routes, { path: location.pathname, - query: location.query, + query: queryString.parse(location.search), state: location.state, - context, }); await render(route, location); @@ -146,8 +156,8 @@ async function onLocationChange(location) { } // Add History API listener and trigger initial change -const removeHistoryListener = history.listen(onLocationChange); -history.replace(currentLocation); +const removeHistoryListener = context.history.listen(onLocationChange); +onLocationChange(currentLocation); // Switch off the native scroll restoration behavior and handle it manually // https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration @@ -161,6 +171,7 @@ if (window.history && 'scrollRestoration' in window.history) { addEventListener(window, 'pagehide', function onPageHide() { removeEventListener(window, 'pagehide', onPageHide); removeHistoryListener(); + locationStates = {}; if (originalScrollRestoration) { window.history.scrollRestoration = originalScrollRestoration; originalScrollRestoration = undefined; @@ -172,6 +183,6 @@ if (module.hot) { module.hot.accept('./routes', () => { routes = require('./routes').default; // eslint-disable-line global-require - onLocationChange(history.getCurrentLocation()); + onLocationChange(context.history.location); }); } diff --git a/src/components/App.js b/src/components/App.js @@ -0,0 +1,57 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import React, { PropTypes } from 'react'; + +const ContextType = { + // Navigation manager, e.g. history.push('/home') + // https://github.com/mjackson/history + history: PropTypes.object.isRequired, + // Enables critical path CSS rendering + // https://github.com/kriasoft/isomorphic-style-loader + insertCss: PropTypes.func.isRequired, +}; + +/** + * The top-level React component setting context (global) variables + * that can be accessed from all the child components. + * + * https://facebook.github.io/react/docs/context.html + * + * Usage example: + * + * const context = { + * history: createBrowserHistory(), + * store: createStore(), + * }; + * + * ReactDOM.render(<App context={context}><HomePage /></App>, container); + */ +class App extends React.Component { + + static propTypes = { + context: PropTypes.shape(ContextType).isRequired, + children: PropTypes.element.isRequired, + }; + + static childContextTypes = ContextType; + + getChildContext() { + return this.props.context; + } + + render() { + // NOTE: If you need to add or modify header, footer etc. of the app, + // please do that inside the Layout component. + return React.Children.only(this.props.children); + } + +} + +export default App; diff --git a/src/components/App/App.js b/src/components/App/App.js @@ -1,66 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -import React, { Component, PropTypes } from 'react'; -import emptyFunction from 'fbjs/lib/emptyFunction'; -import s from './App.css'; -import Header from '../Header'; -import Feedback from '../Feedback'; -import Footer from '../Footer'; - -class App extends Component { - - static propTypes = { - context: PropTypes.shape({ - insertCss: PropTypes.func, - setTitle: PropTypes.func, - setMeta: PropTypes.func, - }), - children: PropTypes.element.isRequired, - error: PropTypes.object, - }; - - static childContextTypes = { - insertCss: PropTypes.func.isRequired, - setTitle: PropTypes.func.isRequired, - setMeta: PropTypes.func.isRequired, - }; - - getChildContext() { - const context = this.props.context; - return { - insertCss: context.insertCss || emptyFunction, - setTitle: context.setTitle || emptyFunction, - setMeta: context.setMeta || emptyFunction, - }; - } - - componentWillMount() { - const { insertCss } = this.props.context; - this.removeCss = insertCss(s); - } - - componentWillUnmount() { - this.removeCss(); - } - - render() { - return !this.props.error ? ( - <div> - <Header /> - {this.props.children} - <Feedback /> - <Footer /> - </div> - ) : this.props.children; - } - -} - -export default App; diff --git a/src/components/App/App.test.js b/src/components/App/App.test.js @@ -1,30 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -/* eslint-env mocha */ -/* eslint-disable padded-blocks, no-unused-expressions */ - -import React from 'react'; -import { expect } from 'chai'; -import { shallow } from 'enzyme'; -import App from './App'; - -describe('App', () => { - - it('renders children correctly', () => { - const wrapper = shallow( - <App context={{ insertCss: () => {} }}> - <div className="child" /> - </App> - ); - - expect(wrapper.contains(<div className="child" />)).to.be.true; - }); - -}); diff --git a/src/components/App/package.json b/src/components/App/package.json @@ -1,6 +0,0 @@ -{ - "name": "App", - "version": "0.0.0", - "private": true, - "main": "./App.js" -} diff --git a/src/components/Html.js b/src/components/Html.js @@ -1,3 +1,12 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + import React, { PropTypes } from 'react'; import { analytics } from '../config'; diff --git a/src/components/App/App.css b/src/components/Layout/Layout.css diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js @@ -0,0 +1,32 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import React, { PropTypes } from 'react'; +import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import s from './Layout.css'; +import Header from '../Header'; +import Feedback from '../Feedback'; +import Footer from '../Footer'; + +function Layout({ children }) { + return ( + <div> + <Header /> + {React.Children.only(children)} + <Feedback /> + <Footer /> + </div> + ); +} + +Layout.propTypes = { + children: PropTypes.element.isRequired, +}; + +export default withStyles(s)(Layout); diff --git a/src/components/Layout/Layout.test.js b/src/components/Layout/Layout.test.js @@ -0,0 +1,33 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +/* eslint-env mocha */ +/* eslint-disable padded-blocks, no-unused-expressions */ + +import React from 'react'; +import { expect } from 'chai'; +import { shallow } from 'enzyme'; +import App from '../App'; +import Layout from './Layout'; + +describe('Layout', () => { + + it('renders children correctly', () => { + const wrapper = shallow( + <App context={{ insertCss: () => {} }}> + <Layout> + <div className="child" /> + </Layout> + </App> + ); + + expect(wrapper.contains(<div className="child" />)).to.be.true; + }); + +}); diff --git a/src/components/Layout/package.json b/src/components/Layout/package.json @@ -0,0 +1,6 @@ +{ + "name": "Layout", + "version": "0.0.0", + "private": true, + "main": "./Layout.js" +} diff --git a/src/components/Link/Link.js b/src/components/Link/Link.js @@ -8,7 +8,6 @@ */ import React, { Component, PropTypes } from 'react'; -import history from '../../core/history'; function isLeftClickEvent(event) { return event.button === 0; @@ -21,14 +20,16 @@ function isModifiedEvent(event) { class Link extends Component { static propTypes = { - to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, + to: PropTypes.string.isRequired, children: PropTypes.node, onClick: PropTypes.func, }; - handleClick = (event) => { - let allowTransition = true; + static contextTypes = { + history: PropTypes.object.isRequired, + }; + handleClick = (event) => { if (this.props.onClick) { this.props.onClick(event); } @@ -38,26 +39,16 @@ class Link extends Component { } if (event.defaultPrevented === true) { - allowTransition = false; + return; } event.preventDefault(); - - if (allowTransition) { - if (this.props.to) { - history.push(this.props.to); - } else { - history.push({ - pathname: event.currentTarget.pathname, - search: event.currentTarget.search, - }); - } - } + this.context.history.push(this.props.to); }; render() { const { to, children, ...props } = this.props; - return <a href={history.createHref(to)} {...props} onClick={this.handleClick}>{children}</a>; + return <a href={to} {...props} onClick={this.handleClick}>{children}</a>; } } diff --git a/src/core/history.js b/src/core/history.js @@ -1,16 +0,0 @@ -/** - * React Starter Kit (https://www.reactstarterkit.com/) - * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE.txt file in the root directory of this source tree. - */ - -import createHistory from 'history/lib/createBrowserHistory'; -import createMemoryHistory from 'history/lib/createMemoryHistory'; -import useQueries from 'history/lib/useQueries'; - -const history = useQueries(process.env.BROWSER ? createHistory : createMemoryHistory)(); - -export default history; diff --git a/src/routes/contact/Contact.js b/src/routes/contact/Contact.js @@ -9,19 +9,24 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import Layout from '../../components/Layout'; import s from './Contact.css'; function Contact({ title }) { return ( - <div className={s.root}> - <div className={s.container}> - <h1>{title}</h1> - <p>...</p> + <Layout> + <div className={s.root}> + <div className={s.container}> + <h1>{title}</h1> + <p>...</p> + </div> </div> - </div> + </Layout> ); } -Contact.propTypes = { title: PropTypes.string.isRequired }; +Contact.propTypes = { + title: PropTypes.string.isRequired, +}; export default withStyles(s)(Contact); diff --git a/src/routes/content/Content.js b/src/routes/content/Content.js @@ -9,16 +9,19 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import Layout from '../../components/Layout'; import s from './Content.css'; function Content({ path, title, content }) { return ( - <div className={s.root}> - <div className={s.container}> - {title && path !== '/' && <h1>{title}</h1>} - <div dangerouslySetInnerHTML={{ __html: content }} /> + <Layout> + <div className={s.root}> + <div className={s.container}> + {title && path !== '/' && <h1>{title}</h1>} + <div dangerouslySetInnerHTML={{ __html: content }} /> + </div> </div> - </div> + </Layout> ); } diff --git a/src/routes/error/ErrorPage.js b/src/routes/error/ErrorPage.js @@ -9,6 +9,7 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import Layout from '../../components/Layout'; import s from './ErrorPage.css'; function ErrorPage({ error }) { @@ -22,15 +23,19 @@ function ErrorPage({ error }) { } return ( - <div> - <h1>{error.name}</h1> - <p>{error.message}</p> - <pre>{error.stack}</pre> - </div> + <Layout> + <div> + <h1>{error.name}</h1> + <p>{error.message}</p> + <pre>{error.stack}</pre> + </div> + </Layout> ); } -ErrorPage.propTypes = { error: PropTypes.object.isRequired }; +ErrorPage.propTypes = { + error: PropTypes.object.isRequired, +}; export { ErrorPage as ErrorPageWithoutStyle }; export default withStyles(s)(ErrorPage); diff --git a/src/routes/home/Home.js b/src/routes/home/Home.js @@ -9,26 +9,29 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import Layout from '../../components/Layout'; import s from './Home.css'; function Home({ news }) { return ( - <div className={s.root}> - <div className={s.container}> - <h1 className={s.title}>React.js News</h1> - <ul className={s.news}> - {news.map((item, index) => ( - <li key={index} className={s.newsItem}> - <a href={item.link} className={s.newsTitle}>{item.title}</a> - <span - className={s.newsDesc} - dangerouslySetInnerHTML={{ __html: item.contentSnippet }} - /> - </li> - ))} - </ul> + <Layout> + <div className={s.root}> + <div className={s.container}> + <h1 className={s.title}>React.js News</h1> + <ul className={s.news}> + {news.map((item, index) => ( + <li key={index} className={s.newsItem}> + <a href={item.link} className={s.newsTitle}>{item.title}</a> + <span + className={s.newsDesc} + dangerouslySetInnerHTML={{ __html: item.contentSnippet }} + /> + </li> + ))} + </ul> + </div> </div> - </div> + </Layout> ); } diff --git a/src/routes/index.js b/src/routes/index.js @@ -7,34 +7,26 @@ * LICENSE.txt file in the root directory of this source tree. */ -import React from 'react'; -import App from '../components/App'; - -// Child routes -import home from './home'; -import contact from './contact'; -import login from './login'; -import register from './register'; -import content from './content'; -import notFound from './notFound'; +/* eslint-disable global-require */ +// The top-level (parent) route export default { path: '/', - // keep in mind, routes are evaluated in order + // Keep in mind, routes are evaluated in order children: [ - home, - contact, - login, - register, + require('./home').default, + require('./contact').default, + require('./login').default, + require('./register').default, // place new routes before... - content, - notFound, + require('./content').default, + require('./notFound').default, ], - async action({ next, context }) { + async action({ next }) { let route; // Execute each child route until one of them return the result @@ -43,14 +35,11 @@ export default { route = await next(); } while (!route); - return { - ...route, + // Provide default values for title, description etc. + route.title = `${route.title || 'Untitled Page'} - www.reactstarterkit.com`; + route.description = route.description || ''; - // Override the result of child route with extensions - title: `${route.title || 'Untitled Page'} - www.reactstarterkit.com`, - description: route.description || '', - component: <App context={context}>{route.component}</App>, - }; + return route; }, }; diff --git a/src/routes/login/Login.js b/src/routes/login/Login.js @@ -9,111 +9,116 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import Layout from '../../components/Layout'; import s from './Login.css'; function Login({ title }) { return ( - <div className={s.root}> - <div className={s.container}> - <h1>{title}</h1> - <p className={s.lead}>Log in with your username or company email address.</p> - <div className={s.formGroup}> - <a className={s.facebook} href="/login/facebook"> - <svg - className={s.icon} - width="30" - height="30" - viewBox="0 0 30 30" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M22 16l1-5h-5V7c0-1.544.784-2 3-2h2V0h-4c-4.072 0-7 2.435-7 7v4H7v5h5v14h6V16h4z" - /> - </svg> - <span>Log in with Facebook</span> - </a> - </div> - <div className={s.formGroup}> - <a className={s.google} href="/login/google"> - <svg - className={s.icon} - width="30" - height="30" - viewBox="0 0 30 30" - xmlns="http://www.w3.org/2000/svg" - > - <path - d={'M30 13h-4V9h-2v4h-4v2h4v4h2v-4h4m-15 2s-2-1.15-2-2c0 0-.5-1.828 1-3 ' + - '1.537-1.2 3-3.035 3-5 0-2.336-1.046-5-3-6h3l2.387-1H10C5.835 0 2 3.345 2 7c0 ' + - '3.735 2.85 6.56 7.086 6.56.295 0 .58-.006.86-.025-.273.526-.47 1.12-.47 1.735 ' + - '0 1.037.817 2.042 1.523 2.73H9c-5.16 0-9 2.593-9 6 0 3.355 4.87 6 10.03 6 5.882 ' + - '0 9.97-3 9.97-7 0-2.69-2.545-4.264-5-6zm-4-4c-2.395 0-5.587-2.857-6-6C4.587 ' + - '3.856 6.607.93 9 1c2.394.07 4.603 2.908 5.017 6.052C14.43 10.195 13 13 11 ' + - '13zm-1 15c-3.566 0-7-1.29-7-4 0-2.658 3.434-5.038 7-5 .832.01 2 0 2 0 1 0 ' + - '2.88.88 4 2 1 1 1 2.674 1 3 0 3-1.986 4-7 4z'} - /> - </svg> - <span>Log in with Google</span> - </a> - </div> - <div className={s.formGroup}> - <a className={s.twitter} href="/login/twitter"> - <svg - className={s.icon} - width="30" - height="30" - viewBox="0 0 30 30" - xmlns="http://www.w3.org/2000/svg" - > - <path - d={'M30 6.708c-1.105.49-2.756 1.143-4 1.292 1.273-.762 2.54-2.56 ' + - '3-4-.97.577-2.087 1.355-3.227 1.773L25 5c-1.12-1.197-2.23-2-4-2-3.398 0-6 ' + - '2.602-6 6 0 .4.047.7.11.956L15 10C9 10 5.034 8.724 2 5c-.53.908-1 1.872-1 ' + - '3 0 2.136 1.348 3.894 3 5-1.01-.033-2.17-.542-3-1 0 2.98 4.186 6.432 7 7-1 ' + - '1-4.623.074-5 0 .784 2.447 3.31 3.95 6 4-2.105 1.648-4.647 2.51-7.53 2.51-.5 ' + - '0-.988-.03-1.47-.084C2.723 27.17 6.523 28 10 28c11.322 0 17-8.867 17-17 ' + - '0-.268.008-.736 0-1 1.2-.868 2.172-2.058 3-3.292z'} - /> - </svg> - <span>Log in with Twitter</span> - </a> - </div> - <strong className={s.lineThrough}>OR</strong> - <form method="post"> + <Layout> + <div className={s.root}> + <div className={s.container}> + <h1>{title}</h1> + <p className={s.lead}>Log in with your username or company email address.</p> <div className={s.formGroup}> - <label className={s.label} htmlFor="usernameOrEmail"> - Username or email address: - </label> - <input - className={s.input} - id="usernameOrEmail" - type="text" - name="usernameOrEmail" - autoFocus - /> + <a className={s.facebook} href="/login/facebook"> + <svg + className={s.icon} + width="30" + height="30" + viewBox="0 0 30 30" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M22 16l1-5h-5V7c0-1.544.784-2 3-2h2V0h-4c-4.072 0-7 2.435-7 7v4H7v5h5v14h6V16h4z" + /> + </svg> + <span>Log in with Facebook</span> + </a> </div> <div className={s.formGroup}> - <label className={s.label} htmlFor="password"> - Password: - </label> - <input - className={s.input} - id="password" - type="password" - name="password" - /> + <a className={s.google} href="/login/google"> + <svg + className={s.icon} + width="30" + height="30" + viewBox="0 0 30 30" + xmlns="http://www.w3.org/2000/svg" + > + <path + d={'M30 13h-4V9h-2v4h-4v2h4v4h2v-4h4m-15 2s-2-1.15-2-2c0 0-.5-1.828 1-3 ' + + '1.537-1.2 3-3.035 3-5 0-2.336-1.046-5-3-6h3l2.387-1H10C5.835 0 2 3.345 2 7c0 ' + + '3.735 2.85 6.56 7.086 6.56.295 0 .58-.006.86-.025-.273.526-.47 1.12-.47 1.735 ' + + '0 1.037.817 2.042 1.523 2.73H9c-5.16 0-9 2.593-9 6 0 3.355 4.87 6 10.03 6 5.882 ' + + '0 9.97-3 9.97-7 0-2.69-2.545-4.264-5-6zm-4-4c-2.395 0-5.587-2.857-6-6C4.587 ' + + '3.856 6.607.93 9 1c2.394.07 4.603 2.908 5.017 6.052C14.43 10.195 13 13 11 ' + + '13zm-1 15c-3.566 0-7-1.29-7-4 0-2.658 3.434-5.038 7-5 .832.01 2 0 2 0 1 0 ' + + '2.88.88 4 2 1 1 1 2.674 1 3 0 3-1.986 4-7 4z'} + /> + </svg> + <span>Log in with Google</span> + </a> </div> <div className={s.formGroup}> - <button className={s.button} type="submit"> - Log in - </button> + <a className={s.twitter} href="/login/twitter"> + <svg + className={s.icon} + width="30" + height="30" + viewBox="0 0 30 30" + xmlns="http://www.w3.org/2000/svg" + > + <path + d={'M30 6.708c-1.105.49-2.756 1.143-4 1.292 1.273-.762 2.54-2.56 ' + + '3-4-.97.577-2.087 1.355-3.227 1.773L25 5c-1.12-1.197-2.23-2-4-2-3.398 0-6 ' + + '2.602-6 6 0 .4.047.7.11.956L15 10C9 10 5.034 8.724 2 5c-.53.908-1 1.872-1 ' + + '3 0 2.136 1.348 3.894 3 5-1.01-.033-2.17-.542-3-1 0 2.98 4.186 6.432 7 7-1 ' + + '1-4.623.074-5 0 .784 2.447 3.31 3.95 6 4-2.105 1.648-4.647 2.51-7.53 2.51-.5 ' + + '0-.988-.03-1.47-.084C2.723 27.17 6.523 28 10 28c11.322 0 17-8.867 17-17 ' + + '0-.268.008-.736 0-1 1.2-.868 2.172-2.058 3-3.292z'} + /> + </svg> + <span>Log in with Twitter</span> + </a> </div> - </form> + <strong className={s.lineThrough}>OR</strong> + <form method="post"> + <div className={s.formGroup}> + <label className={s.label} htmlFor="usernameOrEmail"> + Username or email address: + </label> + <input + className={s.input} + id="usernameOrEmail" + type="text" + name="usernameOrEmail" + autoFocus + /> + </div> + <div className={s.formGroup}> + <label className={s.label} htmlFor="password"> + Password: + </label> + <input + className={s.input} + id="password" + type="password" + name="password" + /> + </div> + <div className={s.formGroup}> + <button className={s.button} type="submit"> + Log in + </button> + </div> + </form> + </div> </div> - </div> + </Layout> ); } -Login.propTypes = { title: PropTypes.string.isRequired }; +Login.propTypes = { + title: PropTypes.string.isRequired, +}; export default withStyles(s)(Login); diff --git a/src/routes/notFound/NotFound.js b/src/routes/notFound/NotFound.js @@ -9,19 +9,24 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import Layout from '../../components/Layout'; import s from './NotFound.css'; function NotFound({ title }) { return ( - <div className={s.root}> - <div className={s.container}> - <h1>{title}</h1> - <p>Sorry, the page you were trying to view does not exist.</p> + <Layout full={false}> + <div className={s.root}> + <div className={s.container}> + <h1>{title}</h1> + <p>Sorry, the page you were trying to view does not exist.</p> + </div> </div> - </div> + </Layout> ); } -NotFound.propTypes = { title: PropTypes.string.isRequired }; +NotFound.propTypes = { + title: PropTypes.string.isRequired, +}; export default withStyles(s)(NotFound); diff --git a/src/routes/register/Register.js b/src/routes/register/Register.js @@ -9,16 +9,19 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; +import Layout from '../../components/Layout'; import s from './Register.css'; function Register({ title }) { return ( - <div className={s.root}> - <div className={s.container}> - <h1>{title}</h1> - <p>...</p> + <Layout> + <div className={s.root}> + <div className={s.container}> + <h1>{title}</h1> + <p>...</p> + </div> </div> - </div> + </Layout> ); } diff --git a/src/server.js b/src/server.js @@ -18,7 +18,9 @@ import jwt from 'jsonwebtoken'; import React from 'react'; import ReactDOM from 'react-dom/server'; import UniversalRouter from 'universal-router'; +import createMemoryHistory from 'history/createMemoryHistory'; import PrettyError from 'pretty-error'; +import App from './components/App'; import Html from './components/Html'; import { ErrorPageWithoutStyle } from './routes/error/ErrorPage'; import errorPageStyle from './routes/error/ErrorPage.css'; @@ -85,18 +87,31 @@ app.use('/graphql', expressGraphQL(req => ({ app.get('*', async (req, res, next) => { try { const css = new Set(); + + // Global (context) variables that can be easily accessed from any React component + // https://facebook.github.io/react/docs/context.html + const context = { + // Navigation manager, e.g. history.push('/home') + // https://github.com/mjackson/history + history: createMemoryHistory({ + initialEntries: [req.url], + }), + // Enables critical path CSS rendering + // https://github.com/kriasoft/isomorphic-style-loader + insertCss: (...styles) => { + // eslint-disable-next-line no-underscore-dangle + styles.forEach(style => css.add(style._getCss())); + }, + }; + const route = await UniversalRouter.resolve(routes, { path: req.path, query: req.query, - context: { - insertCss: (...styles) => { - styles.forEach(style => css.add(style._getCss())); // eslint-disable-line no-underscore-dangle, max-len - }, - }, + context, }); const data = { ...route }; - data.children = ReactDOM.renderToString(route.component); + data.children = ReactDOM.renderToString(<App context={context}>{route.component}</App>); data.style = [...css].join(''); data.script = assets.main.js; const html = ReactDOM.renderToStaticMarkup(<Html {...data} />);