commit 9bf87bfbac2f605f354d122ab9967ee2cfe28b9e
parent 5d12250ea15a100f2f744bb0354adc37e5f7f744
Author: Konstantin Tarkus <hello@tarkus.me>
Date: Thu, 23 Jul 2015 20:19:23 +0300
Update react to v0.14.0-beta1; move routing logic into `/src/router.js`
Diffstat:
21 files changed, 323 insertions(+), 350 deletions(-)
diff --git a/package.json b/package.json
@@ -15,7 +15,9 @@
"jade": "1.11.0",
"lodash": "3.10.0",
"normalize.css": "3.0.3",
- "react": "0.13.3",
+ "react": "^0.14.0-beta1",
+ "react-dom": "^0.14.0-beta1",
+ "react-routing": "0.0.3",
"source-map-support": "0.3.2",
"superagent": "1.2.0"
},
diff --git a/src/actions/.gitignore b/src/actions/.gitignore
diff --git a/src/actions/AppActions.js b/src/actions/AppActions.js
@@ -1,48 +0,0 @@
-/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
-
-import http from 'superagent';
-import { canUseDOM } from 'react/lib/ExecutionEnvironment';
-import Dispatcher from '../core/Dispatcher';
-import ActionTypes from '../constants/ActionTypes';
-
-export default {
-
- navigateTo(path, options) {
- this.loadPage(path, () => {
- if (canUseDOM) {
- if (options && options.replace) {
- window.history.replaceState({}, document.title, path);
- } else {
- window.history.pushState({}, document.title, path);
- }
- }
-
- Dispatcher.dispatch({
- type: ActionTypes.CHANGE_LOCATION,
- path
- });
- });
- },
-
- loadPage(path, cb) {
- Dispatcher.dispatch({
- type: ActionTypes.GET_PAGE,
- path
- });
-
- http.get('/api/query?path=' + encodeURI(path))
- .accept('application/json')
- .end((err, res) => {
- Dispatcher.dispatch({
- type: ActionTypes.RECEIVE_PAGE,
- path,
- err,
- page: res ? res.body : null
- });
- if (cb) {
- cb();
- }
- });
- }
-
-};
diff --git a/src/api/content.js b/src/api/content.js
@@ -0,0 +1,49 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import { join } from 'path';
+import { Router } from 'express';
+import jade from 'jade';
+import fm from 'front-matter';
+import fs from '../utils/fs';
+
+// A folder with Jade/Markdown/HTML content pages
+const CONTENT_DIR = join(__dirname, './content');
+
+// Extract 'front matter' metadata and generate HTML
+const parseJade = (path, jadeContent) => {
+ const content = fm(jadeContent);
+ const html = jade.render(content.body, null, ' ');
+ const page = Object.assign({ path, content: html }, content.attributes);
+ return page;
+};
+
+const router = new Router();
+
+router.get('/', async (req, res, next) => {
+ try {
+ let path = req.query.path;
+
+ if (!path || path === 'undefined') {
+ res.status(400).send({error: `The 'path' query parameter cannot be empty.`});
+ return;
+ }
+
+ let fileName = join(CONTENT_DIR, (path === '/' ? '/index' : path) + '.jade');
+ if (!await fs.exists(fileName)) {
+ fileName = join(CONTENT_DIR, path + '/index.jade');
+ }
+
+ if (!await fs.exists(fileName)) {
+ res.status(404).send({error: `The page '${path}' is not found.`});
+ } else {
+ const source = await fs.readFile(fileName, { encoding: 'utf8' });
+ const content = parseJade(path, source);
+ res.status(200).send(content);
+ }
+ } catch (err) {
+ next(err);
+ }
+});
+
+export default router;
+
diff --git a/src/api/query.js b/src/api/query.js
@@ -1,29 +0,0 @@
-/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
-
-import { Router } from 'express';
-import db from '../core/Database';
-
-const router = new Router();
-
-router.get('/', async (req, res, next) => {
- try {
- let path = req.query.path;
-
- if (!path) {
- res.status(400).send({error: `The 'path' query parameter cannot be empty.`});
- }
-
- let page = await db.getPage(path);
-
- if (page) {
- res.status(200).send(page);
- } else {
- res.status(404).send({error: `The page '${path}' is not found.`});
- }
- } catch (err) {
- next(err);
- }
-});
-
-export default router;
-
diff --git a/src/app.js b/src/app.js
@@ -1,62 +1,61 @@
/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
import 'babel/polyfill';
-import React from 'react';
+import ReactDOM from 'react-dom';
import FastClick from 'fastclick';
-import App from './components/App';
+import router from './router';
import Dispatcher from './core/Dispatcher';
-import AppActions from './actions/AppActions';
+import Location from './core/Location';
import ActionTypes from './constants/ActionTypes';
-let path = decodeURI(window.location.pathname);
-let onSetMeta = (name, content) => {
- // Remove and create a new <meta /> tag in order to make it work
- // with bookmarks in Safari
- let elements = document.getElementsByTagName('meta');
- [].slice.call(elements).forEach((element) => {
- if (element.getAttribute('name') === name) {
- element.parentNode.removeChild(element);
- }
- });
- let meta = document.createElement('meta');
- meta.setAttribute('name', name);
- meta.setAttribute('content', content);
- document.getElementsByTagName('head')[0].appendChild(meta);
+const container = document.getElementById('app');
+const context = {
+ onSetTitle: value => document.title = value,
+ onSetMeta: (name, content) => {
+ // Remove and create a new <meta /> tag in order to make it work
+ // with bookmarks in Safari
+ let elements = document.getElementsByTagName('meta');
+ [].slice.call(elements).forEach((element) => {
+ if (element.getAttribute('name') === name) {
+ element.parentNode.removeChild(element);
+ }
+ });
+ let meta = document.createElement('meta');
+ meta.setAttribute('name', name);
+ meta.setAttribute('content', content);
+ document.getElementsByTagName('head')[0].appendChild(meta);
+ }
};
function run() {
- // Render the top-level React component
- let props = {
- path: path,
- context: {
- onSetTitle: value => document.title = value,
- onSetMeta
- }
- };
- let element = React.createElement(App, props);
- React.render(element, document.getElementById('app'), () => {
- let css = document.getElementById('css');
- css.parentNode.removeChild(css);
+ router.dispatch({ path: window.location.pathname, context }, (state, component) => {
+ ReactDOM.render(component, container, () => {
+ let css = document.getElementById('css');
+ css.parentNode.removeChild(css);
+ });
});
- // Update `Application.path` prop when `window.location` is changed
- Dispatcher.register((action) => {
+ Dispatcher.register(action => {
if (action.type === ActionTypes.CHANGE_LOCATION) {
- element = React.cloneElement(element, {path: action.path});
- React.render(element, document.getElementById('app'));
+ router.dispatch({ path: action.path, context }, (state, component) => {
+ ReactDOM.render(component, container);
+ });
}
});
}
+function handlePopState(event) {
+ Location.navigateTo(window.location.pathname, { replace: !!event.state });
+}
+
// Run the application when both DOM is ready
// and page content is loaded
-Promise.all([
- new Promise((resolve) => {
- if (window.addEventListener) {
- window.addEventListener('DOMContentLoaded', resolve);
- } else {
- window.attachEvent('onload', resolve);
- }
- }).then(() => FastClick.attach(document.body)),
- new Promise((resolve) => AppActions.loadPage(path, resolve))
-]).then(run);
+new Promise(resolve => {
+ if (window.addEventListener) {
+ window.addEventListener('DOMContentLoaded', resolve);
+ window.addEventListener('popstate', handlePopState);
+ } else {
+ window.attachEvent('onload', resolve);
+ window.attachEvent('popstate', handlePopState);
+ }
+}).then(() => FastClick.attach(document.body)).then(run);
diff --git a/src/components/App/App.js b/src/components/App/App.js
@@ -4,76 +4,28 @@ import React, { PropTypes } from 'react';
import styles from './App.less';
import withContext from '../../decorators/withContext';
import withStyles from '../../decorators/withStyles';
-import AppActions from '../../actions/AppActions';
-import AppStore from '../../stores/AppStore';
import Header from '../Header';
-import ContentPage from '../ContentPage';
-import ContactPage from '../ContactPage';
-import LoginPage from '../LoginPage';
-import RegisterPage from '../RegisterPage';
-import NotFoundPage from '../NotFoundPage';
import Feedback from '../Feedback';
import Footer from '../Footer';
-const pages = { ContentPage, ContactPage, LoginPage, RegisterPage, NotFoundPage };
-
@withContext
@withStyles(styles)
class App {
static propTypes = {
- path: PropTypes.string.isRequired
+ children: PropTypes.element.isRequired,
+ error: PropTypes.object
};
- componentDidMount() {
- window.addEventListener('popstate', this.handlePopState);
- }
-
- componentWillUnmount() {
- window.removeEventListener('popstate', this.handlePopState);
- }
-
- shouldComponentUpdate(nextProps) {
- return this.props.path !== nextProps.path;
- }
-
render() {
- let component;
-
- switch (this.props.path) {
-
- case '/':
- case '/about':
- case '/privacy':
- let page = AppStore.getPage(this.props.path);
- component = React.createElement(pages[page.component], page);
- break;
-
- case '/contact':
- component = <ContactPage />;
- break;
-
- case '/login':
- component = <LoginPage />;
- break;
-
- case '/register':
- component = <RegisterPage />;
- break;
- }
-
- return component ? (
+ return !this.props.error ? (
<div>
<Header />
- {component}
+ {this.props.children}
<Feedback />
<Footer />
</div>
- ) : <NotFoundPage />;
- }
-
- handlePopState(event) {
- AppActions.navigateTo(window.location.pathname, {replace: !!event.state});
+ ) : this.props.children;
}
}
diff --git a/src/components/ErrorPage/ErrorPage.js b/src/components/ErrorPage/ErrorPage.js
@@ -0,0 +1,28 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import React, { PropTypes } from 'react';
+import withStyles from '../../decorators/withStyles';
+import styles from './ErrorPage.less';
+
+@withStyles(styles)
+class ErrorPage {
+
+ static contextTypes = {
+ onSetTitle: PropTypes.func.isRequired,
+ onPageNotFound: PropTypes.func.isRequired
+ };
+
+ render() {
+ let title = 'Error';
+ this.context.onSetTitle(title);
+ return (
+ <div>
+ <h1>{title}</h1>
+ <p>Sorry, an critical error occurred on this page.</p>
+ </div>
+ );
+ }
+
+}
+
+export default ErrorPage;
diff --git a/src/components/ErrorPage/ErrorPage.less b/src/components/ErrorPage/ErrorPage.less
@@ -0,0 +1,42 @@
+/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+* {
+ margin: 0;
+ line-height: 1.2;
+}
+
+html {
+ display: table;
+ width: 100%;
+ height: 100%;
+ color: #888;
+ text-align: center;
+ font-family: sans-serif;
+}
+
+body {
+ display: table-cell;
+ margin: 2em auto;
+ vertical-align: middle;
+}
+
+h1 {
+ color: #555;
+ font-weight: 400;
+ font-size: 2em;
+}
+
+p {
+ margin: 0 auto;
+ width: 280px;
+}
+
+@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
@@ -0,0 +1,6 @@
+{
+ "name": "ErrorPage",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./ErrorPage.js"
+}
diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js
@@ -3,7 +3,5 @@
import keyMirror from 'react/lib/keyMirror';
export default keyMirror({
- GET_PAGE: null,
- RECEIVE_PAGE: null,
CHANGE_LOCATION: null
});
diff --git a/src/core/Database.js b/src/core/Database.js
@@ -1,53 +0,0 @@
-/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
-
-import fs from 'fs';
-import path from 'path';
-import jade from 'jade';
-import fm from 'front-matter';
-import Dispatcher from './Dispatcher';
-import ActionTypes from '../constants/ActionTypes';
-
-// A folder with Jade/Markdown/HTML content pages
-const CONTENT_DIR = path.join(__dirname, './content');
-
-// Check if that directory exists, print an error message if not
-fs.exists(CONTENT_DIR, (exists) => {
- if (!exists) {
- console.error(`Error: Directory '${CONTENT_DIR}' does not exist.`);
- }
-});
-
-// Extract 'front matter' metadata and generate HTML
-function parseJade(uri, jadeContent) {
- let content = fm(jadeContent);
- let html = jade.render(content.body, null, ' ');
- let page = Object.assign({path: uri, content: html}, content.attributes);
- return page;
-}
-
-export default {
-
- getPage: (uri) => {
- // Read page content from a Jade file
- return new Promise((resolve) => {
- let fileName = path.join(CONTENT_DIR, (uri === '/' ? '/index' : uri) + '.jade');
- fs.readFile(fileName, {encoding: 'utf8'}, (err, data) => {
- if (err) {
- fileName = path.join(CONTENT_DIR, uri + '/index.jade');
- fs.readFile(fileName, {encoding: 'utf8'}, (err2, data2) => {
- resolve(err2 ? null : parseJade(uri, data2));
- });
- } else {
- resolve(parseJade(uri, data));
- }
- });
- }).then((page) => {
- Dispatcher.dispatch({
- type: ActionTypes.RECEIVE_PAGE,
- page: page});
- return Promise.resolve(page);
- });
- }
-
-};
-
diff --git a/src/core/Location.js b/src/core/Location.js
@@ -0,0 +1,24 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import { canUseDOM } from 'react/lib/ExecutionEnvironment';
+import Dispatcher from '../core/Dispatcher';
+import ActionTypes from '../constants/ActionTypes';
+
+export default {
+
+ navigateTo(path, options) {
+ if (canUseDOM) {
+ if (options && options.replace) {
+ window.history.replaceState({}, document.title, path);
+ } else {
+ window.history.pushState({}, document.title, path);
+ }
+ }
+
+ Dispatcher.dispatch({
+ type: ActionTypes.CHANGE_LOCATION,
+ path
+ });
+ }
+
+};
diff --git a/src/core/http.js b/src/core/http.js
@@ -0,0 +1,35 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import request from 'superagent';
+import ExecutionEnvironment from 'react/lib/ExecutionEnvironment';
+
+const getBaseUrl = (() => {
+ let baseUrl;
+ return () => baseUrl || (baseUrl = ExecutionEnvironment.canUseDOM ? '' :
+ process.env.WEBSITE_HOSTNAME ?
+ `http://${process.env.WEBSITE_HOSTNAME}` :
+ `http://127.0.0.1:${global.server.get('port')}`);
+})();
+
+const http = {
+
+ get: path => new Promise((resolve, reject) => {
+ request
+ .get(getBaseUrl() + path)
+ .accept('application/json')
+ .end((err, res) => {
+ if (err) {
+ if (err.status === 404) {
+ resolve(null);
+ } else {
+ reject(err);
+ }
+ } else {
+ resolve(res.body);
+ }
+ });
+ })
+
+};
+
+export default http;
diff --git a/src/router.js b/src/router.js
@@ -0,0 +1,38 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import React from 'react';
+import Router from 'react-routing/src/Router';
+import http from './core/http';
+import App from './components/App';
+import ContentPage from './components/ContentPage';
+import ContactPage from './components/ContactPage';
+import LoginPage from './components/LoginPage';
+import RegisterPage from './components/RegisterPage';
+import NotFoundPage from './components/NotFoundPage';
+import ErrorPage from './components/ErrorPage';
+
+const router = new Router(on => {
+
+ on('*', async (state, next) => {
+ const component = await next();
+ return component && <App context={state.context}>{component}</App>;
+ });
+
+ on('/contact', async () => <ContactPage />);
+
+ on('/login', async () => <LoginPage />);
+
+ on('/register', async () => <RegisterPage />);
+
+ on('*', async (state) => {
+ const content = await http.get(`/api/content?path=${state.path}`);
+ return content && <ContentPage {...content} />;
+ });
+
+ on('error', (state, error) => state.statusCode === 404 ?
+ <App context={state.context} error={error}><NotFoundPage /></App> :
+ <App context={state.context} error={error}><ErrorPage /></App>);
+
+});
+
+export default router;
diff --git a/src/server.js b/src/server.js
@@ -5,13 +5,10 @@ import _ from 'lodash';
import fs from 'fs';
import path from 'path';
import express from 'express';
-import React from 'react';
-import './core/Dispatcher';
-import './stores/AppStore';
-import db from './core/Database';
-import App from './components/App';
+import ReactDOM from 'react-dom/server';
+import router from './router';
-const server = express();
+const server = global.server = express();
server.set('port', (process.env.PORT || 5000));
server.use(express.static(path.join(__dirname, 'public')));
@@ -19,7 +16,7 @@ server.use(express.static(path.join(__dirname, 'public')));
//
// Register API middleware
// -----------------------------------------------------------------------------
-server.use('/api/query', require('./api/query'));
+server.use('/api/content', require('./api/content'));
//
// Register server-side rendering middleware
@@ -31,29 +28,23 @@ const template = _.template(fs.readFileSync(templateFile, 'utf8'));
server.get('*', async (req, res, next) => {
try {
- // TODO: Temporary fix #159
- if (['/', '/about', '/privacy'].indexOf(req.path) !== -1) {
- await db.getPage(req.path);
- }
- let notFound = false;
- let css = [];
- let data = {description: ''};
- let app = (<App
- path={req.path}
- context={{
- onInsertCss: value => css.push(value),
- onSetTitle: value => data.title = value,
- onSetMeta: (key, value) => data[key] = value,
- onPageNotFound: () => notFound = true
- }} />);
-
- data.body = React.renderToString(app);
- data.css = css.join('');
- let html = template(data);
- if (notFound) {
- res.status(404);
- }
- res.send(html);
+ let statusCode = 200;
+ const data = { title: '', description: '', css: '', body: '' };
+ const css = [];
+ const context = {
+ onInsertCss: value => css.push(value),
+ onSetTitle: value => data.title = value,
+ onSetMeta: (key, value) => data[key] = value,
+ onPageNotFound: () => statusCode = 404
+ };
+
+ await router.dispatch({ path: req.path, context }, (state, component) => {
+ data.body = ReactDOM.renderToString(component);
+ data.css = css.join('');
+ });
+
+ const html = template(data);
+ res.status(statusCode).send(html);
} catch (err) {
next(err);
}
diff --git a/src/stores/.gitignore b/src/stores/.gitignore
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
@@ -1,80 +0,0 @@
-/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
-
-import EventEmitter from 'eventemitter3';
-import Dispatcher from '../core/Dispatcher';
-import ActionTypes from '../constants/ActionTypes';
-
-const CHANGE_EVENT = 'change';
-
-var pages = {};
-var loading = false;
-
-var AppStore = Object.assign({}, EventEmitter.prototype, {
-
- isLoading() {
- return loading;
- },
-
- /**
- * Gets page data by the given URL path.
- *
- * @param {String} path URL path.
- * @returns {*} Page data.
- */
- getPage(path) {
- return path in pages ? pages[path] : null;
- },
-
- /**
- * Emits change event to all registered event listeners.
- *
- * @returns {Boolean} Indication if we've emitted an event.
- */
- emitChange() {
- return this.emit(CHANGE_EVENT);
- },
-
- /**
- * Register a new change event listener.
- *
- * @param {function} callback Callback function.
- */
- onChange(callback) {
- this.on(CHANGE_EVENT, callback);
- },
-
- /**
- * Remove change event listener.
- *
- * @param {function} callback Callback function.
- */
- off(callback) {
- this.removeListener(CHANGE_EVENT, callback);
- }
-
-});
-
-AppStore.dispatchToken = Dispatcher.register((action) => {
-
- switch (action.type) {
-
- case ActionTypes.GET_PAGE:
- loading = true;
- AppStore.emitChange();
- break;
-
- case ActionTypes.RECEIVE_PAGE:
- loading = false;
- if (!action.err) {
- pages[action.page.path] = action.page;
- }
- AppStore.emitChange();
- break;
-
- default:
- // Do nothing
- }
-
-});
-
-export default AppStore;
diff --git a/src/utils/Link.js b/src/utils/Link.js
@@ -1,7 +1,7 @@
/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
import invariant from 'react/lib/invariant';
-import AppActions from '../actions/AppActions';
+import Location from '../core/Location';
function handleClick(event) {
@@ -23,7 +23,7 @@ function handleClick(event) {
var path = el.pathname + el.search + (el.hash || '');
event.preventDefault();
- AppActions.navigateTo(path);
+ Location.navigateTo(path);
}
export default { handleClick };
diff --git a/src/utils/fs.js b/src/utils/fs.js
@@ -0,0 +1,19 @@
+/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */
+
+import fs from 'fs';
+
+const exists = filename => new Promise(resolve => {
+ fs.exists(filename, resolve);
+});
+
+const readFile = filename => new Promise((resolve, reject) => {
+ fs.readFile(filename, 'utf8', (err, data) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(data);
+ }
+ });
+});
+
+export default { exists, readFile };
diff --git a/webpack.config.js b/webpack.config.js
@@ -6,6 +6,7 @@
* LICENSE.txt file in the root directory of this source tree.
*/
+import path from 'path';
import webpack, { DefinePlugin, BannerPlugin } from 'webpack';
import merge from 'lodash/object/merge';
import autoprefixer from 'autoprefixer-core';
@@ -58,14 +59,6 @@ const config = {
},
module: {
- preLoaders: [
- {
- test: /\.js$/,
- exclude: /node_modules/,
- loader: 'eslint-loader'
- }
- ],
-
loaders: [
{
test: /\.css$/,
@@ -93,12 +86,19 @@ const config = {
},
{
test: /\.jsx?$/,
- exclude: /node_modules/,
+ include: [
+ path.resolve(__dirname, 'node_modules/react-routing'),
+ path.resolve(__dirname, 'src')
+ ],
loader: 'babel-loader'
}
]
},
+ resolveLoader: {
+ root: path.join(__dirname, 'node_modules')
+ },
+
postcss: [autoprefixer(AUTOPREFIXER_BROWSERS)]
};