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:
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);