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