├── .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) =>