ff-stream-web

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

commit ff5016fd2f52ae9d852d7ebf640ce108f05342b7
parent c9f7230b5c50b9ece6ab6bc710ecad8be76f2aae
Author: Konstantin Tarkus <hello@tarkus.me>
Date:   Fri,  8 Apr 2016 23:16:08 +0300

Update routing library and the list of routes

Diffstat:
MCHANGELOG.md | 5+++++
Mpackage.json | 58++++++++++++++++++++++++++++++++--------------------------
Msrc/client.js | 74+++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/components/App/App.js | 15++++++---------
Dsrc/components/ContentPage/ContentPage.js | 43-------------------------------------------
Dsrc/components/ContentPage/ContentPage.scss | 21---------------------
Dsrc/components/ContentPage/package.json | 6------
Dsrc/components/ErrorPage/ErrorPage.js | 48------------------------------------------------
Dsrc/components/ErrorPage/ErrorPage.scss | 57---------------------------------------------------------
Dsrc/components/ErrorPage/package.json | 6------
Dsrc/routes.js | 46----------------------------------------------
Msrc/routes/contact/Contact.js | 7+++++--
Msrc/routes/contact/index.js | 13++++++++-----
Asrc/routes/content/Content.js | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/routes/content/Content.scss | 21+++++++++++++++++++++
Asrc/routes/content/index.js | 36++++++++++++++++++++++++++++++++++++
Asrc/routes/error/ErrorPage.js | 40++++++++++++++++++++++++++++++++++++++++
Asrc/routes/error/ErrorPage.scss | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/routes/error/index.js | 27+++++++++++++++++++++++++++
Msrc/routes/home/Home.js | 6+++++-
Msrc/routes/home/index.js | 27+++++++++++++++++++++------
Asrc/routes/index.js | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/routes/login/Login.js | 7+++++--
Msrc/routes/login/index.js | 13++++++++-----
Msrc/routes/register/Register.js | 7+++++--
Msrc/routes/register/index.js | 13++++++++-----
Msrc/server.js | 30++++++++++++++++++------------
27 files changed, 438 insertions(+), 333 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. ### [Unreleased][unreleased] +- Rename `onSetTitle`, `onSetMeta` context variables to `setTitle`, `setMeta` +- Move `Content` component to `src/routes/content` +- Move `ErrorPage` component to `src/routes/error` +- Move the list of top-level routes to `src/routes/index` +- Update routing to use `universal-router` library - Move Babel, ESLint and JSCS configurations to `package.json` [#497](https://github.com/kriasoft/react-starter-kit/pull/497) - Convert `Feedback`, `Footer`, `Header`, and `Navigation` to functional stateless components - Move page / screen components into the `src/routes` folder along with the routing information for them [BREAKING CHANGE]. [6553936](https://github.com/kriasoft/react-starter-kit/commit/6553936e693e24a8ac6178f4962af15e0ea87dfd) diff --git a/package.json b/package.json @@ -11,45 +11,45 @@ "body-parser": "1.15.0", "classnames": "2.2.3", "cookie-parser": "1.4.1", - "core-js": "2.2.1", + "core-js": "2.2.2", "eventemitter3": "1.2.0", "express": "4.13.4", "express-graphql": "0.4.13", "express-jwt": "3.3.0", "fastclick": "1.0.6", - "fbjs": "0.8.0-alpha.3", - "front-matter": "2.0.6", - "graphiql": "0.6.6", - "graphql": "0.4.18", + "fbjs": "0.8.0", + "front-matter": "2.0.7", + "graphiql": "0.7.0", + "graphql": "0.5.0", "history": "2.0.1", "isomorphic-style-loader": "0.0.12", "jade": "1.11.0", "jsonwebtoken": "5.7.0", - "markdown-it": "6.0.0", - "node-fetch": "1.4.1", + "markdown-it": "6.0.1", + "node-fetch": "1.5.0", "normalize.css": "4.0.0", "passport": "0.3.2", "passport-facebook": "2.1.0", - "pg": "4.5.1", + "pg": "4.5.2", "pretty-error": "2.0.0", - "react": "15.0.0-rc.2", - "react-dom": "15.0.0-rc.2", - "react-routing": "0.0.7", + "react": "15.0.1", + "react-dom": "15.0.1", "source-map-support": "0.4.0", + "universal-router": "1.1.0-beta.3", "whatwg-fetch": "0.11.0" }, "devDependencies": { "assets-webpack-plugin": "^3.4.0", - "autoprefixer": "^6.3.4", - "babel-cli": "^6.6.5", - "babel-eslint": "^6.0.0", - "babel-jest": "^9.0.3", + "autoprefixer": "^6.3.6", + "babel-cli": "^6.7.5", + "babel-eslint": "^6.0.2", + "babel-jest": "^10.0.1", "babel-loader": "^6.2.4", "babel-plugin-react-transform": "^2.0.2", "babel-plugin-transform-react-constant-elements": "^6.5.0", "babel-plugin-transform-react-inline-elements": "^6.6.5", "babel-plugin-transform-react-remove-prop-types": "^0.2.4", - "babel-plugin-transform-runtime": "^6.6.0", + "babel-plugin-transform-runtime": "^6.7.5", "babel-preset-es2015": "^6.6.0", "babel-preset-node5": "^11.0.1", "babel-preset-react": "^6.5.0", @@ -58,10 +58,10 @@ "css-loader": "^0.23.1", "del": "^2.2.0", "enzyme": "^2.2.0", - "eslint": "^2.5.1", + "eslint": "^2.7.0", "eslint-config-airbnb": "^6.2.0", "eslint-loader": "^1.3.0", - "eslint-plugin-react": "^4.2.3", + "eslint-plugin-react": "^4.3.0", "estraverse-fb": "^1.3.1", "extend": "^3.0.0", "file-loader": "^0.8.5", @@ -69,23 +69,23 @@ "git-repository": "^0.1.4", "glob": "^7.0.3", "jade-loader": "^0.8.0", - "jest-cli": "^0.9.2", + "jest-cli": "^0.10.0", "jscs": "^2.11.0", "json-loader": "^0.5.4", "mkdirp": "^0.5.1", "ncp": "^2.0.0", "postcss": "^5.0.19", - "postcss-import": "^8.0.2", + "postcss-import": "^8.1.0", "postcss-loader": "^0.8.2", "postcss-scss": "^0.1.7", "precss": "^1.4.0", "raw-loader": "^0.5.1", - "react-addons-test-utils": "^15.0.0-rc.2", + "react-addons-test-utils": "^15.0.1", "react-transform-catch-errors": "^1.0.2", "react-transform-hmr": "^1.0.4", - "redbox-react": "^1.2.2", - "stylelint": "^5.2.1", - "stylelint-config-standard": "^4.0.1", + "redbox-react": "^1.2.3", + "stylelint": "^5.3.0", + "stylelint-config-standard": "^5.0.0", "url-loader": "^0.5.7", "webpack": "^1.12.14", "webpack-hot-middleware": "^2.10.0", @@ -111,12 +111,18 @@ "rules": { "no-confusing-arrow": 0, "react/jsx-quotes": 0, - "jsx-quotes": [2, "prefer-double"] + "jsx-quotes": [ + 2, + "prefer-double" + ] } }, "jscsConfig": { "preset": "airbnb", - "excludeFiles": ["build/**", "node_modules/**"], + "excludeFiles": [ + "build/**", + "node_modules/**" + ], "disallowSpacesInsideTemplateStringPlaceholders": null }, "jest": { diff --git a/src/client.js b/src/client.js @@ -10,16 +10,16 @@ import 'babel-polyfill'; import ReactDOM from 'react-dom'; import FastClick from 'fastclick'; -import Router from './routes'; +import { match } from 'universal-router'; +import routes from './routes'; import Location from './core/Location'; import { addEventListener, removeEventListener } from './core/DOMUtils'; -let cssContainer = document.getElementById('css'); -const appContainer = document.getElementById('app'); +const container = document.getElementById('app'); const context = { insertCss: styles => styles._insertCss(), - onSetTitle: value => (document.title = value), - onSetMeta: (name, content) => { + setTitle: value => (document.title = value), + setMeta: (name, content) => { // Remove and create a new <meta /> tag in order to make it work // with bookmarks in Safari const elements = document.getElementsByTagName('meta'); @@ -37,49 +37,61 @@ const context = { }, }; -// Google Analytics tracking. Don't send 'pageview' event after the first -// rendering, as it was already sent by the Html component. -let trackPageview = () => (trackPageview = () => window.ga('send', 'pageview')); +// Restore the scroll position if it was saved into the state +function restoreScrollPosition(state) { + if (state && state.scrollY !== undefined) { + window.scrollTo(state.scrollX, state.scrollY); + } else { + window.scrollTo(0, 0); + } +} -function render(state) { - Router.dispatch(state, (newState, component) => { - ReactDOM.render(component, appContainer, () => { - // Restore the scroll position if it was saved into the state - if (state.scrollY !== undefined) { - window.scrollTo(state.scrollX, state.scrollY); - } else { - window.scrollTo(0, 0); - } +let renderComplete = (state, callback) => { + restoreScrollPosition(state); + const elem = document.getElementById('css'); + if (elem) elem.parentNode.removeChild(elem); + callback(true); + renderComplete = (s) => { + restoreScrollPosition(s); - trackPageview(); + // Google Analytics tracking. Don't send 'pageview' event after + // the initial rendering, as it was already sent + window.ga('send', 'pageview'); - // Remove the pre-rendered CSS because it's no longer used - // after the React app is launched - if (cssContainer) { - cssContainer.parentNode.removeChild(cssContainer); - cssContainer = null; - } - }); + callback(true); + }; +}; + +function render(state, component) { + return new Promise((resolve, reject) => { + try { + ReactDOM.render( + component, + container, + renderComplete.bind(undefined, state, resolve) + ); + } catch (err) { + reject(err); + } }); } function run() { let currentLocation = null; - let currentState = null; // Make taps on links and buttons work fast on mobiles FastClick.attach(document.body); // Re-render the app when window.location changes - const unlisten = Location.listen(location => { + const removeLocationListener = Location.listen(location => { currentLocation = location; - currentState = Object.assign({}, location.state, { + match(routes, { path: location.pathname, query: location.query, state: location.state, context, - }); - render(currentState); + render: render.bind(undefined, location.state), + }).catch(err => console.error(err)); // eslint-disable-line no-console }); // Save the page scroll position into the current location's state @@ -101,7 +113,7 @@ function run() { addEventListener(window, 'scroll', setPageOffset); addEventListener(window, 'pagehide', () => { removeEventListener(window, 'scroll', setPageOffset); - unlisten(); + removeLocationListener(); }); } diff --git a/src/components/App/App.js b/src/components/App/App.js @@ -19,9 +19,8 @@ class App extends Component { static propTypes = { context: PropTypes.shape({ insertCss: PropTypes.func, - onSetTitle: PropTypes.func, - onSetMeta: PropTypes.func, - onPageNotFound: PropTypes.func, + setTitle: PropTypes.func, + setMeta: PropTypes.func, }), children: PropTypes.element.isRequired, error: PropTypes.object, @@ -29,18 +28,16 @@ class App extends Component { static childContextTypes = { insertCss: PropTypes.func.isRequired, - onSetTitle: PropTypes.func.isRequired, - onSetMeta: PropTypes.func.isRequired, - onPageNotFound: PropTypes.func.isRequired, + setTitle: PropTypes.func.isRequired, + setMeta: PropTypes.func.isRequired, }; getChildContext() { const context = this.props.context; return { insertCss: context.insertCss || emptyFunction, - onSetTitle: context.onSetTitle || emptyFunction, - onSetMeta: context.onSetMeta || emptyFunction, - onPageNotFound: context.onPageNotFound || emptyFunction, + setTitle: context.setTitle || emptyFunction, + setMeta: context.setMeta || emptyFunction, }; } diff --git a/src/components/ContentPage/ContentPage.js b/src/components/ContentPage/ContentPage.js @@ -1,43 +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 withStyles from 'isomorphic-style-loader/lib/withStyles'; -import s from './ContentPage.scss'; - -class ContentPage extends Component { - - static propTypes = { - path: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - title: PropTypes.string, - }; - - static contextTypes = { - onSetTitle: PropTypes.func.isRequired, - }; - - componentWillMount() { - this.context.onSetTitle(this.props.title); - } - - render() { - return ( - <div className={s.root}> - <div className={s.container}> - {this.props.path === '/' ? null : <h1>{this.props.title}</h1>} - <div dangerouslySetInnerHTML={{ __html: this.props.content || '' }} /> - </div> - </div> - ); - } - -} - -export default withStyles(ContentPage, s); diff --git a/src/components/ContentPage/ContentPage.scss b/src/components/ContentPage/ContentPage.scss @@ -1,21 +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 '../variables.scss'; - -.root { - padding-left: 20px; - padding-right: 20px; -} - -.container { - margin: 0 auto; - padding: 0 0 40px; - max-width: $max-content-width; -} diff --git a/src/components/ContentPage/package.json b/src/components/ContentPage/package.json @@ -1,6 +0,0 @@ -{ - "name": "ContentPage", - "version": "0.0.0", - "private": true, - "main": "./ContentPage.js" -} diff --git a/src/components/ErrorPage/ErrorPage.js b/src/components/ErrorPage/ErrorPage.js @@ -1,48 +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 withStyles from 'isomorphic-style-loader/lib/withStyles'; -import s from './ErrorPage.scss'; - -let title = 'Error'; -let content = 'Sorry, a critical error occurred on this page.'; - -class ErrorPage extends Component { - - static contextTypes = { - onSetTitle: PropTypes.func.isRequired, - onPageNotFound: PropTypes.func.isRequired, - }; - - static propTypes = { - statusCode: PropTypes.number, - }; - - componentWillMount() { - this.context.onSetTitle(title); - } - - render() { - if (this.props.statusCode === 404) { - title = 'Page Not Found'; - content = 'Sorry, the page you were trying to view does not exist.'; - } - - return ( - <div> - <h1>{title}</h1> - <p>{content}</p> - </div> - ); - } - -} - -export default withStyles(ErrorPage, s); diff --git a/src/components/ErrorPage/ErrorPage.scss b/src/components/ErrorPage/ErrorPage.scss @@ -1,57 +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. - */ - -* { - line-height: 1.2; - margin: 0; -} - -html { - color: #888; - display: table; - font-family: sans-serif; - height: 100%; - text-align: center; - width: 100%; -} - -body { - display: table-cell; - vertical-align: middle; - margin: 2em auto; -} - -h1 { - color: #555; - font-size: 2em; - font-weight: 400; -} - -p { - margin: 0 auto; - width: 280px; -} - -pre { - text-align: left; - margin-top: 2rem; -} - -@media only screen and (max-width: 280px) { - - body, p { - width: 95%; - } - - h1 { - font-size: 1.5em; - margin: 0 0 0.3em; - } - -} diff --git a/src/components/ErrorPage/package.json b/src/components/ErrorPage/package.json @@ -1,6 +0,0 @@ -{ - "name": "ErrorPage", - "version": "0.0.0", - "private": true, - "main": "./ErrorPage.js" -} diff --git a/src/routes.js b/src/routes.js @@ -1,46 +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 from 'react'; -import Router from 'react-routing/src/Router'; -import fetch from './core/fetch'; -import App from './components/App'; -import ContentPage from './components/ContentPage'; -import ErrorPage from './components/ErrorPage'; - -const routes = [ - require('./routes/home'), - require('./routes/contact'), - require('./routes/login'), - require('./routes/register'), -]; - -const router = new Router(on => { - on('*', async (state, next) => { - const component = await next(); - return component && <App context={state.context}>{component}</App>; - }); - - routes.forEach(route => { - on(route.path, route.action); - }); - - on('*', async (state) => { - const query = `/graphql?query={content(path:"${state.path}"){path,title,content,component}}`; - const response = await fetch(query); - const { data } = await response.json(); - return data && data.content && <ContentPage {...data.content} />; - }); - - on('error', (state, error) => - <App context={state.context} error={error}><ErrorPage statusCode={state.statusCode} /></App> - ); -}); - -export default router; diff --git a/src/routes/contact/Contact.js b/src/routes/contact/Contact.js @@ -11,7 +11,10 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; import s from './Contact.scss'; -function Contact({ title }) { +const title = 'Contact Us'; + +function Contact(props, context) { + context.setTitle(title); return ( <div className={s.root}> <div className={s.container}> @@ -22,6 +25,6 @@ function Contact({ title }) { ); } -Contact.propTypes = { title: PropTypes.string.isRequired }; +Contact.contextTypes = { setTitle: PropTypes.func.isRequired }; export default withStyles(Contact, s); diff --git a/src/routes/contact/index.js b/src/routes/contact/index.js @@ -10,9 +10,12 @@ import React from 'react'; import Contact from './Contact'; -export const path = '/contact'; -export const action = async (state) => { - const title = 'Contact Us'; - state.context.onSetTitle(title); - return <Contact title={title} />; +export default { + + path: '/contact', + + action() { + return <Contact />; + }, + }; diff --git a/src/routes/content/Content.js b/src/routes/content/Content.js @@ -0,0 +1,43 @@ +/** + * 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 withStyles from 'isomorphic-style-loader/lib/withStyles'; +import s from './Content.scss'; + +class Content extends Component { + + static contextTypes = { + setTitle: PropTypes.func.isRequired, + }; + + static propTypes = { + path: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, + title: PropTypes.string, + }; + + componentWillMount() { + this.context.setTitle(this.props.title); + } + + render() { + return ( + <div className={s.root}> + <div className={s.container}> + {this.props.path === '/' ? null : <h1>{this.props.title}</h1>} + <div dangerouslySetInnerHTML={{ __html: this.props.content || '' }} /> + </div> + </div> + ); + } + +} + +export default withStyles(Content, s); diff --git a/src/routes/content/Content.scss b/src/routes/content/Content.scss @@ -0,0 +1,21 @@ +/** + * 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 '../../components/variables.scss'; + +.root { + padding-left: 20px; + padding-right: 20px; +} + +.container { + margin: 0 auto; + padding: 0 0 40px; + max-width: $max-content-width; +} diff --git a/src/routes/content/index.js b/src/routes/content/index.js @@ -0,0 +1,36 @@ +/** + * 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 from 'react'; +import Content from './Content'; +import fetch from '../../core/fetch'; + +export default { + + path: '*', + + async action({ path }) { // eslint-disable-line react/prop-types + const resp = await fetch('/graphql', { + method: 'post', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: `{content(path:"${path}"){path,title,content,component}}`, + }), + credentials: 'include', + }); + if (resp.status !== 200) throw new Error(resp.statusText); + const { data } = await resp.json(); + if (!data || !data.content) return undefined; + return <Content {...data.content} />; + }, + +}; diff --git a/src/routes/error/ErrorPage.js b/src/routes/error/ErrorPage.js @@ -0,0 +1,40 @@ +/** + * 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 './ErrorPage.scss'; + +function ErrorPage({ error }, context) { + let title = 'Error'; + let content = 'Sorry, a critical error occurred on this page.'; + let errorMessage = null; + + if (error.status === 404) { + title = 'Page Not Found'; + content = 'Sorry, the page you were trying to view does not exist.'; + } else if (process.env.NODE_ENV !== 'production') { + errorMessage = <pre>{error.stack}</pre>; + } + + context.setTitle(title); + + return ( + <div> + <h1>{title}</h1> + <p>{content}</p> + {errorMessage} + </div> + ); +} + +ErrorPage.propTypes = { error: PropTypes.object.isRequired }; +ErrorPage.contextTypes = { setTitle: PropTypes.func.isRequired }; + +export default withStyles(ErrorPage, s); diff --git a/src/routes/error/ErrorPage.scss b/src/routes/error/ErrorPage.scss @@ -0,0 +1,60 @@ +/** + * 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. + */ + +* { + line-height: 1.2; + margin: 0; +} + +html { + color: #888; + display: table; + font-family: sans-serif; + height: 100%; + text-align: center; + width: 100%; +} + +body { + display: table-cell; + vertical-align: middle; + /* stylelint-disable */ + margin: 2em auto; + /* stylelint-enable */ +} + +h1 { + color: #555; + font-size: 2em; + font-weight: 400; +} + +p { + margin: 0 auto; + width: 280px; +} + +pre { + text-align: left; + margin-top: 2rem; +} + +@media only screen and (max-width: 280px) { + + body, + p { + width: 95%; + } + + h1 { + font-size: 1.5em; + margin: 0 0 0.3em; + } + +} diff --git a/src/routes/error/index.js b/src/routes/error/index.js @@ -0,0 +1,27 @@ +/** + * 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 from 'react'; +import App from '../../components/App'; +import ErrorPage from './ErrorPage'; + +export default { + + path: '/error', + + action({ render, context, error }) { + return render( + <App context={context} error={error}> + <ErrorPage error={error} /> + </App>, + error.status || 500 + ); + }, + +}; diff --git a/src/routes/home/Home.js b/src/routes/home/Home.js @@ -11,7 +11,10 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; import s from './Home.scss'; -function Home({ news }) { +const title = 'React Starter Kit'; + +function Home({ news }, context) { + context.setTitle(title); return ( <div className={s.root}> <div className={s.container}> @@ -39,5 +42,6 @@ Home.propTypes = { contentSnippet: PropTypes.string, })).isRequired, }; +Home.contextTypes = { setTitle: PropTypes.func.isRequired }; export default withStyles(Home, s); diff --git a/src/routes/home/index.js b/src/routes/home/index.js @@ -11,10 +11,25 @@ import React from 'react'; import Home from './Home'; import fetch from '../../core/fetch'; -export const path = '/'; -export const action = async (state) => { - const response = await fetch('/graphql?query={news{title,link,contentSnippet}}'); - const { data } = await response.json(); - state.context.onSetTitle('React.js Starter Kit'); - return <Home news={data.news} />; +export default { + + path: '/', + + async action() { + const resp = await fetch('/graphql', { + method: 'post', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: '{news{title,link,contentSnippet}}', + }), + credentials: 'include', + }); + const { data } = await resp.json(); + if (!data || !data.news) throw new Error('Failed to load the news feed.'); + return <Home news={data.news} />; + }, + }; diff --git a/src/routes/index.js b/src/routes/index.js @@ -0,0 +1,42 @@ +/** + * 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 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 error from './error'; + +export default { + + path: '/', + + children: [ + home, + contact, + login, + register, + content, + error, + ], + + async action({ next, render, context }) { + const component = await next(); + if (component === undefined) return component; + return render( + <App context={context}>{component}</App> + ); + }, + +}; diff --git a/src/routes/login/Login.js b/src/routes/login/Login.js @@ -11,7 +11,10 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; import s from './Login.scss'; -function Login({ title }) { +const title = 'Log In'; + +function Login(props, context) { + context.setTitle(title); return ( <div className={s.root}> <div className={s.container}> @@ -112,6 +115,6 @@ function Login({ title }) { ); } -Login.propTypes = { title: PropTypes.string.isRequired }; +Login.contextTypes = { setTitle: PropTypes.func.isRequired }; export default withStyles(Login, s); diff --git a/src/routes/login/index.js b/src/routes/login/index.js @@ -10,9 +10,12 @@ import React from 'react'; import Login from './Login'; -export const path = '/login'; -export const action = async (state) => { - const title = 'Log In'; - state.context.onSetTitle(title); - return <Login title={title} />; +export default { + + path: '/login', + + action() { + return <Login />; + }, + }; diff --git a/src/routes/register/Register.js b/src/routes/register/Register.js @@ -11,7 +11,10 @@ import React, { PropTypes } from 'react'; import withStyles from 'isomorphic-style-loader/lib/withStyles'; import s from './Register.scss'; -function Register({ title }) { +const title = 'New User Registration'; + +function Register(props, context) { + context.setTitle(title); return ( <div className={s.root}> <div className={s.container}> @@ -22,6 +25,6 @@ function Register({ title }) { ); } -Register.propTypes = { title: PropTypes.string.isRequired }; +Register.contextTypes = { setTitle: PropTypes.func.isRequired }; export default withStyles(Register, s); diff --git a/src/routes/register/index.js b/src/routes/register/index.js @@ -10,9 +10,12 @@ import React from 'react'; import Register from './Register'; -export const path = '/register'; -export const action = async (state) => { - const title = 'New User Registration'; - state.context.onSetTitle(title); - return <Register title={title} />; +export default { + + path: '/register', + + action() { + return <Register />; + }, + }; diff --git a/src/server.js b/src/server.js @@ -16,10 +16,11 @@ import expressJwt from 'express-jwt'; import expressGraphQL from 'express-graphql'; import jwt from 'jsonwebtoken'; import ReactDOM from 'react-dom/server'; +import { match } from 'universal-router'; import PrettyError from 'pretty-error'; import passport from './core/passport'; import schema from './data/schema'; -import Router from './routes'; +import routes from './routes'; import assets from './assets'; import { port, auth, analytics } from './config'; @@ -80,6 +81,7 @@ server.use('/graphql', expressGraphQL(req => ({ // ----------------------------------------------------------------------------- server.get('*', async (req, res, next) => { try { + let css = []; let statusCode = 200; const template = require('./views/index.jade'); const data = { title: '', description: '', css: '', body: '', entry: assets.main.js }; @@ -88,17 +90,21 @@ server.get('*', async (req, res, next) => { data.trackingId = analytics.google.trackingId; } - const css = []; - const context = { - insertCss: styles => css.push(styles._getCss()), - onSetTitle: value => (data.title = value), - onSetMeta: (key, value) => (data[key] = value), - onPageNotFound: () => (statusCode = 404), - }; - - await Router.dispatch({ path: req.path, query: req.query, context }, (state, component) => { - data.body = ReactDOM.renderToString(component); - data.css = css.join(''); + await match(routes, { + path: req.path, + query: req.query, + context: { + insertCss: styles => css.push(styles._getCss()), + setTitle: value => (data.title = value), + setMeta: (key, value) => (data[key] = value), + }, + render(component, status = 200) { + css = []; + statusCode = status; + data.body = ReactDOM.renderToString(component); + data.css = css.join(''); + return true; + }, }); res.status(statusCode);