├── .babelrc
├── .gitignore
├── README.md
├── client
└── bootstrap.js
├── components
├── BillboardPanel
│ ├── BillboardPanel.js
│ ├── BillboardPanel.styl
│ ├── background.jpg
│ └── package.json
├── Button
│ ├── Button.js
│ ├── Button.styl
│ └── package.json
├── Layout
│ ├── Layout.js
│ ├── Layout.styl
│ └── package.json
└── variables.styl
├── configs
├── development
│ ├── application.config.js
│ └── webpack.config.js
├── index.js
└── production
│ ├── application.config.js
│ └── webpack.config.js
├── index.js
├── nodemon.json
├── package.json
├── pages
└── FuturamaPage
│ ├── FuturamaPage.js
│ ├── FuturamaPage.styl
│ └── package.json
├── public
└── favicon.ico
├── server
├── bundler.js
├── components
│ └── Page
│ │ ├── Page.js
│ │ └── package.json
├── index.js
├── router.js
└── templates
│ ├── error.hbs
│ └── layout.hbs
└── tests
└── fixtures
└── futurama
└── index.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Landing
2 |
3 | Experimental approach to build isomorphic landing pages on React components.
4 |
5 | # Usage
6 |
7 | Before usage you should install all dependencies:
8 |
9 | npm install
10 |
11 | React Landing runs little node server to build static assets and pre-render markup. To start it, simply run:
12 |
13 | npm start
14 |
15 | Now, you can see example Futurama page at `http://localhost:3000/futurama`.
16 |
17 | You may also want to run React Landing in watch mode. In this mode [nodemon](https://github.com/remy/nodemon) and [webpack-dev-server](webpack.github.io/docs/webpack-dev-server.html) are running at your service:
18 |
19 | npm run-script watch
20 |
21 | # Explanation
22 |
23 | Behind React Landing lies two common ideas. First, it should separate interface development from monolithic application, which usually contains both business logic and multitude of view templates. Second, it must share components library with single-page application built on React.
24 |
25 | # Examples
26 |
27 | We are now experimenting with this approach at VezetVsem. Currently, you can see just one example page, but more are coming!
28 |
29 | - [vezetvsem.ru/flat-moving](http://vezetvsem.ru/flat-moving)
30 |
--------------------------------------------------------------------------------
/client/bootstrap.js:
--------------------------------------------------------------------------------
1 | var data = JSON.parse(document.getElementById('data').innerText);
2 | React.render(React.createElement(Page, data), document.getElementById('application'));
3 |
--------------------------------------------------------------------------------
/components/BillboardPanel/BillboardPanel.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import Button from 'Button';
3 |
4 | import './BillboardPanel.styl';
5 |
6 | class BillboardPanel extends React.Component {
7 |
8 | static contextTypes = {
9 | content: PropTypes.object.isRequired
10 | }
11 |
12 | render() {
13 | var content = this.context.content;
14 | return (
15 |
16 |
17 |
18 | {this.context.content.title}
19 |
20 |
21 | {this.context.content.slogan}
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | export default BillboardPanel;
30 |
--------------------------------------------------------------------------------
/components/BillboardPanel/BillboardPanel.styl:
--------------------------------------------------------------------------------
1 | @require('../variables');
2 |
3 | .BillboardPanel {
4 | display: table;
5 | width: 100%;
6 | height: 100%;
7 | padding-top: 84px; // Compensate visual height of page header.
8 |
9 | text-align: center;
10 | color: #fff;
11 | background: url('./background.jpg') bottom center no-repeat;
12 | background-size: cover;
13 |
14 | @media (max-width: $small-screen) {
15 | padding-top: 64px;
16 | }
17 |
18 | &--content {
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | height: 100%;
24 | padding: 0 30px;
25 |
26 | @media (max-width: $small-screen) {
27 | padding: 0 15px;
28 | }
29 | }
30 |
31 | &--title {
32 | font: 900 90px/1.2 'Maven Pro';
33 | display: flex;
34 | align-items: center;
35 | flex-grow: 2;
36 | text-transform: uppercase;
37 | color: #B70101;
38 |
39 | shadows =,;
40 | for i in (-3..3) {
41 | for j in (-3..3) {
42 | push(shadows, (i * 1px) (j * 1px) 0 #FFE761);
43 | }
44 | }
45 | shift(shadows);
46 | shift(shadows);
47 |
48 | text-shadow: shadows
49 |
50 | @media (max-width: $small-screen) {
51 | font-size: 50px;
52 | }
53 | }
54 |
55 | &--subtitle {
56 | font: 500 36px/1.2 'Maven Pro';
57 | flex-grow: 1;
58 | text-transform: uppercase;
59 | text-shadow: 3px 3px 4px rgba(0, 0, 0, 0.8);
60 |
61 | @media (max-width: $small-screen) {
62 | font-size: 28px;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/components/BillboardPanel/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vtambourine/react-landing/7b7fefd4a1bd29604e52c179b3506a71b60a916e/components/BillboardPanel/background.jpg
--------------------------------------------------------------------------------
/components/BillboardPanel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BillboardPanel",
3 | "main": "./BillboardPanel.js"
4 | }
5 |
--------------------------------------------------------------------------------
/components/Button/Button.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import classNames from 'classnames';
3 |
4 | import './Button.styl';
5 |
6 | class Button extends React.Component {
7 |
8 | static propTypes = {
9 | /**
10 | * Button text
11 | * @type {string}
12 | */
13 | label: PropTypes.string,
14 |
15 | /**
16 | * Button link.
17 | * If present, button will be rendered as .
18 | * @type {string}
19 | */
20 | url: PropTypes.string,
21 |
22 | /**
23 | * Click event listener.
24 | * @type {Function}
25 | */
26 | onClick: PropTypes.func
27 | }
28 |
29 | render() {
30 | var content;
31 | if (this.props.url) {
32 | content =
33 |
34 | {this.props.label || this.props.children}
35 | ;
36 | } else {
37 | content =
38 |
39 | {this.props.label || this.props.children}
40 | ;
41 | }
42 |
43 | return (
44 |
46 | {content}
47 |
48 | );
49 | }
50 |
51 | }
52 |
53 | export default Button;
54 |
--------------------------------------------------------------------------------
/components/Button/Button.styl:
--------------------------------------------------------------------------------
1 | @require('../variables');
2 |
3 | .Button {
4 | font: 400 18px/1.4 Roboto, Helvetica, Arial, sans-serif;
5 | display: inline-block;
6 | border-radius: 3px;
7 | background: $green-color;
8 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3);
9 | text-transform: uppercase;
10 | color: $dark-text-color;
11 | cursor: pointer;
12 | transition: background-color 0.2s;
13 |
14 | &:hover {
15 | background: $highlighed-green-color;
16 | }
17 |
18 | @media (max-width: $small-screen) {
19 | font-size: 15px;
20 | }
21 |
22 | &-large {
23 | font-weight: 500;
24 | border-radius: 5px;
25 | }
26 |
27 | &-flat {
28 | color: #fff;
29 | border: 2px white solid;
30 | border-radius: 5px;
31 | box-shadow: none;
32 | background: none;
33 | transition: background-color 0.2s;
34 |
35 | &:hover {
36 | background: rgba(255, 255, 255, 0.2);
37 | }
38 | }
39 |
40 | &--control {
41 | display: block;
42 | padding: 12px 20px;
43 | color: inherit;
44 |
45 | &, &:focus, &:hover {
46 | color: inherit;
47 | text-decoration: none;
48 | }
49 | }
50 |
51 | &-large &--control {
52 | display: block;
53 | padding: 21px 65px;
54 |
55 | @media (max-width: $small-screen) {
56 | padding: 21px;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/components/Button/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Button.js",
3 | "main": "./Button.js"
4 | }
5 |
--------------------------------------------------------------------------------
/components/Layout/Layout.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import Button from 'Button';
3 |
4 | import './Layout.styl';
5 |
6 | class Layout extends React.Component {
7 |
8 | static contextTypes = {
9 | user: PropTypes.object.isRequired,
10 | content: PropTypes.object.isRequired
11 | }
12 |
13 | toggleHeaderContent = () => {
14 | var headerNode = React.findDOMNode(this.refs.header);
15 | headerNode.classList.toggle('-hidden');
16 | }
17 |
18 | render() {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 | {this.context.user.name}
26 |
27 |
28 |
29 |
32 |
33 |
34 |
35 |
37 |
39 |
40 |
41 | {this.context.user.name}
42 |
43 |
44 |
47 |
48 |
49 |
50 |
51 | {this.props.children}
52 |
53 |
54 | );
55 | }
56 |
57 | }
58 |
59 | export default Layout;
60 |
--------------------------------------------------------------------------------
/components/Layout/Layout.styl:
--------------------------------------------------------------------------------
1 | @require '../variables.styl';
2 |
3 | * {
4 | box-sizing: border-box;
5 | }
6 |
7 | html,
8 | body {
9 | font: 14px/1.4 "Helvetica Neue",Helvetica,Arial,sans-serif;
10 | width: 100%;
11 | height: 100%;
12 | margin: 0;
13 | padding: 0;
14 | }
15 |
16 | .Layout {
17 | position: relative;
18 | width: 100%;
19 | height: 100%;
20 |
21 | &--header {
22 | font-size: 14px;
23 | position: absolute;
24 | top: 0;
25 | width: 100%;
26 | text-align: center;
27 | }
28 |
29 | &--content {
30 | width: 100%;
31 | height: 100%;
32 |
33 | @media (max-width: $small-screen) {
34 | min-height: 0;
35 | }
36 | }
37 | }
38 |
39 | .Layout-desktop {
40 | @extend .Layout;
41 |
42 | &--header {
43 | @media (max-width: $small-screen) {
44 | display: none;
45 | }
46 |
47 | color: #fff;
48 | }
49 |
50 | &--headerRow {
51 | display: flex;
52 | justify-content: flex-end;
53 | align-items: baseline;
54 | width: 100%;
55 | max-width: $max-width;
56 | margin: 0 auto;
57 | padding: 14px 30px;
58 | }
59 |
60 | &--headerItem {
61 | padding: 0 20px;
62 | text-align: center;
63 | }
64 |
65 | &--profile {
66 | display: inline-block;
67 | }
68 | }
69 |
70 | .Layout-mobile {
71 | @extend .Layout;
72 |
73 | &--header {
74 | @media (min-width: $small-screen + 1) {
75 | display: none;
76 | }
77 | }
78 |
79 | &--drawerButton {
80 | $size = 48px;
81 |
82 | position: absolute;
83 | z-index: 1;
84 | top: 14px;
85 | left: 14px;
86 | width: $size;
87 | height: $size;
88 | border-radius: $size;
89 | background-color: $green-color;
90 | cursor: pointer;
91 |
92 | &:hover {
93 | background-color: $highlighed-green-color;
94 | }
95 | }
96 |
97 | &--headerContent {
98 | display: flex;
99 | justify-content: space-between;
100 | align-items: center;
101 | padding: 74px 12px 20px;
102 | color: #333;
103 | background-color: #fff;
104 | box-shadow: 0 5px 10px alpha(#000, 0.5);
105 | transform: translateY(0%);
106 | transition: transform 0.2s;
107 | }
108 |
109 | &--header.-hidden &--headerContent {
110 | transform: translateY(-100%);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/components/Layout/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Layout",
3 | "main": "./Layout.js",
4 | "dependencies": {
5 | "autoprefixer": "^5.2.0",
6 | "autoprefixer-core": "^5.2.1",
7 | "postcss-loader": "^0.5.1"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/components/variables.styl:
--------------------------------------------------------------------------------
1 | $small-screen = 480px;
2 | $max-width = 980px;
3 |
4 | $green-color = #68E297;
5 | $highlighed-green-color = #9FFA9A;
6 |
--------------------------------------------------------------------------------
/configs/development/application.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | export default {
4 | static: {
5 | path: path.resolve(__dirname, 'build/'),
6 | url: 'http://localhost:3001/assets/',
7 | },
8 | devServer: {
9 | host: 'localhost',
10 | port: 3001
11 | },
12 | scripts: [
13 | 'http://localhost:3001/webpack-dev-server.js',
14 | 'https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0-beta1/react.js'
15 | ],
16 | styles: [
17 | 'https://fonts.googleapis.com/css?family=Maven+Pro:400,500,900'
18 | ]
19 | };
20 |
--------------------------------------------------------------------------------
/configs/development/webpack.config.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import webpack from 'webpack';
4 | import autoprefixer from 'autoprefixer-core';
5 | import {appConfig} from '../../configs';
6 |
7 | var cwd = process.cwd();
8 | var OUTPUT_PATH = path.join(cwd, 'build');
9 |
10 | export default {
11 | devtool: 'eval',
12 | entry: [
13 | // Entry points for bundles should be computed in server application.
14 | ],
15 | output: {
16 | path: OUTPUT_PATH,
17 | filename: '[name].js',
18 | publicPath: appConfig.static.url,
19 | library: "Page",
20 | libraryTarget: "var"
21 | },
22 | resolve: {
23 | root: [
24 | path.join(cwd, 'components')
25 | ]
26 | },
27 | externals: [
28 | {
29 | 'react': 'var React'
30 | }
31 | ],
32 | module: {
33 | loaders: [
34 | {
35 | test: /\.(jpe?g|png|gif|svg)$/,
36 | loader: `file`
37 | },
38 | {
39 | test: /\.js$/,
40 | exclude: /node_modules/,
41 | loader: 'babel'
42 | },
43 | {
44 | test: /\.css$/,
45 | loaders: ['style', 'css']
46 | },
47 | {
48 | test: /\.styl$/,
49 | loaders: ['style', 'css', 'postcss', 'stylus']
50 | }
51 | ]
52 | },
53 | postcss: function () {
54 | return [autoprefixer]
55 | },
56 | plugins: [
57 | new webpack.HotModuleReplacementPlugin(),
58 | new webpack.NoErrorsPlugin(),
59 | new webpack.ProgressPlugin((percentage, message) => {
60 | var MOVE_LEFT = new Buffer('1b5b3130303044', 'hex').toString();
61 | var CLEAR_LINE = new Buffer('1b5b304b', 'hex').toString();
62 | process.stdout.write(CLEAR_LINE + Math.round(percentage * 100) + '% :' + message + MOVE_LEFT);
63 | }),
64 | function () {
65 | this.plugin('done', (stats) => {
66 | console.log(stats.toString({
67 | chunks: false,
68 | colors: true
69 | }));
70 | });
71 | }
72 | ]
73 | };
74 |
--------------------------------------------------------------------------------
/configs/index.js:
--------------------------------------------------------------------------------
1 | var mode = process.env.NODE_ENV || 'development';
2 |
3 | export var appConfig = require(`./${mode}/application.config`);
4 | export var webpackConfig = require(`./${mode}/webpack.config`);
5 |
--------------------------------------------------------------------------------
/configs/production/application.config.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | export default {
4 | static: {
5 | path: path.resolve(__dirname, 'build/'),
6 | url: '/assets/'
7 | },
8 | scripts: [
9 | 'https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0-beta1/react.min.js'
10 | ],
11 | styles: [
12 | 'https://fonts.googleapis.com/css?family=Maven+Pro:400,500,900'
13 | ]
14 | };
15 |
--------------------------------------------------------------------------------
/configs/production/webpack.config.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import webpack from 'webpack';
4 | import autoprefixer from 'autoprefixer-core';
5 | import ExtractTextPlugin from 'extract-text-webpack-plugin';
6 |
7 | var cwd = process.cwd();
8 | var OUTPUT_PATH = path.join(cwd, 'build');
9 |
10 | export default {
11 | entry: [
12 | // Entry points for bundles should be computed in server application.
13 | ],
14 | output: {
15 | path: OUTPUT_PATH,
16 | filename: '[name].js',
17 | library: "Page",
18 | libraryTarget: "var"
19 | },
20 | resolve: {
21 | root: [
22 | path.join(cwd, 'components')
23 | ]
24 | },
25 | externals: [
26 | {
27 | 'react': 'var React'
28 | }
29 | ],
30 | module: {
31 | loaders: [
32 | {
33 | test: /\.(jpe?g|png|gif|svg)$/,
34 | loader: 'file'
35 | },
36 | {
37 | test: /\.js$/,
38 | exclude: /node_modules/,
39 | loader: 'babel?optional=runtime'
40 | },
41 | {
42 | test: /\.css$/,
43 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader')
44 | },
45 | {
46 | test: /\.scss$/,
47 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader')
48 | },
49 | {
50 | test: /\.styl$/,
51 | loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!stylus-loader')
52 | }
53 | ]
54 | },
55 | postcss: function () {
56 | return [autoprefixer]
57 | },
58 | plugins: [
59 | new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}),
60 | new webpack.optimize.OccurenceOrderPlugin(),
61 | new webpack.optimize.UglifyJsPlugin(),
62 | new ExtractTextPlugin('[name].css', {
63 | allChunks: true
64 | }),
65 | new webpack.ProgressPlugin(function(percentage, message) {
66 | var MOVE_LEFT = new Buffer("1b5b3130303044", "hex").toString();
67 | var CLEAR_LINE = new Buffer("1b5b304b", "hex").toString();
68 | process.stdout.write(CLEAR_LINE + Math.round(percentage * 100) + "% :" + message + MOVE_LEFT);
69 | }),
70 | function () {
71 | this.plugin("done", function (stats) {
72 | console.log(stats.toString({
73 | chunks: false,
74 | colors: true
75 | }));
76 | });
77 | }
78 | ]
79 | };
80 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('babel/register');
2 |
3 | process.env.NODE_ENV = process.env.NODE_ENV || 'development';
4 |
5 | // Substitude all node imports of `styl` modules to support both webpack
6 | // styles requires and server rendering of components.
7 | require.extensions['.styl'] = function () {
8 | return '';
9 | };
10 |
11 | var app = require('./server');
12 | var debug = require('debug')('landing-kit:server');
13 | var http = require('http');
14 |
15 | // Start bundler.
16 | // This will build static assets once for production environment
17 | // or run Webpack Dev Server to serve static for development.
18 | // require('./server/bundler');
19 |
20 | // Get port from environment and store in Express.
21 | var port = process.env.PORT || 3000;
22 | app.set('port', port);
23 |
24 | // Create HTTP server.
25 | var server = http.createServer(app);
26 |
27 | // Listen on provided port, on all network interfaces.
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Event listener for HTTP server "error" event.
34 | * @param {Object} error
35 | */
36 | function onError(error) {
37 | if (error.syscall !== 'listen') {
38 | throw error;
39 | }
40 |
41 | var bind = typeof port === 'string'
42 | ? 'Pipe ' + port
43 | : 'Port ' + port;
44 |
45 | // Handle specific listen errors with friendly messages.
46 | switch (error.code) {
47 | case 'EACCES':
48 | console.error(bind + ' requires elevated privileges');
49 | process.exit(1);
50 | break;
51 | case 'EADDRINUSE':
52 | console.error(bind + ' is already in use');
53 | process.exit(1);
54 | break;
55 | default:
56 | throw error;
57 | }
58 | }
59 |
60 | /**
61 | * Event listener for HTTP server "listening" event.
62 | */
63 | function onListening() {
64 | var addr = server.address();
65 | var bind = typeof addr === 'string'
66 | ? 'pipe ' + addr
67 | : 'port ' + addr.port;
68 | debug('Listening on ' + bind);
69 | }
70 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": true,
3 | "watch": [
4 | "client/",
5 | "components",
6 | "server/",
7 | "configs/"
8 | ],
9 | "ignore": [
10 | "*.styl"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "landing-kit",
3 | "version": "0.0.1",
4 | "author": "Benjamin Tambourine ",
5 | "description": "React Landing",
6 | "main": "index.js",
7 | "config": {
8 | "components": "components"
9 | },
10 | "scripts": {
11 | "start": "NODE_PATH=$npm_package_config_components NODE_ENV=production node index.js",
12 | "watch": "NODE_PATH=$npm_package_config_components ./node_modules/.bin/nodemon index.js",
13 | "clean": "rm -fr build/"
14 | },
15 | "license": "ISC",
16 | "private": true,
17 | "dependencies": {
18 | "autoprefixer-core": "^5.2.1",
19 | "babel": "^5.8.19",
20 | "babel-core": "^5.8.19",
21 | "babel-loader": "^5.3.2",
22 | "babel-runtime": "^5.8.20",
23 | "body-parser": "^1.13.2",
24 | "classnames": "^2.1.3",
25 | "compression": "^1.5.2",
26 | "cookie-parser": "^1.3.5",
27 | "css-loader": "^0.15.6",
28 | "debug": "^2.2.0",
29 | "express": "^4.13.1",
30 | "extract-text-webpack-plugin": "^0.8.2",
31 | "file-loader": "^0.8.4",
32 | "hbs": "^3.1.0",
33 | "json-loader": "^0.5.2",
34 | "morgan": "^1.6.1",
35 | "postcss-loader": "^0.5.1",
36 | "react": "^0.14.0-beta1",
37 | "react-dom": "^0.14.0-beta1",
38 | "sass-loader": "^1.0.4",
39 | "serve-favicon": "^2.3.0",
40 | "style-loader": "^0.12.3",
41 | "stylus-loader": "^1.2.1",
42 | "webpack": "^1.10.5",
43 | "webpack-dev-server": "^1.10.1"
44 | },
45 | "devDependencies": {
46 | "nodemon": "^1.4.1"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pages/FuturamaPage/FuturamaPage.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import Layout from 'Layout';
3 | import BillboardPanel from 'BillboardPanel';
4 |
5 | import './FuturamaPage.styl';
6 |
7 | class FuturamaPage extends React.Component {
8 |
9 | static childContextTypes = {
10 | user: PropTypes.object.isRequired,
11 | content: PropTypes.object.isRequired
12 | }
13 |
14 | getChildContext() {
15 | return {
16 | user: this.props.user,
17 | content: this.props.content
18 | }
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | }
32 |
33 | export default FuturamaPage;
34 |
--------------------------------------------------------------------------------
/pages/FuturamaPage/FuturamaPage.styl:
--------------------------------------------------------------------------------
1 | .FuturamaPage {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
--------------------------------------------------------------------------------
/pages/FuturamaPage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "FuturamaPage",
3 | "main": "./FuturamaPage.js"
4 | }
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vtambourine/react-landing/7b7fefd4a1bd29604e52c179b3506a71b60a916e/public/favicon.ico
--------------------------------------------------------------------------------
/server/bundler.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import url from 'url';
4 | import webpack from 'webpack';
5 | import WebpackDevServer from 'webpack-dev-server';
6 | import {appConfig, webpackConfig} from '../configs';
7 |
8 | var debug = require('debug')('landing-kit:server');
9 | var cwd = process.cwd();
10 |
11 | var staticUrl = process.env.STATIC_URL || appConfig.static.url;
12 |
13 | var assetsPromise = new Promise((resolve) => {
14 | webpackConfig.plugins.push(function() {
15 | this.plugin('done', (stats) => {
16 | var assets = stats.toJson().assetsByChunkName;
17 | resolve(assets);
18 | });
19 | });
20 | });
21 |
22 | var PAGES_DIR = path.join(cwd, 'pages/');
23 |
24 | if (process.env.NODE_ENV === 'production') {
25 | // Collect dictionary of bundles.
26 | // Each bundle is a root components of single page.
27 | var entries = fs.readdirSync(PAGES_DIR).reduce((result, name) => {
28 | result[name] = path.join(PAGES_DIR, name, name + '.js');
29 | return result;
30 | }, {});
31 | webpackConfig.entry = entries;
32 |
33 | // Create webpack bundle compiler.
34 | var webpackCompiler = webpack(webpackConfig);
35 | webpackCompiler.run((error) => {
36 | if (error) {
37 | throw error;
38 | }
39 | });
40 | } else {
41 | var entries = fs.readdirSync(PAGES_DIR).reduce((result, name) => {
42 | result[name] = [
43 | 'webpack/hot/dev-server',
44 | `webpack-dev-server/client?${appConfig.static.url}`,
45 | path.join(PAGES_DIR, name, name + '.js')
46 | ]
47 | return result;
48 | }, {});
49 |
50 | webpackConfig.entry = entries;
51 | var webpackCompiler = webpack(webpackConfig);
52 | var webpackDevServer = new WebpackDevServer(webpackCompiler, {
53 | publicPath: appConfig.static.url,
54 | quiet: true,
55 | noInfo: true
56 | });
57 |
58 | webpackDevServer.listen(appConfig.devServer.port, appConfig.devServer.host, () => {
59 | debug('Webpack Dev Server is listening on port ' + appConfig.devServer.port);
60 | });
61 | }
62 |
63 | function bundler() {
64 | return (request, response, next) => {
65 | var bundles = request.bundles = {};
66 | assetsPromise
67 | .then((assets) => {
68 | Object.keys(assets).forEach((name) => {
69 | var bundle = assets[name];
70 | var scripts = [];
71 | var styles = [];
72 | bundles[name] = { scripts, styles };
73 | if (!Array.isArray(bundle)) {
74 | bundle = [bundle];
75 | }
76 | for (let chunk of bundle) {
77 | if (chunk.match(/\.js$/)) {
78 | scripts.push(url.resolve(staticUrl, chunk));
79 | } else if ( chunk.match(/\.css$/)) {
80 | styles.push(url.resolve(staticUrl, chunk));
81 | }
82 | }
83 | });
84 | next();
85 | })
86 | .catch((error) => {
87 | console.log(error.stack);
88 | });
89 | }
90 | }
91 |
92 | export default bundler;
93 |
--------------------------------------------------------------------------------
/server/components/Page/Page.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import React, { PropTypes } from 'react';
4 |
5 | var bootstrapScript = fs.readFileSync(path.join(process.cwd(), 'client/bootstrap.js'));
6 |
7 | class Page extends React.Component {
8 |
9 | static propTypes = {
10 | title: PropTypes.string.isRequired,
11 | meta: PropTypes.objectOf(PropTypes.string).isRequired,
12 | styles: PropTypes.array.isRequired,
13 | scripts: PropTypes.array.isRequired
14 | }
15 |
16 | static defaultProps = {
17 | styles: [],
18 | scripts: []
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 | {this.props.data.title}
33 |
34 | {this.props.styles.map((style, key) => )}
35 |
36 |
37 |
40 |
41 | {this.props.scripts.map((script, key) => )}
42 |
43 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | }
54 |
55 | export default Page;
56 |
--------------------------------------------------------------------------------
/server/components/Page/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Page",
3 | "main": "./Page.js"
4 | }
5 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import logger from 'morgan';
3 | import favicon from 'serve-favicon';
4 | import express from 'express';
5 | import compression from 'compression';
6 | import cookieParser from 'cookie-parser';
7 | import bodyParser from 'body-parser';
8 | import router from './router';
9 | import {appConfig} from '../configs';
10 |
11 | const PUBLIC_DIR = path.join(process.cwd(), 'public');
12 |
13 | var server = express();
14 |
15 | server.set('views', path.join(__dirname, 'templates'));
16 | server.set('view engine', 'hbs');
17 |
18 | server.use(compression());
19 |
20 | server.use(favicon(path.join(PUBLIC_DIR, 'favicon.ico')));
21 | server.use(logger('dev'));
22 | server.use(bodyParser.json());
23 | server.use(bodyParser.urlencoded({ extended: false }));
24 | server.use(cookieParser());
25 | server.use(express.static(PUBLIC_DIR));
26 | server.use('/assets', express.static(path.join(process.cwd(), 'build')));
27 |
28 | server.get('/', (req, res) => {
29 | res.setHeader('Content-Type', 'text/html');
30 | res.end('Hello, World!');
31 | });
32 |
33 | // Store global lists of static files in the request.
34 | // Later this lists will be pupulated by build results.
35 | server.use((request, response, next) => {
36 | request.assets = {
37 | scripts: [].concat(appConfig.scripts),
38 | styles: [].concat(appConfig.styles)
39 | };
40 | next();
41 | });
42 |
43 | // Enable bundler.
44 | // This will build static assets once for production environment
45 | // or run Webpack Dev Server to serve static for development.
46 | var bundler = require('./bundler.js');
47 | server.use(bundler());
48 |
49 | // Enable router.
50 | // Router is responsible for decision which page component will be used
51 | // as a root for certain request.
52 | server.use(router);
53 |
54 | server.use((req, res, next) => {
55 | var error = new Error('Not Found');
56 | error.status = 404;
57 | next(error);
58 | });
59 |
60 | if (server.get('env') === 'development') {
61 | server.use(function (error, req, res, next) {
62 | res.status(error.status || 500);
63 | res.render('error', {
64 | message: error.message,
65 | error: error
66 | });
67 | });
68 | }
69 |
70 | server.use(function (error, req, res, next) {
71 | res.status(error.status || 500);
72 | res.render('error', {
73 | message: error.message,
74 | error: {}
75 | });
76 | });
77 |
78 | export default server;
79 |
--------------------------------------------------------------------------------
/server/router.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom/server';
4 | import Page from './components/Page';
5 |
6 | var router = express.Router();
7 |
8 | router.use('/:page', (req, res, next) => {
9 | var data = req.body;
10 | if (req.query.data) {
11 | try {
12 | data = JSON.parse(req.body);
13 | } catch (error) {
14 | var error = new Error('Invalid data parameter. Expected JSON');
15 | error.status = 500;
16 | next(error);
17 | return;
18 | }
19 | }
20 | var page = req.params.page;
21 | var componentName = page[0].toUpperCase() + page.slice(1) + 'Page';
22 |
23 | // In development mode use fixture data to populate template context.
24 | // TODO: Figure out how to test production builds on fixture data.
25 | // if (process.env.NODE_ENV === 'development') {
26 | var fixture = require(`../tests/fixtures/${page}`);
27 | data = Object.assign({}, fixture, data);
28 | // }
29 |
30 | try {
31 | var Component = require(`../pages/${componentName}`);
32 | var styles = [].concat(req.assets.styles, req.bundles[componentName].styles);
33 | var scripts = [].concat(req.assets.scripts, req.bundles[componentName].scripts);
34 | var body = ReactDOM.renderToString();
35 | var html =
36 | '' +
37 | ReactDOM.renderToStaticMarkup();
42 |
43 | res.send(html);
44 | } catch (error) {
45 | // TODO: Separate 404 errors from other.
46 | next(error);
47 | }
48 | });
49 |
50 | export default router;
51 |
--------------------------------------------------------------------------------
/server/templates/error.hbs:
--------------------------------------------------------------------------------
1 | {{message}}
2 | {{error.status}}
3 | {{error.stack}}
4 |
--------------------------------------------------------------------------------
/server/templates/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}}
5 |
13 |
14 |
15 | {{{body}}}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/fixtures/futurama/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Futurama",
3 | "meta": {
4 | "keywords": "futurama",
5 | "description": "Futurama adult animated science fiction sitcom"
6 | },
7 | "content": {
8 | "title": "Futurama",
9 | "slogan": "The Proud Result of Prison Labor"
10 | },
11 | "user": {
12 | "name": "Philip J. Fry",
13 | "login": "fry"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------