├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── README.md ├── data ├── cart.json ├── products.json └── user.json ├── karma.conf.js ├── package.json ├── src ├── app.es6 ├── assets │ ├── cart-item-placeholder.jpg │ └── style.css ├── components │ ├── app.jsx │ ├── banner.jsx │ ├── cart.jsx │ ├── detail.jsx │ ├── html.jsx │ ├── item.jsx │ ├── login.jsx │ ├── payment.jsx │ ├── products.jsx │ └── profile.jsx ├── index.html ├── main.jsx ├── middleware │ └── renderView.jsx ├── server.js └── shared │ ├── cart-action-creators.es6 │ ├── cart-reducer.es6 │ ├── init-redux.es6 │ ├── products-action-creators.es6 │ ├── products-reducer.es6 │ └── sharedRoutes.jsx ├── test ├── .eslintrc.js ├── components │ ├── app.spec.jsx │ ├── cart.spec.jsx │ └── item.spec.jsx └── integration │ └── app.spec.jsx └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": [ 4 | "transform-es2015-destructuring", 5 | "transform-es2015-parameters", 6 | "transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # Matches multiple files with brace expansion notation 14 | # Set default charset 15 | [*.{js}] 16 | charset = utf-8 17 | 18 | # Tab indentation (no size specified) 19 | [Makefile] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.config.js 2 | package.json 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react", 5 | "jsx-a11y", 6 | "import" 7 | ], 8 | "rules": { 9 | "arrow-body-style": [0, "as-needed", { 10 | requireReturnForObjectLiteral: false, 11 | }], 12 | "comma-dangle": ["error", "never"], 13 | "no-underscore-dangle": "off", 14 | "jsx-a11y/no-static-element-interactions": "off", 15 | "react/no-did-mount-set-state": "off" 16 | }, 17 | "globals": { 18 | "window": true, 19 | "document": true 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | browser.* 6 | .idea 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isomorphic App Example 2 | 3 | ## Get the Book 4 | 5 | This example is used in [Isomorphic Development with JavaScript](http://bit.ly/isomorphicdevwithjs-github) 6 | 7 | ## All things Westies 8 | 9 | ### Setup 10 | ``` 11 | npm i 12 | npm start 13 | ``` 14 | 15 | Runs at [localhost:3000](localhost:3000) 16 | 17 | ### Slides from ForwardJS 18 | 19 | https://www.slideshare.net/ElyseKolkerGordon/building-universal-web-apps-with-react-72715124 20 | 21 | ### Slides from ReactJS meetup 22 | 23 | https://docs.google.com/presentation/d/1zxF2wvvOxctqqt78ho5D2lCKkU8R2X0wcY_O8TIbVGA/pub?start=false&loop=false&delayms=10000 24 | -------------------------------------------------------------------------------- /data/cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "name": "Mug", 5 | "price": 5, 6 | "thumbnail": "http://localhost:3000/assets/cart-item-placeholder.jpg" 7 | }, 8 | { 9 | "name": "Socks", 10 | "price": 10, 11 | "thumbnail": "http://localhost:3000/assets/cart-item-placeholder.jpg" 12 | }, 13 | { 14 | "name": "Dog Collar", 15 | "price": 15, 16 | "thumbnail": "http://localhost:3000/assets/cart-item-placeholder.jpg" 17 | }, 18 | { 19 | "name": "Treats", 20 | "price": 15, 21 | "thumbnail": "http://localhost:3000/assets/cart-item-placeholder.jpg" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /data/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "mugs": { 3 | "items": [], 4 | "iconImage": "", 5 | "label": "", 6 | "description": "" 7 | }, 8 | "socks": { 9 | "items": [], 10 | "iconImage": "", 11 | "label": "", 12 | "description": "" 13 | }, 14 | "collars": { 15 | "items": [], 16 | "iconImage": "", 17 | "label": "", 18 | "description": "" 19 | }, 20 | "leashes": { 21 | "items": [], 22 | "iconImage": "", 23 | "label": "", 24 | "description": "" 25 | }, 26 | "treats": { 27 | "items": [], 28 | "iconImage": "", 29 | "label": "", 30 | "description": "" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /data/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "12x4fg4" 3 | } 4 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require('./webpack.config.js'); 2 | 3 | Object.assign(webpackConfig, { 4 | externals: { 5 | 'react/addons': true, 6 | 'react/lib/ExecutionEnvironment': true, 7 | 'react/lib/ReactContext': 'window' 8 | } 9 | }); 10 | 11 | module.exports = (config) => { 12 | config.set({ 13 | frameworks: ['mocha', 'chai-sinon'], 14 | 15 | files: [ 16 | 'node_modules/babel-polyfill/dist/polyfill.js', 17 | 'test/**/*.+(es6|jsx)' 18 | ], 19 | 20 | exclude: [ 21 | 'src/test/middleware/**/*', 22 | 'src/middleware/**/*', 23 | 'src/test/**/*.server.spec.*' 24 | ], 25 | 26 | preprocessors: { 27 | 'test/**/*.+(js|jsx|es6)': ['webpack', 'sourcemap'] 28 | }, 29 | 30 | browsers: ['Chrome'], 31 | 32 | autoWatch: true, 33 | 34 | plugins: [ 35 | 'karma-*' 36 | ], 37 | 38 | webpack: require('./webpack.config.js'), 39 | 40 | webpackMiddleware: { 41 | noInfo: false, 42 | stats: { 43 | chunks: false, 44 | colors: true, 45 | hash: false, 46 | version: false, 47 | timings: false, 48 | assets: false, 49 | modules: false, 50 | reasons: false, 51 | children: false, 52 | source: false, 53 | errors: true, 54 | errorDetails: true, 55 | warnings: false, 56 | publicPath: false 57 | } 58 | } 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overview-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "build:browser": "node_modules/.bin/webpack", 8 | "prestart": "npm run build:browser", 9 | "start": "node src/server.js", 10 | "examples": "node examples-server.js", 11 | "test:browser": "node_modules/.bin/karma start", 12 | "test:server": "node_modules/.bin/mocha test/components/* --compilers js:babel-register --watch" 13 | }, 14 | "author": "elyseko", 15 | "license": "ISC", 16 | "dependencies": { 17 | "babel-register": "^6.24.1", 18 | "classnames": "^2.2.5", 19 | "express": "^4.15.3", 20 | "isomorphic-fetch": "^2.2.1", 21 | "react": "^15.6.1", 22 | "react-dom": "^15.6.1", 23 | "react-redux": "^5.0.5", 24 | "react-router": "^3.0.5", 25 | "redux": "^3.7.2", 26 | "redux-logger": "^3.0.6", 27 | "redux-thunk": "^2.2.0" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.24.1", 31 | "babel-core": "^6.25.0", 32 | "babel-loader": "^7.1.1", 33 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 34 | "babel-plugin-transform-es2015-parameters": "^6.24.1", 35 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 36 | "babel-preset-es2015": "^6.24.1", 37 | "babel-preset-react": "^6.24.1", 38 | "chai": "^4.1.0", 39 | "css-loader": "^0.28.4", 40 | "enzyme": "^2.9.1", 41 | "eslint": "^4.3.0", 42 | "eslint-config-airbnb": "^15.1.0", 43 | "eslint-plugin-import": "^2.7.0", 44 | "eslint-plugin-jsx-a11y": "^6.0.2", 45 | "eslint-plugin-mocha": "^4.11.0", 46 | "eslint-plugin-react": "^7.1.0", 47 | "karma": "^1.7.0", 48 | "karma-chai-sinon": "^0.1.5", 49 | "karma-chrome-launcher": "^2.2.0", 50 | "karma-mocha": "^1.3.0", 51 | "karma-sourcemap-loader": "^0.3.7", 52 | "karma-webpack": "^2.0.4", 53 | "mocha": "^3.4.2", 54 | "react-addons-test-utils": "^15.6.0", 55 | "react-test-renderer": "^16.0.0", 56 | "sinon": "^2.4.1", 57 | "sinon-chai": "^2.12.0", 58 | "style-loader": "^0.18.2", 59 | "webpack": "^3.4.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app.es6: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import fs from 'fs'; 3 | import renderViewMiddleware from './middleware/renderView'; 4 | 5 | const app = express(); 6 | 7 | app.get('/api/user/cart', (req, res) => { 8 | fs.readFile('./data/cart.json', 'utf8', (err, data) => { 9 | if (err) { 10 | return res.status(404).send; 11 | } 12 | return res.send(JSON.parse(data)); 13 | }); 14 | }); 15 | 16 | app.get('/api/products/:type', (req, res) => { 17 | fs.readFile('./data/products.json', 'utf8', (err, data) => { 18 | if (err) { 19 | return res.status(404).send; 20 | } 21 | const products = JSON.parse(data); 22 | return res.send(products[req.params.type].items); 23 | }); 24 | }); 25 | 26 | app.get('/api/products', (req, res) => { 27 | fs.readFile('./data/products.json', 'utf8', (err, data) => { 28 | if (err) { 29 | return res.status(404).send; 30 | } 31 | return res.send(JSON.parse(data)); 32 | }); 33 | }); 34 | 35 | app.get('/api/blog', (req, res) => { 36 | fs.readFile('./data/blog.json', 'utf8', (err, data) => { 37 | if (err) { 38 | return res.status(404).send; 39 | } 40 | return res.send(JSON.parse(data)); 41 | }); 42 | }); 43 | 44 | app.get('/test', (req, res) => { 45 | res.send('Test route success!'); 46 | }); 47 | 48 | app.get('/*', renderViewMiddleware); 49 | 50 | // setup static files to load css 51 | app.use(express.static(__dirname)); 52 | 53 | app.listen(3000, () => { 54 | console.log('App listening on port: 3000'); 55 | }); 56 | -------------------------------------------------------------------------------- /src/assets/cart-item-placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isomorphic-dev-js/complete-isomorphic-example/953b32b2a699e5c9b9e7e096b1d47ec9034030de/src/assets/cart-item-placeholder.jpg -------------------------------------------------------------------------------- /src/assets/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Overriding semantic UI fonts to prevent font performance issues 3 | which makes examples clearer. 4 | */ 5 | #react-content *{ 6 | font-family: Arial; 7 | } 8 | 9 | #react-content .icon { 10 | font-family: 'Icons'; 11 | } 12 | 13 | #react-content .products .icon { 14 | font-size: 2em; 15 | } 16 | 17 | #react-content .products .category-title { 18 | margin-top: 10px; 19 | } 20 | 21 | #react-content .products { 22 | text-align: center; 23 | } 24 | 25 | #react-content .icon.search { 26 | margin-left: -40px; 27 | margin-top: 7px; 28 | font-size: 1.2em; 29 | } 30 | 31 | .main.container { 32 | margin-top: 7em; 33 | } 34 | 35 | .ui.positive.basic.button { 36 | display: block; 37 | margin-top: 10px; 38 | } 39 | 40 | .cart .right.aligned.content { 41 | text-align: right; 42 | } 43 | 44 | .banner { 45 | position: fixed; 46 | bottom: 0; 47 | width: 100%; 48 | background-color: #CCC; 49 | display: none; 50 | } 51 | 52 | .banner.show { 53 | display: block; 54 | } 55 | 56 | .banner .content { 57 | margin: 10px; 58 | text-align: center; 59 | } 60 | 61 | .banner .dismiss { 62 | position: absolute; 63 | right: 10px; 64 | top: 5px; 65 | } 66 | 67 | .btn-reset { 68 | background: none; 69 | border: 0; 70 | color: inherit; 71 | /* cursor: default; */ 72 | font: inherit; 73 | line-height: normal; 74 | overflow: visible; 75 | padding: 0; 76 | box-sizing: content-box; 77 | } 78 | 79 | .products .tooltip { 80 | margin-top: 10px; 81 | max-width: 200px; 82 | } 83 | -------------------------------------------------------------------------------- /src/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import Banner from './banner'; 4 | 5 | const App = (props) => { 6 | return ( 7 |