commit 10a0e82dc72677ed22637688622d6a2dc7c652be
parent 88c98e7f3594ed84a64a9dd2d2f747f7523a48a4
Author: Konstantin Tarkus <hello@tarkus.me>
Date: Fri, 16 Jan 2015 10:55:20 +0300
Add server-side rendering with Node.js/Express; big refactoring
- Add Node.js/Express web server (see ./src/server.js)
- Remove Flux store base class
- Remove PageStore in favor of AppStore
- Refactor AppStore to use EventEmitter3 (see ./src/stores/AppStore.js)
- Refactor `serve` Gulp task to use nodemon
- Add __SERVER__ env variable
- Move HTML template for React component(s) to ./src/index.html
- Refactor client-side startup script (see ./src/app.js)
- Add CHANGE_LOCATION, LOAD_PAGE action types
- Add NavigationMixin to be used in the top-level component
- Remove Index.js, Privacy.js React components
- Add HomePage, ContentPage, NotFoundPage, ErrorPage React components
- Replace <Link> with <a>
- Remove PageActions, RouteActions in favor of AppActions
Diffstat:
39 files changed, 888 insertions(+), 677 deletions(-)
diff --git a/.jshintrc b/.jshintrc
@@ -15,6 +15,7 @@
"globals": {
"require": false,
"__dirname": false,
- "__DEV__": false
+ "__DEV__": false,
+ "__SERVER__": false
}
}
diff --git a/config/webpack.js b/config/webpack.js
@@ -39,14 +39,18 @@ module.exports = function(release) {
plugins: release ? [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
- '__DEV__': false
+ '__DEV__': false,
+ '__SERVER__': false
}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.UglifyJsPlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.AggressiveMergingPlugin()
] : [
- new webpack.DefinePlugin({'__DEV__': true})
+ new webpack.DefinePlugin({
+ '__DEV__': true,
+ '__SERVER__': false
+ })
],
resolve: {
diff --git a/gulpfile.js b/gulpfile.js
@@ -14,14 +14,9 @@ var gulp = require('gulp');
var $ = require('gulp-load-plugins')();
var del = require('del');
var path = require('path');
-var merge = require('merge-stream');
var runSequence = require('run-sequence');
var webpack = require('webpack');
-var browserSync = require('browser-sync');
var pagespeed = require('psi');
-var fs = require('fs');
-var url = require('url');
-var ReactTools = require('react-tools');
var argv = require('minimist')(process.argv.slice(2));
// Settings
@@ -42,30 +37,6 @@ var AUTOPREFIXER_BROWSERS = [ // https://github.com/ai/autoprefi
var src = {};
var watch = false;
-var pkgs = (function() {
- var pkgs = {};
- var map = function(source) {
- for (var key in source) {
- pkgs[key.replace(/[^a-z0-9]/gi, '')] = source[key].substring(1);
- }
- };
- map(require('./package.json').dependencies);
- return pkgs;
-}());
-
-// Configure JSX Harmony transform in order to be able
-// require .js files with JSX (see 'pages' task)
-var originalJsTransform = require.extensions['.js'];
-var reactTransform = function(module, filename) {
- if (filename.indexOf('node_modules') === -1) {
- var src = fs.readFileSync(filename, {encoding: 'utf8'});
- src = ReactTools.transform(src, {harmony: true, stripTypes: true});
- module._compile(src, filename);
- } else {
- originalJsTransform(module, filename);
- }
-};
-require.extensions['.js'] = reactTransform;
// The default task
gulp.task('default', ['serve']);
@@ -75,12 +46,8 @@ gulp.task('clean', del.bind(null, [DEST]));
// 3rd party libraries
gulp.task('vendor', function() {
- return merge(
- gulp.src('./node_modules/jquery/dist/**')
- .pipe(gulp.dest(DEST + '/vendor/jquery-' + pkgs.jquery)),
- gulp.src('./node_modules/bootstrap/dist/fonts/**')
- .pipe(gulp.dest(DEST + '/fonts'))
- );
+ return gulp.src('./node_modules/bootstrap/dist/fonts/**')
+ .pipe(gulp.dest(DEST + '/fonts'));
});
// Static files
@@ -105,42 +72,6 @@ gulp.task('images', function() {
.pipe($.size({title: 'images'}));
});
-// HTML pages
-gulp.task('pages', function() {
- src.pages = ['src/components/pages/**/*.js', 'src/components/pages/404.html'];
-
- var currentPage = {};
- var Dispatcher = require('./src/core/Dispatcher');
- var ActionTypes = require('./src/constants/ActionTypes');
-
- // Capture document.title and other page metadata changes
- Dispatcher.register(function(payload) {
- if (payload.action.actionType == ActionTypes.SET_CURRENT_PAGE)
- {
- currentPage = payload.action.page;
- }
- return true;
- });
-
- var render = $.render({
- template: './src/components/pages/index.html',
- data: function() { return currentPage; }
- })
- .on('error', function(err) { console.log(err); render.end(); });
-
- return gulp.src(src.pages)
- .pipe($.changed(DEST, {extension: '.html'}))
- .pipe($.if('*.js', render))
- .pipe($.replace('UA-XXXXX-X', GOOGLE_ANALYTICS_ID))
- .pipe($.if(RELEASE, $.htmlmin({
- removeComments: true,
- collapseWhitespace: true,
- minifyJS: true
- }), $.jsbeautifier()))
- .pipe(gulp.dest(DEST))
- .pipe($.size({title: 'pages'}));
-});
-
// CSS style sheets
gulp.task('styles', function() {
src.styles = 'src/styles/**/*.{css,less}';
@@ -188,51 +119,54 @@ gulp.task('bundle', function(cb) {
// Build the app from source code
gulp.task('build', ['clean'], function(cb) {
- runSequence(['vendor', 'assets', 'images', 'pages', 'styles', 'bundle'], cb);
+ runSequence(['vendor', 'assets', 'images', 'styles', 'bundle'], cb);
});
// Launch a lightweight HTTP Server
gulp.task('serve', function(cb) {
+ var nodemon = require('nodemon');
+ var browserSync = require('browser-sync');
+
watch = true;
runSequence('build', function() {
- browserSync({
- notify: false,
- // Customize the BrowserSync console logging prefix
- logPrefix: 'RSK',
- // Run as an https by uncommenting 'https: true'
- // Note: this uses an unsigned certificate which on first access
- // will present a certificate warning in the browser.
- // https: true,
- server: {
- baseDir: DEST,
- // Allow web page requests without .html file extension in URLs
- middleware: function(req, res, cb) {
- var uri = url.parse(req.url);
- if (uri.pathname.length > 1 &&
- uri.pathname.lastIndexOf('/browser-sync/', 0) !== 0 &&
- !fs.existsSync(DEST + uri.pathname)) {
- if (fs.existsSync(DEST + uri.pathname + '.html')) {
- req.url = uri.pathname + '.html' + (uri.search || '');
- } else {
- res.statusCode = 404;
- req.url = '/404.html' + (uri.search || '');
- }
- }
- cb();
- }
- }
- });
- gulp.watch(src.assets, ['assets']);
- gulp.watch(src.images, ['images']);
- gulp.watch(src.pages, ['pages']);
- gulp.watch(src.styles, ['styles']);
- gulp.watch(DEST + '/**/*.*', function(file) {
- browserSync.reload(path.relative(__dirname, file.path));
+ var server = require('nodemon')({
+ script: 'src/server.js',
+ watch: [path.join(__dirname, 'src/**/*.js')],
+ env: {NODE_ENV: 'development'}
+ }).on('log', function(log) {
+ $.util.log('nodemon', $.util.colors.green(log.message));
+ }).on('crash', function() {
+ $.util.log($.util.colors.red('nodemon crashed'));
+ }).once('start', function() {
+ browserSync({
+ notify: false,
+ // Customize the BrowserSync console logging prefix
+ logPrefix: 'QC',
+ // Run as an https by setting 'https: true'
+ // Note: this uses an unsigned certificate which on first access
+ // will present a certificate warning in the browser.
+ https: false,
+ // Informs browser-sync to proxy our Express app which would run
+ // at the following location
+ proxy: 'http://localhost:5000'
+ });
+
+ process.on('exit', function () {
+ browserSync.exit();
+ server.emit('exit');
+ });
+
+ gulp.watch(src.assets, ['assets']);
+ gulp.watch(src.images, ['images']);
+ gulp.watch(src.styles, ['styles']);
+ gulp.watch(DEST + '/**/*.*', function (file) {
+ browserSync.reload(path.relative(__dirname, file.path));
+ });
+ cb();
});
- cb();
});
});
diff --git a/package.json b/package.json
@@ -6,10 +6,16 @@
"repository": "https://github.com/kriasoft/react-starter-kit",
"license": "MIT",
"dependencies": {
- "bootstrap": "^3.3.1",
- "director": "^1.2.7",
- "flux": "^2.0.1",
- "react": "^0.12.2"
+ "bootstrap": "3.3.1",
+ "director": "1.2.7",
+ "eventemitter3": "0.1.6",
+ "express": "4.10.7",
+ "flux": "2.0.1",
+ "front-matter": "0.2.1",
+ "jade": "1.9.0",
+ "lodash": "2.4.1",
+ "react": "0.12.2",
+ "superagent": "0.21.0"
},
"devDependencies": {
"autoprefixer-loader": "^1.1.0",
@@ -44,8 +50,8 @@
"jsx-loader": "^0.12.2",
"less": "^2.2.0",
"less-loader": "^2.0.0",
- "merge-stream": "^0.1.7",
"minimist": "^1.1.0",
+ "nodemon": "^1.2.1",
"protractor": "^1.6.0",
"psi": "^1.0.4",
"react-tools": "^0.12.2",
diff --git a/src/actions/AppActions.js b/src/actions/AppActions.js
@@ -0,0 +1,45 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var Dispatcher = require('../core/Dispatcher');
+var ActionTypes = require('../constants/ActionTypes');
+var ExecutionEnvironment = require('react/lib/ExecutionEnvironment');
+var http = require('superagent');
+
+module.exports = {
+
+ navigateTo(path) {
+ if (ExecutionEnvironment.canUseDOM) {
+ window.history.pushState({}, document.title, path);
+ }
+
+ Dispatcher.handleViewAction({
+ actionType: ActionTypes.CHANGE_LOCATION, path: path
+ });
+ },
+
+ loadPage(path, cb) {
+ Dispatcher.handleViewAction({
+ actionType: ActionTypes.LOAD_PAGE, path: path
+ });
+
+ http.get('/api/page' + path)
+ .accept('application/json')
+ .end((err, res) => {
+ Dispatcher.handleServerAction({
+ actionType: ActionTypes.LOAD_PAGE, path: path, err: err, page: res
+ });
+ if (cb) {
+ cb();
+ }
+ });
+ }
+
+};
diff --git a/src/actions/PageActions.js b/src/actions/PageActions.js
@@ -1,29 +0,0 @@
-/*
- * React.js Starter Kit
- * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-'use strict';
-
-var Dispatcher = require('../core/Dispatcher');
-var ActionTypes = require('../constants/ActionTypes');
-var pageDefaults = require('../constants/Settings').defaults.page;
-var assign = require('react/lib/Object.assign');
-
-module.exports = {
-
- /**
- * Set metadata for the current page (title, description, keywords etc.).
- * @param {object} The page object.
- */
- set(page) {
- Dispatcher.handleViewAction({
- actionType: ActionTypes.SET_CURRENT_PAGE,
- page: assign({}, pageDefaults, page)
- });
- }
-
-};
diff --git a/src/actions/RouteActions.js b/src/actions/RouteActions.js
@@ -1,27 +0,0 @@
-/*
- * React.js Starter Kit
- * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-'use strict';
-
-var Dispatcher = require('../core/Dispatcher');
-var ActionTypes = require('../constants/ActionTypes');
-
-module.exports = {
-
- /**
- * Set the current route.
- * @param {string} route Supply a route value, such as `todos/completed`.
- */
- setRoute(route) {
- Dispatcher.handleViewAction({
- actionType: ActionTypes.SET_CURRENT_ROUTE,
- route: route
- });
- }
-
-};
diff --git a/src/app.js b/src/app.js
@@ -9,54 +9,56 @@
'use strict';
var React = require('react');
-var ExecutionEnvironment = require('react/lib/ExecutionEnvironment');
-var {Router} = require('director');
+var App = require('./components/App');
var Dispatcher = require('./core/Dispatcher');
+var AppActions = require('./actions/AppActions');
var ActionTypes = require('./constants/ActionTypes');
-var router;
// Export React so the dev tools can find it
(window !== window.top ? window.top : window).React = React;
-Dispatcher.register((payload) => {
-
- var action = payload.action;
-
- switch (action.actionType)
- {
- case ActionTypes.SET_CURRENT_ROUTE:
- router.setRoute(action.route);
- break;
-
- case ActionTypes.SET_CURRENT_PAGE:
- if (ExecutionEnvironment.canUseDOM) {
- document.title = action.page.title;
+// Initial properties and callbacks
+// which should be passed into the top-level React component (App)
+var props = {
+ path: decodeURI(window.location.pathname),
+ onSetTitle: (title) => {
+ document.title = title;
+ },
+ onSetMeta: (name, content) => {
+ // Remove and create a new <meta /> tag in order to make it work
+ // with bookmarks in Safari
+ var elements = document.getElementsByTagName('meta');
+ [].slice.call(elements).forEach((element) => {
+ if (element.getAttribute('name') === name) {
+ element.parentNode.removeChild(element);
}
- break;
- }
+ });
+ var meta = document.createElement('meta');
+ meta.setAttribute('name', name);
+ meta.setAttribute('content', content);
+ document.getElementsByTagName('head')[0].appendChild(meta);
+ },
+ onPageNotFound: () => { /* do nothing */ }
+};
- return true; // No errors. Needed by promise in Dispatcher.
-});
+// Render application when DOM is ready
+function startup() {
+ // Render the top-level React component and mount it to the `document.body`
+ var app = React.render(React.createElement(App, props), document.body);
-/**
- * Check if Page component has a layout property; and if yes, wrap the page
- * into the specified layout, then mount to document.body.
- */
-function render(page) {
- var layout = null, child = null, props = {};
- while ((layout = page.type.layout || (page.defaultProps && page.defaultProps.layout))) {
- child = React.createElement(page, props, child);
- page = layout;
- }
- React.render(React.createElement(page, props, child), document.body);
+ // Set Application.path property when `window.location` is changed
+ Dispatcher.register((payload) => {
+ if (payload.action.actionType === ActionTypes.CHANGE_LOCATION) {
+ app.setProps({path: decodeURI(payload.action.path)});
+ }
+ });
}
-// Define URL routes
-// See https://github.com/flatiron/director
-var routes = {
- '/': () => render(require('./components/pages/Index')),
- '/privacy': () => render(require('./components/pages/Privacy'))
-};
-
-// Initialize a router
-router = new Router(routes).configure({html5history: true}).init();
+// Load page content
+AppActions.loadPage(props.path, () => {
+ if (window.addEventListener) {
+ window.addEventListener('DOMContentLoaded', startup, false);
+ } else {
+ window.attachEvent('onload', startup);
+ }
+});
diff --git a/src/components/App/App.js b/src/components/App/App.js
@@ -0,0 +1,93 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+require('./App.less');
+
+var React = require('react');
+var ExecutionEnvironment = require('react/lib/ExecutionEnvironment');
+var AppActions = require('../../actions/AppActions');
+var NavigationMixin = require('./NavigationMixin');
+var AppStore = require('../../stores/AppStore');
+var Navbar = require('../Navbar');
+var ContentPage = require('../ContentPage');
+var NotFoundPage = require('../NotFoundPage');
+
+var Application = React.createClass({
+
+ mixins: [NavigationMixin],
+
+ propTypes: {
+ path: React.PropTypes.string.isRequired,
+ onSetTitle: React.PropTypes.func.isRequired,
+ onSetMeta: React.PropTypes.func.isRequired,
+ onPageNotFound: React.PropTypes.func.isRequired
+ },
+
+ getInitialState() {
+ return {loading: false};
+ },
+
+ componentWillMount() {
+ if (ExecutionEnvironment.canUseDOM) {
+ this.setState({loading: true});
+ AppActions.loadPage(this.props.path, () => {
+ this.setState({loading: false});
+ });
+ }
+ },
+
+ render() {
+ var page = AppStore.getPage(this.props.path);
+
+ if (page === undefined) {
+ return false;
+ }
+
+ this.props.onSetTitle(page.title);
+
+ if (page.type === 'notfound') {
+ this.props.onPageNotFound();
+ return React.createElement(NotFoundPage, page);
+ }
+
+ return (
+ /* jshint ignore:start */
+ <div className="App">
+ <Navbar />
+ {
+ this.props.path === '/' ?
+ <div className="jumbotron">
+ <div className="container text-center">
+ <h1>React</h1>
+ <p>Complex web apps made easy</p>
+ </div>
+ </div> :
+ <div className="container">
+ <h2>{page.title}</h2>
+ </div>
+ }
+ <ContentPage className="container" {...page} />
+ <div className="navbar-footer">
+ <div className="container">
+ <p className="text-muted">
+ <span>© KriaSoft</span>
+ <span><a href="/">Home</a></span>
+ <span><a href="/privacy">Privacy</a></span>
+ </p>
+ </div>
+ </div>
+ </div>
+ /* jshint ignore:end */
+ );
+ }
+
+});
+
+module.exports = Application;
diff --git a/src/components/App/App.less b/src/components/App/App.less
@@ -0,0 +1,9 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+.App {}
diff --git a/src/components/App/NavigationMixin.js b/src/components/App/NavigationMixin.js
@@ -0,0 +1,93 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var React = require('react');
+var ExecutionEnvironment = require('react/lib/ExecutionEnvironment');
+var AppActions = require('../../actions/AppActions');
+
+var NavigationMixin = {
+
+ componentDidMount() {
+ if (ExecutionEnvironment.canUseDOM) {
+ window.addEventListener('popstate', this.handlePopState);
+ window.addEventListener('click', this.handleClick);
+ }
+ },
+
+ componentWillUnmount() {
+ window.removeEventListener('popstate', this.handlePopState);
+ window.removeEventListener('click', this.handleClick);
+ },
+
+ handlePopState(event) {
+ console.log('Application.handlePopState(' + (event.state ? event.state.path : '') + ')');
+ if (event.state) {
+ var path = event.state.path;
+ // TODO: Replace current location
+ // replace(path, event.state);
+ } else {
+ AppActions.navigateTo(window.location.pathname);
+ }
+ },
+
+ handleClick(event) {
+ if (event.button === 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.defaultPrevented) {
+ return;
+ }
+
+ // Ensure link
+ var el = event.target;
+ while (el && el.nodeName !== 'A') {
+ el = el.parentNode;
+ }
+ if (!el || el.nodeName !== 'A') {
+ return;
+ }
+
+ // Ignore if tag has
+ // 1. "download" attribute
+ // 2. rel="external" attribute
+ if (el.getAttribute('download') || el.getAttribute('rel') === 'external') {
+ return;
+ }
+
+ // Ensure non-hash for the same path
+ var link = el.getAttribute('href');
+ if (el.pathname === location.pathname && (el.hash || '#' === link)) {
+ return;
+ }
+
+ // Check for mailto: in the href
+ if (link && link.indexOf('mailto:') > -1) {
+ return;
+ }
+
+ // Check target
+ if (el.target) {
+ return;
+ }
+
+ // X-origin
+ var origin = window.location.protocol + '//' + window.location.hostname +
+ (window.location.port ? ':' + window.location.port : '');
+ if (!(el.href && el.href.indexOf(origin) === 0)) {
+ return;
+ }
+
+ // Rebuild path
+ var path = el.pathname + el.search + (el.hash || '');
+
+ event.preventDefault();
+ AppActions.navigateTo(path);
+ }
+
+};
+
+module.exports = NavigationMixin;
diff --git a/src/components/App/package.json b/src/components/App/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "App",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./App.js"
+}
diff --git a/src/components/Application/Application.js b/src/components/Application/Application.js
@@ -1,85 +0,0 @@
-/*
- * React.js Starter Kit
- * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-'use strict';
-
-require('./Application.less');
-
-var React = require('react');
-var PageStore = require('../../stores/PageStore');
-var Link = require('../Link');
-var Navbar = require('../Navbar');
-
-/**
- * Retrieves the current page metadata from the PageStore.
- * @returns {{title: string}}
- */
-function getState() {
- return {
- title: PageStore.get().title
- };
-}
-
-var DefaultLayout = React.createClass({
-
- mixins: [PageStore.Mixin],
-
- getInitialState() {
- return getState();
- },
-
- componentDidMount() {
- PageStore.emitChange();
- },
-
- render() {
- /* jshint ignore:start */
- var header = this.props.children.type.breadcrumb ? (
- <div className="container">
- <h2>{this.state.title}</h2>
- {this.props.children.type.breadcrumb}
- </div>
- ) : (
- <div className="jumbotron">
- <div className="container text-center">
- <h1>React</h1>
- <p>Complex web apps made easy</p>
- </div>
- </div>
- );
- /* jshint ignore:end */
-
- return (
- /* jshint ignore:start */
- <div>
- <Navbar />
- {header}
- {this.props.children}
- <div className="navbar-footer">
- <div className="container">
- <p className="text-muted">
- <span>© KriaSoft</span>
- <span><Link to="/">Home</Link></span>
- <span><Link to="/privacy">Privacy</Link></span>
- </p>
- </div>
- </div>
- </div>
- /* jshint ignore:end */
- );
- },
-
- /**
- * Event handler for 'change' events coming from the PageStore.
- */
- onChange() {
- this.setState(getState());
- }
-});
-
-module.exports = DefaultLayout;
diff --git a/src/components/Application/Application.less b/src/components/Application/Application.less
diff --git a/src/components/Application/package.json b/src/components/Application/package.json
@@ -1,6 +0,0 @@
-{
- "name": "Application",
- "version": "0.0.0",
- "private": true,
- "main": "./Application.js"
-}
diff --git a/src/components/ContentPage/ContentPage.js b/src/components/ContentPage/ContentPage.js
@@ -0,0 +1,30 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var React = require('react');
+
+var ContentPage = React.createClass({
+
+ propTypes: {
+ body: React.PropTypes.string.isRequired
+ },
+
+ render() {
+ var { className, title, body, other } = this.props;
+
+ /* jshint ignore:start */
+ return <div className={'ContentPage ' + className}
+ dangerouslySetInnerHTML={{__html: body}} />;
+ /* jshint ignore:end */
+ }
+
+});
+
+module.exports = ContentPage;
diff --git a/src/components/ContentPage/ContentPage.less b/src/components/ContentPage/ContentPage.less
@@ -0,0 +1,9 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+.ContentPage {}
diff --git a/src/components/ContentPage/package.json b/src/components/ContentPage/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "ContentPage",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./ContentPage.js"
+}
diff --git a/src/components/HomePage/HomePage.js b/src/components/HomePage/HomePage.js
@@ -0,0 +1,28 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var React = require('react');
+
+var HomePage = React.createClass({
+
+ propTypes: {
+ body: React.PropTypes.string.isRequired
+ },
+
+ render() {
+ /* jshint ignore:start */
+ return <div className="ContentPage"
+ dangerouslySetInnerHTML={{__html: this.props.body}} />;
+ /* jshint ignore:end */
+ }
+
+});
+
+module.exports = HomePage;
diff --git a/src/components/HomePage/HomePage.less b/src/components/HomePage/HomePage.less
@@ -0,0 +1,9 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+.HomePage {}
diff --git a/src/components/HomePage/package.json b/src/components/HomePage/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "HomePage",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./HomePage.js"
+}
diff --git a/src/components/Link/Link.js b/src/components/Link/Link.js
@@ -9,7 +9,7 @@
'use strict';
var React = require('react');
-var RouteActions = require('../../actions/RouteActions');
+var AppActions = require('../../actions/AppActions');
var Link = React.createClass({
@@ -31,7 +31,7 @@ var Link = React.createClass({
handleClick(e) {
e.preventDefault();
- RouteActions.setRoute(this.props.to);
+ AppActions.navigateTo(this.props.to);
}
});
diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js
@@ -9,7 +9,6 @@
'use strict';
var React = require('react');
-var Link = require('../Link');
var Navbar = React.createClass({
@@ -18,10 +17,10 @@ var Navbar = React.createClass({
/* jshint ignore:start */
<div className="navbar-top" role="navigation">
<div className="container">
- <Link className="navbar-brand row" to="/">
+ <a className="navbar-brand row" href="/">
<img src="/images/logo-small.png" width="38" height="38" alt="React" />
<span>React.js Starter Kit</span>
- </Link>
+ </a>
</div>
</div>
/* jshint ignore:end */
diff --git a/src/components/NotFoundPage/NotFoundPage.js b/src/components/NotFoundPage/NotFoundPage.js
@@ -0,0 +1,30 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+//require('./NotFoundPage.less');
+
+var React = require('react');
+
+var NotFoundPage = React.createClass({
+
+ render() {
+ /* jshint ignore:start */
+ return (
+ <div>
+ <h1>Page Not Found</h1>
+ <p>Sorry, but the page you were trying to view does not exist.</p>
+ </div>
+ );
+ /* jshint ignore:end */
+ }
+
+});
+
+module.exports = NotFoundPage;
diff --git a/src/components/NotFoundPage/NotFoundPage.less b/src/components/NotFoundPage/NotFoundPage.less
@@ -0,0 +1,48 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * 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;
+}
+
+@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/NotFoundPage/package.json b/src/components/NotFoundPage/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "NotFoundPage",
+ "version": "0.0.0",
+ "private": true,
+ "main": "./NotFoundPage.js"
+}
diff --git a/src/components/pages/404.html b/src/components/pages/404.html
@@ -1,59 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
- <meta charset="utf-8">
- <title>Page Not Found</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
-
- * {
- line-height: 1.5;
- margin: 0;
- }
-
- html {
- color: #888;
- font-family: sans-serif;
- text-align: center;
- }
-
- body {
- left: 50%;
- margin: -43px 0 0 -150px;
- position: absolute;
- top: 50%;
- width: 300px;
- }
-
- h1 {
- color: #555;
- font-size: 2em;
- font-weight: 400;
- }
-
- p {
- line-height: 1.2;
- }
-
- @media only screen and (max-width: 270px) {
-
- body {
- margin: 10px auto;
- position: static;
- width: 95%;
- }
-
- h1 {
- font-size: 1.5em;
- }
-
- }
-
- </style>
-</head>
-<body>
- <h1>Page Not Found</h1>
- <p>Sorry, but the page you were trying to view does not exist.</p>
-</body>
-</html>
-<!-- IE needs 512+ bytes: http://blogs.msdn.com/b/ieinternals/archive/2010/08/19/http-error-pages-in-internet-explorer.aspx -->
diff --git a/src/components/pages/Index.js b/src/components/pages/Index.js
@@ -1,64 +0,0 @@
-/*
- * React.js Starter Kit
- * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-'use strict';
-
-var React = require('react');
-var PageActions = require('../../actions/PageActions');
-var App = require('../Application');
-
-var HomePage = React.createClass({
-
- statics: {
- layout: App
- },
-
- componentWillMount() {
- PageActions.set({title: 'React.js Starter Kit'});
- },
-
- render() {
- return (
- /* jshint ignore:start */
- <div className="container">
- <div className="row">
- <div className="col-sm-4">
- <h3>Runtime Components</h3>
- <dl>
- <dt><a href="https://facebook.github.io/react/">React</a></dt>
- <dd>A JavaScript library for building user interfaces, developed by Facebook</dd>
- <dt><a href="https://github.com/flatiron/director">Director</a></dt>
- <dd>A tiny and isomorphic URL router for JavaScript</dd>
- <dt><a href="http://getbootstrap.com/">Bootstrap</a></dt>
- <dd>CSS framework for developing responsive, mobile first interfaces</dd>
- </dl>
- </div>
- <div className="col-sm-4">
- <h3>Development Tools</h3>
- <dl>
- <dt><a href="http://gulpjs.com">Gulp</a></dt>
- <dd>JavaScript streaming build system and task automation</dd>
- <dt><a href="http://webpack.github.io/">Webpack</a></dt>
- <dd>Compiles front-end source code into modules / bundles</dd>
- <dt><a href="http://www.browsersync.io/">BrowserSync</a></dt>
- <dd>A lightweight HTTP server for development</dd>
- </dl>
- </div>
- <div className="col-sm-4">
- <h3>Fork me on GitHub</h3>
- <p><a href="https://github.com/kriasoft/react-starter-kit">github.com/kriasoft/react-starter-kit</a></p>
- </div>
- </div>
- </div>
- /* jshint ignore:end */
- );
- }
-
-});
-
-module.exports = HomePage;
diff --git a/src/components/pages/Privacy.js b/src/components/pages/Privacy.js
@@ -1,85 +0,0 @@
-/*
- * React.js Starter Kit
- * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-'use strict';
-
-var React = require('react');
-var PageActions = require('../../actions/PageActions');
-var App = require('../Application');
-var Link = require('../Link');
-
-var PrivacyPage = React.createClass({
-
- statics: {
- layout: App,
- breadcrumb: (
- /* jshint ignore:start */
- <ol className="breadcrumb">
- <li><Link to="/">Home</Link></li>
- <li className="active">Privacy</li>
- </ol>
- /* jshint ignore:end */
- )
- },
-
- componentWillMount() {
- PageActions.set({title: 'Privacy Policy'});
- },
-
- render() {
- return (
- /* jshint ignore:start */
- <div className="container">
- <p>
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean consequat tortor fermentum mi
- fermentum dignissim. Nullam vel ipsum ut ligula elementum lobortis. Maecenas aliquam, massa laoreet
- lacinia pretium, nisi urna venenatis tortor, nec imperdiet tellus libero efficitur metus. Fusce
- semper posuere ligula, et facilisis metus bibendum interdum. Mauris at mauris sit amet sem pharetra
- commodo a eu leo. Nam at est non risus cursus maximus. Nam feugiat augue libero, id consectetur
- tortor bibendum non. Quisque nec fringilla lorem. Nullam efficitur vulputate mauris, nec maximus leo
- dignissim id.
- </p>
- <p>
- In hac habitasse platea dictumst. Duis sagittis dui ac ex suscipit maximus. Morbi pellentesque
- venenatis felis sed convallis. Nulla varius, nibh vitae placerat tempus, mauris sem elementum ipsum,
- eget sollicitudin nisl est vel purus. Fusce malesuada odio velit, non cursus leo fermentum id. Cras
- pharetra sodales fringilla. Etiam quis est a dolor egestas pellentesque. Maecenas non scelerisque
- purus, congue cursus arcu. Donec vel dapibus mi. Mauris maximus posuere placerat. Sed et libero eu
- nibh tristique mollis a eget lectus. Donec interdum augue sollicitudin vehicula hendrerit. Vivamus
- justo orci, molestie ac sollicitudin ac, lobortis at tellus. Etiam rhoncus ullamcorper risus eu
- tempor. Sed porttitor, neque ac efficitur gravida, arcu lacus pharetra dui, in consequat elit tellus
- auctor nulla. Donec placerat elementum diam, vitae imperdiet lectus luctus at.
- </p>
- <p>
- Nullam eu feugiat mi. Quisque nec tristique nisl, dignissim dictum leo. Nam non quam nisi. Donec
- rutrum turpis ac diam blandit, id pulvinar mauris suscipit. Pellentesque tincidunt libero ultricies
- risus iaculis, sit amet consequat velit blandit. Fusce quis varius nulla. Nullam nisi nisi, suscipit
- ut magna quis, feugiat porta nibh. Sed id enim lectus. Suspendisse elementum justo sapien, sit amet
- consequat orci accumsan et. Aliquam ornare ullamcorper sem sed finibus. Nullam ac lacus pulvinar,
- egestas felis ut, accumsan est.
- </p>
- <p>
- Pellentesque sagittis vehicula sem quis luctus. Proin sodales magna in lorem hendrerit aliquam.
- Integer eu varius orci. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
- cubilia Curae; Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia
- Curae; Ut at mauris nibh. Suspendisse maximus ac eros at vestibulum.
- </p>
- <p>
- Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque egestas tortor et dui
- consequat faucibus. Nunc vitae odio ornare, venenatis ligula a, vulputate nisl. Aenean congue varius
- ex, sit amet bibendum odio posuere at. Nulla facilisi. In finibus, nulla vitae tincidunt ornare,
- sapien nulla fermentum mauris, sed consectetur tortor arcu eget arcu. Vestibulum vel quam enim.
- </p>
- </div>
- /* jshint ignore:end */
- );
- }
-
-});
-
-module.exports = PrivacyPage;
diff --git a/src/components/pages/index.html b/src/components/pages/index.html
@@ -1,27 +0,0 @@
-<!doctype html>
-<html class="no-js" lang="">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <title><%= title %></title>
- <meta name="description" content="<%= description %>">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <link rel="stylesheet" href="/css/bootstrap.css">
- </head>
- <body>
- <!--[if lt IE 8]>
- <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
- <![endif]-->
- <%= body %>
- <script src="/app.js"></script>
- <!-- Google Analytics -->
- <script>
- (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
- function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
- e=o.createElement(i);r=o.getElementsByTagName(i)[0];
- e.src='//www.google-analytics.com/analytics.js';
- r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
- ga('create','UA-XXXXX-X');ga('send','pageview');
- </script>
- </body>
-</html>
diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js
@@ -12,11 +12,10 @@ var keyMirror = require('react/lib/keyMirror');
var ActionTypes = keyMirror({
- // Route action types
- SET_CURRENT_ROUTE: null,
-
- // Page action types
- SET_CURRENT_PAGE: null
+ LOAD_PAGE: null,
+ LOAD_PAGE_SUCCESS: null,
+ LOAD_PAGE_ERROR: null,
+ CHANGE_LOCATION: null
});
diff --git a/src/core/Store.js b/src/core/Store.js
@@ -1,84 +0,0 @@
-/*
- * React.js Starter Kit
- * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-'use strict';
-
-var EventEmitter = require('events').EventEmitter;
-var assign = require('react/lib/Object.assign');
-var invariant = require('react/lib/invariant');
-
-var CHANGE_EVENT = 'change';
-
-/**
- * The Flux store base class.
- */
-class Store {
-
- /**
- * Constructs a Store object, extends it with EventEmitter and supplied
- * methods parameter, and creates a mixin property for use in components.
- *
- * @param {object} methods Public methods for Store instance.
- * @constructor
- */
- constructor(methods) {
-
- var self = this;
-
- invariant(!methods.dispatcherToken,'"dispatcherToken" is a reserved name and cannot be used as a method name.');
- invariant(!methods.Mixin,'"Mixin" is a reserved name and cannot be used as a method name.');
-
- assign(this, EventEmitter.prototype, methods);
-
- this.dispatcherToken = null;
-
- /**
- * Base functionality for every Store constructor. Mixed into the
- * `Store` prototype, but exposed statically for easy access.
- */
- this.Mixin = {
-
- componentDidMount: function() {
- self.addChangeListener(this.onChange);
- },
-
- componentWillUnmount: function() {
- self.removeChangeListener(this.onChange);
- }
-
- };
- }
-
- /**
- * Emits change event.
- */
- emitChange() {
- this.emit(CHANGE_EVENT);
- }
-
- /**
- * Adds a change listener.
- *
- * @param {function} callback Callback function.
- */
- addChangeListener(callback) {
- this.on(CHANGE_EVENT, callback);
- }
-
- /**
- * Removes a change listener.
- *
- * @param {function} callback Callback function.
- */
- removeChangeListener(callback) {
- this.removeListener(CHANGE_EVENT, callback);
- }
-
-}
-
-module.exports = Store;
diff --git a/src/index.html b/src/index.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html class="no-js" lang="">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <title><%- title %></title>
+ <meta name="description" content="<%- description %>">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="stylesheet" href="/css/bootstrap.css">
+ <script src="/app.js"></script>
+ </head>
+ <body>
+ <!--[if lt IE 8]>
+ <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
+ <![endif]-->
+ <%= body %>
+ <!-- Google Analytics -->
+ <script>
+ (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
+ function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
+ e=o.createElement(i);r=o.getElementsByTagName(i)[0];
+ e.src='//www.google-analytics.com/analytics.js';
+ r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
+ ga('create','UA-XXXXX-X');ga('send','pageview');
+ </script>
+ </body>
+</html>
diff --git a/src/pages/about.jade b/src/pages/about.jade
@@ -0,0 +1,46 @@
+---
+title: About
+---
+p.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean consequat
+ tortor fermentum mi fermentum dignissim. Nullam vel ipsum ut ligula elementum
+ lobortis. Maecenas aliquam, massa laoreet lacinia pretium, nisi urna venenatis
+ tortor, nec imperdiet tellus libero efficitur metus. Fusce semper posuere
+ ligula, et facilisis metus bibendum interdum. Mauris at mauris sit amet sem
+ pharetra commodo a eu leo. Nam at est non risus cursus maximus. Nam feugiat
+ augue libero, id consectetur tortor bibendum non. Quisque nec fringilla lorem.
+ Nullam efficitur vulputate mauris, nec maximus leo dignissim id.
+p.
+ In hac habitasse platea dictumst. Duis sagittis dui ac ex suscipit maximus.
+ Morbi pellentesque venenatis felis sed convallis. Nulla varius, nibh vitae
+ placerat tempus, mauris sem elementum ipsum, eget sollicitudin nisl est vel
+ purus. Fusce malesuada odio velit, non cursus leo fermentum id. Cras pharetra
+ sodales fringilla. Etiam quis est a dolor egestas pellentesque. Maecenas non
+ scelerisque purus, congue cursus arcu. Donec vel dapibus mi. Mauris maximus
+ posuere placerat. Sed et libero eu nibh tristique mollis a eget lectus. Donec
+ interdum augue sollicitudin vehicula hendrerit. Vivamus justo orci, molestie
+ ac sollicitudin ac, lobortis at tellus. Etiam rhoncus ullamcorper risus eu
+ tempor. Sed porttitor, neque ac efficitur gravida, arcu lacus pharetra dui, in
+ consequat elit tellus auctor nulla. Donec placerat elementum diam, vitae
+ imperdiet lectus luctus at.
+p.
+ Nullam eu feugiat mi. Quisque nec tristique nisl, dignissim dictum leo. Nam
+ non quam nisi. Donec rutrum turpis ac diam blandit, id pulvinar mauris
+ suscipit. Pellentesque tincidunt libero ultricies risus iaculis, sit amet
+ consequat velit blandit. Fusce quis varius nulla. Nullam nisi nisi, suscipit
+ ut magna quis, feugiat porta nibh. Sed id enim lectus. Suspendisse elementum
+ justo sapien, sit amet consequat orci accumsan et. Aliquam ornare ullamcorper
+ sem sed finibus. Nullam ac lacus pulvinar, egestas felis ut, accumsan est.
+p.
+ Pellentesque sagittis vehicula sem quis luctus. Proin sodales magna in lorem
+ hendrerit aliquam. Integer eu varius orci. Vestibulum ante ipsum primis in
+ faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum ante ipsum
+ primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut at mauris
+ nibh. Suspendisse maximus ac eros at vestibulum.
+p.
+ Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque egestas
+ tortor et dui consequat faucibus. Nunc vitae odio ornare, venenatis ligula a,
+ vulputate nisl. Aenean congue varius ex, sit amet bibendum odio posuere at.
+ Nulla facilisi. In finibus, nulla vitae tincidunt ornare, sapien nulla
+ fermentum mauris, sed consectetur tortor arcu eget arcu. Vestibulum vel quam
+ enim.
diff --git a/src/pages/index.jade b/src/pages/index.jade
@@ -0,0 +1,25 @@
+---
+title: React.js Starter Kit
+---
+div.row
+ div.col-sm-4
+ h3 Runtime Components
+ dl
+ dt <a href="https://facebook.github.io/react/">React</a>
+ dd A JavaScript library for building user interfaces, developed by Facebook
+ dt <a href="https://github.com/flatiron/director">Director</a>
+ dd A tiny and isomorphic URL router for JavaScript
+ dt <a href="http://getbootstrap.com/">Bootstrap</a>
+ dd CSS framework for developing responsive, mobile first interfaces
+ div.col-sm-4
+ h3 Development Tools
+ dl
+ dt <a href="http://gulpjs.com">Gulp</a>
+ dd JavaScript streaming build system and task automation
+ dt <a href="http://webpack.github.io/">Webpack</a>
+ dd Compiles front-end source code into modules / bundles
+ dt <a href="http://www.browsersync.io/">BrowserSync</a>
+ dd A lightweight HTTP server for development
+ div.col-sm-4
+ h3 Fork me on GitHub
+ p <a href="https://github.com/kriasoft/react-starter-kit">github.com/kriasoft/react-starter-kit</a>
diff --git a/src/pages/privacy.jade b/src/pages/privacy.jade
@@ -0,0 +1,51 @@
+---
+title: Privacy Policy
+---
+mixin breadcrumb
+ ol.breadcrumb
+ li <a href="/">Home</a>
+ li <a href="/privacy">Privacy</a>
+
+p.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean consequat
+ tortor fermentum mi fermentum dignissim. Nullam vel ipsum ut ligula elementum
+ lobortis. Maecenas aliquam, massa laoreet lacinia pretium, nisi urna venenatis
+ tortor, nec imperdiet tellus libero efficitur metus. Fusce semper posuere
+ ligula, et facilisis metus bibendum interdum. Mauris at mauris sit amet sem
+ pharetra commodo a eu leo. Nam at est non risus cursus maximus. Nam feugiat
+ augue libero, id consectetur tortor bibendum non. Quisque nec fringilla lorem.
+ Nullam efficitur vulputate mauris, nec maximus leo dignissim id.
+p.
+ In hac habitasse platea dictumst. Duis sagittis dui ac ex suscipit maximus.
+ Morbi pellentesque venenatis felis sed convallis. Nulla varius, nibh vitae
+ placerat tempus, mauris sem elementum ipsum, eget sollicitudin nisl est vel
+ purus. Fusce malesuada odio velit, non cursus leo fermentum id. Cras pharetra
+ sodales fringilla. Etiam quis est a dolor egestas pellentesque. Maecenas non
+ scelerisque purus, congue cursus arcu. Donec vel dapibus mi. Mauris maximus
+ posuere placerat. Sed et libero eu nibh tristique mollis a eget lectus. Donec
+ interdum augue sollicitudin vehicula hendrerit. Vivamus justo orci, molestie
+ ac sollicitudin ac, lobortis at tellus. Etiam rhoncus ullamcorper risus eu
+ tempor. Sed porttitor, neque ac efficitur gravida, arcu lacus pharetra dui, in
+ consequat elit tellus auctor nulla. Donec placerat elementum diam, vitae
+ imperdiet lectus luctus at.
+p.
+ Nullam eu feugiat mi. Quisque nec tristique nisl, dignissim dictum leo. Nam
+ non quam nisi. Donec rutrum turpis ac diam blandit, id pulvinar mauris
+ suscipit. Pellentesque tincidunt libero ultricies risus iaculis, sit amet
+ consequat velit blandit. Fusce quis varius nulla. Nullam nisi nisi, suscipit
+ ut magna quis, feugiat porta nibh. Sed id enim lectus. Suspendisse elementum
+ justo sapien, sit amet consequat orci accumsan et. Aliquam ornare ullamcorper
+ sem sed finibus. Nullam ac lacus pulvinar, egestas felis ut, accumsan est.
+p.
+ Pellentesque sagittis vehicula sem quis luctus. Proin sodales magna in lorem
+ hendrerit aliquam. Integer eu varius orci. Vestibulum ante ipsum primis in
+ faucibus orci luctus et ultrices posuere cubilia Curae; Vestibulum ante ipsum
+ primis in faucibus orci luctus et ultrices posuere cubilia Curae; Ut at mauris
+ nibh. Suspendisse maximus ac eros at vestibulum.
+p.
+ Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque egestas
+ tortor et dui consequat faucibus. Nunc vitae odio ornare, venenatis ligula a,
+ vulputate nisl. Aenean congue varius ex, sit amet bibendum odio posuere at.
+ Nulla facilisi. In finibus, nulla vitae tincidunt ornare, sapien nulla
+ fermentum mauris, sed consectetur tortor arcu eget arcu. Vestibulum vel quam
+ enim.
diff --git a/src/server.js b/src/server.js
@@ -0,0 +1,116 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var _ = require('lodash');
+var fs = require('fs');
+var path = require('path');
+var express = require('express');
+var React = require('react');
+var ReactTools = require('react-tools');
+
+// Set global variables
+global.__DEV__ = process.env.NODE_ENV == 'development';
+global.__SERVER__ = true;
+
+// Configure JSX Harmony transform in order to be able
+// require .js files with JSX
+var jsExt = require.extensions['.js'];
+var ignoreExt = function(module, file) { module._compile('', file); };
+require.extensions['.less'] = ignoreExt;
+require.extensions['.svg'] = ignoreExt;
+require.extensions['.js'] = function(module, file) {
+ if (file.indexOf('node_modules') === -1) {
+ var src = fs.readFileSync(file, 'utf8');
+ try {
+ src = ReactTools.transform(src, {harmony: true, stripTypes: true});
+ } catch (e) {
+ throw new Error('Error transforming ' + file + '. ' + e.toString());
+ }
+ module._compile(src, file);
+ } else {
+ jsExt(module, file);
+ }
+};
+
+// The top-level React component + HTML template for it
+var App = React.createFactory(require('./components/App'));
+var template = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8');
+var Dispatcher = require('./core/Dispatcher');
+var ActionTypes = require('./constants/ActionTypes');
+var AppStore = require('./stores/AppStore');
+
+var server = express();
+
+server.set('port', (process.env.PORT || 5000));
+server.use(express.static(path.join(__dirname, '../build')));
+
+// Page API
+server.get('/api/page/*', function(req, res) {
+ var path = req.path.substr(9);
+ var page = AppStore.getPage(path);
+ res.send(page);
+});
+
+// Server-side rendering
+server.get('*', function(req, res) {
+ var data = {description: ''};
+ var app = new App({
+ path: req.path,
+ onSetTitle: function(title) { data.title = title; },
+ onSetMeta: function(name, content) { data[name] = content; },
+ onPageNotFound: function() { res.status(404); }
+ });
+ data.body = React.renderToString(app);
+ var html = _.template(template, data);
+ res.send(html);
+});
+
+// Load pages from the `/src/pages/` folder into the AppStore
+(function() {
+ var assign = require('react/lib/Object.assign');
+ var fm = require('front-matter');
+ var jade = require('jade');
+ var sourceDir = path.join(__dirname, './pages');
+ var getFiles = function(dir) {
+ var pages = [];
+ fs.readdirSync(dir).forEach(function(file) {
+ var stat = fs.statSync(path.join(dir, file));
+ if (stat && stat.isDirectory()) {
+ pages = pages.concat(getFiles(file));
+ } else {
+ // Convert the file to a Page object
+ var filename = path.join(dir, file);
+ var url = filename.
+ substr(sourceDir.length, filename.length - sourceDir.length - 5)
+ .replace('\\', '/');
+ if (url.indexOf('/index', url.length - 6) !== -1) {
+ url = url.substr(0, url.length - (url.length > 6 ? 6 : 5));
+ }
+ var source = fs.readFileSync(filename, 'utf8');
+ var content = fm(source);
+ var html = jade.render(content.body, null, ' ');
+ var page = assign({}, {path: url, body: html}, content.attributes);
+ Dispatcher.handleServerAction({
+ actionType: ActionTypes.LOAD_PAGE,
+ path: url,
+ page: page
+ });
+ }
+ });
+ return pages;
+ };
+ return getFiles(sourceDir);
+})();
+
+server.listen(server.get('port'), function() {
+ console.log('The server is running at http://localhost:' + server.get('port'));
+});
+
+module.exports.server = server;
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
@@ -0,0 +1,96 @@
+/*
+ * React.js Starter Kit
+ * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.txt file in the root directory of this source tree.
+ */
+
+'use strict';
+
+var Dispatcher = require('../core/Dispatcher');
+var ActionTypes = require('../constants/ActionTypes');
+var PayloadSources = require('../constants/PayloadSources');
+var EventEmitter = require('eventemitter3');
+var assign = require('react/lib/Object.assign');
+
+var CHANGE_EVENT = 'change';
+
+var _pages = {};
+var _loading = false;
+
+if (__SERVER__) {
+ console.log('Fill the AppStore with data');
+ _pages['/'] = {title: 'Home Page'};
+ _pages['/privacy'] = {title: 'Privacy Policy'};
+}
+
+var AppStore = assign({}, EventEmitter.prototype, {
+
+ /**
+ * Gets page data by the given URL path.
+ *
+ * @param {String} path URL path.
+ * @returns {*} Page data.
+ */
+ getPage(path) {
+ return path in _pages ? _pages[path] : {
+ title: 'Page Not Found',
+ type: 'notfound'
+ };
+ },
+
+ /**
+ * 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.off(CHANGE_EVENT, callback);
+ }
+
+});
+
+AppStore.dispatcherToken = Dispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.actionType) {
+
+ case ActionTypes.LOAD_PAGE:
+ if (action.source === PayloadSources.VIEW_ACTION) {
+ _loading = true;
+ } else {
+ if (!action.err) {
+ _pages[action.path] = action.page;
+ }
+ }
+ AppStore.emitChange();
+ break;
+
+ default:
+ // Do nothing
+
+ }
+
+});
+
+module.exports = AppStore;
+
diff --git a/src/stores/PageStore.js b/src/stores/PageStore.js
@@ -1,47 +0,0 @@
-/*
- * React.js Starter Kit
- * Copyright (c) 2014 Konstantin Tarkus (@koistya), KriaSoft LLC.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE.txt file in the root directory of this source tree.
- */
-
-'use strict';
-
-var Store = require('../core/Store');
-var Dispatcher = require('../core/Dispatcher');
-var ActionTypes = require('../constants/ActionTypes');
-
-/**
- * @typedef Page
- * @type {object}
- * @property {string} title
- * @property {string} description
- * @property {string} keywords
- */
-var _page;
-
-var PageStore = new Store({
-
- /**
- * Gets metadata associated with the current page.
- * @returns {Page}
- */
- get() {
- return _page || require('../constants/Settings').defaults.page;
- }
-
-});
-
-PageStore.dispatcherToken = Dispatcher.register(payload => {
-
- var action = payload.action;
-
- if (action.actionType == ActionTypes.SET_CURRENT_PAGE) {
- _page = action.page;
- PageStore.emitChange();
- }
-
-});
-
-module.exports = PageStore;