├── .gitignore
├── LICENSE
├── README.md
├── gulpfile.js
├── nodemon.json
├── package.json
├── src
├── client
│ ├── main.js
│ └── routes.js
├── server
│ ├── app.js
│ └── routes.js
└── shared
│ ├── boardgames.json
│ ├── cache.js
│ ├── components
│ ├── app.jsx
│ ├── card.jsx
│ ├── dataWrapper.jsx
│ ├── detail.jsx
│ └── home.jsx
│ ├── routeUtils.js
│ ├── routes.js
│ ├── store.js
│ └── viewUtils.js
├── views
└── pages
│ └── index.ejs
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | public/
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Elyse Kolker Gordon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iso-react-demo
2 | Example of how to build an isomorphic react app.
3 | [Slides from #strangeloop](https://speakerdeck.com/elyseko/building-isomorphic-web-apps-with-react)
4 |
5 | Runs at localhost:4000.
6 |
7 | # Setup
8 | ```
9 | npm install
10 |
11 | # if you don't have webpack
12 | npm install webpack -g
13 | ```
14 |
15 | Run the following in separate windows.
16 | ```
17 | webpack --watch
18 | gulp browser-sync
19 | npm start
20 | ```
21 |
22 | * When you start browser sync it will open a window for you automatically,
23 | since there is no index.html it will show an error. To verify it is working, test
24 | /build/browser.js
25 |
26 | # Libraries
27 | * Requires webpack to build
28 | * Browser sync is to server static assets so we don't need to bundle them
29 | into the webpack bundle for the server. Browser sync will decide what port to
30 | use automatically and log it in the terminal window.
31 |
32 | # References/Resources
33 | * [React](https://facebook.github.io/react/)
34 | * [React Router](https://github.com/rackt/react-router)
35 | * [JSX](https://facebook.github.io/react/docs/jsx-in-depth.html)
36 | * [Webpack](https://webpack.github.io/)
37 | * [Browser Sync](http://www.browsersync.io/)
38 |
39 | * [List of Isomorphic Apps](http://isomorphic.net/)
40 | * [React Nexus](https://blog.rotenberg.io/isomorphic-apps-done-right-with-react-nexus/)
41 | * [Pellet (Vevo)](https://github.com/Rebelizer/pellet)
42 |
43 | * [airbnb isomorphic article](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/)
44 | * [Webpack React How To](http://www.christianalfoni.com/articles/2015_04_19_The-ultimate-webpack-setup)
45 | * [Webpack How TO](https://github.com/petehunt/webpack-howto)
46 | * [Webpack Dev Server Help](http://stackoverflow.com/questions/27532246/how-to-use-webpack-for-development-without-webpack-dev-server)
47 | * [Exploring Isomorphic Javascript](http://nicolashery.com/exploring-isomorphic-javascript/)
48 | * [Simple Isomrophic Example](http://jmfurlott.com/tutorial-setting-up-a-simple-isomorphic-react-app/)
49 | * [Getting Started with React](https://blog.risingstack.com/the-react-way-getting-started-tutorial/)
50 | * [Webpack Env Variables](http://nicolashery.com/using-environment-variables-with-webpack-and-divshot/)
51 | * [Higher Order Components](https://gist.github.com/sebmarkbage/ef0bf1f338a7182b6775)
52 | * [Node and Webpack](http://jlongster.com/Backend-Apps-with-Webpack--Part-II)
53 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var path = require('path');
3 | var browserSync = require('browser-sync').create();
4 |
5 | gulp.task('browser-sync', function () {
6 | browserSync.init({
7 | server: path.join(__dirname, "public/")
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": true,
3 | "ignore": ["*_spec.js", "node_modules/*"]
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "iso-react-demo",
3 | "version": "0.0.0",
4 | "description": "Demo of isomorphic react app",
5 | "main": "src/server/app.js",
6 | "scripts": {
7 | "start": "./node_modules/.bin/nodemon public/build/server.js",
8 | "debug": "node-debug public/build/server.js",
9 | "test": "./node_modules/.bin/jasmine-node test --autoTest"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://elyseko@github.com/elyseko/iso-react-demo.git"
14 | },
15 | "keywords": [
16 | "react",
17 | "isomorphic"
18 | ],
19 | "author": "Elyse Kolker Gordon",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/elyseko/iso-react-demo/issues"
23 | },
24 | "homepage": "https://github.com/elyseko/iso-react-demo",
25 | "dependencies": {
26 | "babel-core": "^5.8.22",
27 | "babel-loader": "^5.3.2",
28 | "babel-plugin-dev-expression": "^0.1.0",
29 | "babel-runtime": "^5.8.20",
30 | "deep-equal": "^1.0.1",
31 | "ejs": "^2.3.3",
32 | "ejs-loader": "^0.2.1",
33 | "express": "^4.13.3",
34 | "history": "^1.9.0",
35 | "http-proxy": "^1.11.1",
36 | "invariant": "^2.1.0",
37 | "json-loader": "^0.5.2",
38 | "react": "^0.14.0-beta3",
39 | "react-dom": "^0.14.0-beta3",
40 | "react-router": "^1.0.0-beta4",
41 | "style-loader": "^0.12.3",
42 | "stylus-loader": "^1.2.1",
43 | "superagent": "^1.3.0",
44 | "warning": "^2.0.0",
45 | "webpack": "^1.11.0"
46 | },
47 | "devDependencies": {
48 | "browser-sync": "^2.9.6",
49 | "gulp": "^3.9.0",
50 | "gulp-babel": "^5.2.1",
51 | "nodemon": "^1.7.0"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/client/main.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom'
2 | import React from 'react'
3 | import routes from './routes'
4 | import Cache from '../shared/cache'
5 |
6 | let cache = new Cache(window.$data());
7 |
8 | ReactDOM.render(routes,
9 | document.getElementById('react')
10 | )
11 |
--------------------------------------------------------------------------------
/src/client/routes.js:
--------------------------------------------------------------------------------
1 | /*
2 | Client (browser) Routes
3 |
4 | - includes shared routes
5 | - sets up history in browsery compliant way
6 | */
7 |
8 | import React from "react"
9 | import { Router, IndexRoute } from "react-router"
10 | import createHistory from '$history'
11 | import sharedRoutes from "../shared/routes"
12 | import DataWrapper from '../shared/components/dataWrapper'
13 |
14 |
15 | // runs once for each component that the router
16 | // knows about
17 | function createElement(Component, props) {
18 | return
19 | }
20 |
21 | export default (
22 |
23 |
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/src/server/app.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import path from 'path';
3 |
4 | // instantiate react-router
5 | import router from './routes'
6 |
7 | const app = express();
8 |
9 | // point at the ejs templates
10 | app.set('view engine', 'ejs');
11 |
12 | //view routes
13 | app.get('/*', router)
14 |
15 | app.listen(4000, function(){
16 | console.log("running on port 4000")
17 | });
18 |
--------------------------------------------------------------------------------
/src/server/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/server';
3 |
4 | import { RoutingContext, match } from 'react-router'
5 | import routes from '../shared/routes';
6 | import createLocation from '$history'
7 | import routeUtils from '../shared/routeUtils'
8 |
9 | let createElement = (Component, props) => {
10 | props.data = props.params.data;
11 | props.params.data = undefined;
12 | return
13 | }
14 |
15 | module.exports = (req, res) => {
16 | let location = createLocation(req.url);
17 | match({ routes, location }, (error, redirectLocation, renderProps) => {
18 | if (redirectLocation)
19 | res.redirect(301, redirectLocation.pathname + redirectLocation.search)
20 | else if (error)
21 | res.status(500).send(error.message)
22 | else if (renderProps == null)
23 | res.status(404).send('Not found')
24 | else {
25 | let components = renderProps.components
26 | let requests = routeUtils.getListOfRequests(components);
27 | renderProps.createElement = createElement;
28 | // define callback
29 | let count = requests.length;
30 | let viewData = {};
31 | let callback = (err, data)=> {
32 | if(!err) {
33 | viewData[data.id] = data.result;
34 | } else {
35 | viewData[data.id] = err;
36 | }
37 | count--;
38 | if (count <= 0) {
39 | renderProps.params.data = viewData;
40 | let html = ReactDOM.renderToString();
41 | res.render('pages/index', {"title": "Test", "html": html, data: JSON.stringify(viewData)});
42 | }
43 | }
44 | routeUtils.batchRequests(requests, callback, renderProps.params);
45 | }
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/src/shared/boardgames.json:
--------------------------------------------------------------------------------
1 | {
2 | "items": [
3 | {
4 | "id": 1,
5 | "name": "M.U.L.E. The Board Game",
6 | "thumbnail": "//cf.geekdo-images.com/images/pic2634492_t.jpg",
7 | "bgg_id": 182619,
8 | "year_published": "2015"
9 | },
10 | {
11 | "id": 2,
12 | "name": "Nevermore",
13 | "thumbnail": "//cf.geekdo-images.com/images/pic2404085_t.jpg",
14 | "bgg_id": 173047,
15 | "year_published": "2015"
16 | },
17 | {
18 | "id": 3,
19 | "name": "Star Wars: Imperial Assault",
20 | "thumbnail": "//cf.geekdo-images.com/images/pic2247647_t.jpg",
21 | "bgg_id": 164153,
22 | "year_published": "2014"
23 | },
24 | {
25 | "id": 4,
26 | "name": "Treasure Hunter",
27 | "thumbnail": "//cf.geekdo-images.com/images/pic2632365_t.jpg",
28 | "bgg_id": 182189,
29 | "year_published": "2015"
30 | },
31 | {
32 | "id": 5,
33 | "name": "Machi Koro: Deluxe Edition",
34 | "thumbnail": "//cf.geekdo-images.com/images/pic2486977_t.jpg",
35 | "bgg_id": 175239,
36 | "year_published": "2015"
37 | },
38 | {
39 | "id": 6,
40 | "name": "T.I.M.E Stories",
41 | "thumbnail": "//cf.geekdo-images.com/images/pic2617634_t.png",
42 | "bgg_id": 146508,
43 | "year_published": "2015"
44 | },
45 | {
46 | "id": 7,
47 | "name": "Churchill",
48 | "thumbnail": "//cf.geekdo-images.com/images/pic2467234_t.jpg",
49 | "bgg_id": 132018,
50 | "year_published": "2015"
51 | },
52 | {
53 | "id": 8,
54 | "name": "Forbidden Stars",
55 | "thumbnail": "//cf.geekdo-images.com/images/pic2471359_t.jpg",
56 | "bgg_id": 175155,
57 | "year_published": "2015"
58 | },
59 | {
60 | "id": 9,
61 | "name": "Star Trek: Frontiers",
62 | "thumbnail": "//cf.geekdo-images.com/images/pic2627040_t.jpg",
63 | "bgg_id": 182340,
64 | "year_published": "2016"
65 | },
66 | {
67 | "id": 10,
68 | "name": "Glory to Rome",
69 | "thumbnail": "//cf.geekdo-images.com/images/pic1079512_t.png",
70 | "bgg_id": 19857,
71 | "year_published": "2005"
72 | },
73 | {
74 | "id": 11,
75 | "name": "Blood Rage",
76 | "thumbnail": "//cf.geekdo-images.com/images/pic2439223_t.jpg",
77 | "bgg_id": 170216,
78 | "year_published": "2015"
79 | },
80 | {
81 | "id": 12,
82 | "name": "Cultists of Cthulhu",
83 | "thumbnail": "//cf.geekdo-images.com/images/pic2628734_t.png",
84 | "bgg_id": 157452,
85 | "year_published": "2016"
86 | },
87 | {
88 | "id": 13,
89 | "name": "Ashes: Rise of the Phoenixborn",
90 | "thumbnail": "//cf.geekdo-images.com/images/pic2479679_t.jpg",
91 | "bgg_id": 167400,
92 | "year_published": "2015"
93 | },
94 | {
95 | "id": 14,
96 | "name": "City of Iron",
97 | "thumbnail": "//cf.geekdo-images.com/images/pic1292441_t.jpg",
98 | "bgg_id": 123499,
99 | "year_published": "2013"
100 | },
101 | {
102 | "id": 15,
103 | "name": "Legendary: A Marvel Deck Building Game",
104 | "thumbnail": "//cf.geekdo-images.com/images/pic1430769_t.jpg",
105 | "bgg_id": 129437,
106 | "year_published": "2012"
107 | },
108 | {
109 | "id": 16,
110 | "name": "Twilight Struggle",
111 | "thumbnail": "//cf.geekdo-images.com/images/pic361592_t.jpg",
112 | "bgg_id": 12333,
113 | "year_published": "2005"
114 | },
115 | {
116 | "id": 17,
117 | "name": "Codenames",
118 | "thumbnail": "//cf.geekdo-images.com/images/pic2582929_t.jpg",
119 | "bgg_id": 178900,
120 | "year_published": "2015"
121 | },
122 | {
123 | "id": 18,
124 | "name": "Dead of Winter: A Crossroads Game",
125 | "thumbnail": "//cf.geekdo-images.com/images/pic2221472_t.jpg",
126 | "bgg_id": 150376,
127 | "year_published": "2014"
128 | },
129 | {
130 | "id": 19,
131 | "name": "Star Wars: Imperial Assault – Twin Shadows",
132 | "thumbnail": "//cf.geekdo-images.com/images/pic2473289_t.jpg",
133 | "bgg_id": 175211,
134 | "year_published": "2015"
135 | },
136 | {
137 | "id": 20,
138 | "name": "Star Wars: X-Wing Miniatures Game",
139 | "thumbnail": "//cf.geekdo-images.com/images/pic1603292_t.jpg",
140 | "bgg_id": 103885,
141 | "year_published": "2012"
142 | },
143 | {
144 | "id": 21,
145 | "name": "Mage Knight Board Game",
146 | "thumbnail": "//cf.geekdo-images.com/images/pic1083380_t.jpg",
147 | "bgg_id": 96848,
148 | "year_published": "2011"
149 | },
150 | {
151 | "id": 22,
152 | "name": "Android: Netrunner",
153 | "thumbnail": "//cf.geekdo-images.com/images/pic1324609_t.jpg",
154 | "bgg_id": 124742,
155 | "year_published": "2012"
156 | },
157 | {
158 | "id": 23,
159 | "name": "Legendary: Secret Wars - Volume 1",
160 | "thumbnail": "//cf.geekdo-images.com/images/pic2545261_t.png",
161 | "bgg_id": 175156,
162 | "year_published": "2015"
163 | },
164 | {
165 | "id": 24,
166 | "name": "La Granja",
167 | "thumbnail": "//cf.geekdo-images.com/images/pic2031777_t.jpg",
168 | "bgg_id": 146886,
169 | "year_published": "2014"
170 | },
171 | {
172 | "id": 25,
173 | "name": "Eldritch Horror",
174 | "thumbnail": "//cf.geekdo-images.com/images/pic1872452_t.jpg",
175 | "bgg_id": 146021,
176 | "year_published": "2013"
177 | },
178 | {
179 | "id": 26,
180 | "name": "Terra Mystica",
181 | "thumbnail": "//cf.geekdo-images.com/images/pic1356616_t.jpg",
182 | "bgg_id": 120677,
183 | "year_published": "2012"
184 | },
185 | {
186 | "id": 27,
187 | "name": "CVlizations",
188 | "thumbnail": "//cf.geekdo-images.com/images/pic2622816_t.jpg",
189 | "bgg_id": 181494,
190 | "year_published": "2015"
191 | },
192 | {
193 | "id": 28,
194 | "name": "Mission: Red Planet (Second Edition)",
195 | "thumbnail": "//cf.geekdo-images.com/images/pic2499748_t.jpg",
196 | "bgg_id": 176920,
197 | "year_published": "2015"
198 | },
199 | {
200 | "id": 29,
201 | "name": "Mistfall",
202 | "thumbnail": "//cf.geekdo-images.com/images/pic2410035_t.png",
203 | "bgg_id": 168274,
204 | "year_published": "2015"
205 | },
206 | {
207 | "id": 30,
208 | "name": "Mice and Mystics",
209 | "thumbnail": "//cf.geekdo-images.com/images/pic1312072_t.jpg",
210 | "bgg_id": 124708,
211 | "year_published": "2012"
212 | },
213 | {
214 | "id": 31,
215 | "name": "Evolution",
216 | "thumbnail": "//cf.geekdo-images.com/images/pic2558560_t.jpg",
217 | "bgg_id": 155703,
218 | "year_published": "2014"
219 | },
220 | {
221 | "id": 32,
222 | "name": "The Voyages of Marco Polo",
223 | "thumbnail": "//cf.geekdo-images.com/images/pic2461346_t.png",
224 | "bgg_id": 171623,
225 | "year_published": "2015"
226 | },
227 | {
228 | "id": 33,
229 | "name": "The Lord of the Rings: The Card Game",
230 | "thumbnail": "//cf.geekdo-images.com/images/pic906495_t.jpg",
231 | "bgg_id": 77423,
232 | "year_published": "2011"
233 | },
234 | {
235 | "id": 34,
236 | "name": "Scythe",
237 | "thumbnail": "//cf.geekdo-images.com/images/pic2323719_t.jpg",
238 | "bgg_id": 169786,
239 | "year_published": "2016"
240 | },
241 | {
242 | "id": 35,
243 | "name": "Imperial Settlers",
244 | "thumbnail": "//cf.geekdo-images.com/images/pic2000680_t.jpg",
245 | "bgg_id": 154203,
246 | "year_published": "2014"
247 | },
248 | {
249 | "id": 36,
250 | "name": "Caverna: The Cave Farmers",
251 | "thumbnail": "//cf.geekdo-images.com/images/pic1790789_t.jpg",
252 | "bgg_id": 102794,
253 | "year_published": "2013"
254 | },
255 | {
256 | "id": 37,
257 | "name": "Viceroy",
258 | "thumbnail": "//cf.geekdo-images.com/images/pic2254354_t.jpg",
259 | "bgg_id": 157526,
260 | "year_published": "2014"
261 | },
262 | {
263 | "id": 38,
264 | "name": "504",
265 | "thumbnail": "//cf.geekdo-images.com/images/pic2570515_t.jpg",
266 | "bgg_id": 175878,
267 | "year_published": "2015"
268 | },
269 | {
270 | "id": 39,
271 | "name": "Warhammer Quest: The Adventure Card Game",
272 | "thumbnail": "//cf.geekdo-images.com/images/pic2625794_t.jpg",
273 | "bgg_id": 181521,
274 | "year_published": "2015"
275 | },
276 | {
277 | "id": 40,
278 | "name": "Flick 'em Up!",
279 | "thumbnail": "//cf.geekdo-images.com/images/pic2439671_t.png",
280 | "bgg_id": 169124,
281 | "year_published": "2015"
282 | },
283 | {
284 | "id": 41,
285 | "name": "Shadows of Brimstone: City of the Ancients",
286 | "thumbnail": "//cf.geekdo-images.com/images/pic2037825_t.jpg",
287 | "bgg_id": 146791,
288 | "year_published": "2014"
289 | },
290 | {
291 | "id": 42,
292 | "name": "Discoveries",
293 | "thumbnail": "//cf.geekdo-images.com/images/pic2571301_t.jpg",
294 | "bgg_id": 171669,
295 | "year_published": "2015"
296 | },
297 | {
298 | "id": 43,
299 | "name": "Mysterium",
300 | "thumbnail": "//cf.geekdo-images.com/images/pic2601683_t.jpg",
301 | "bgg_id": 181304,
302 | "year_published": "2015"
303 | },
304 | {
305 | "id": 44,
306 | "name": "Brass",
307 | "thumbnail": "//cf.geekdo-images.com/images/pic261878_t.jpg",
308 | "bgg_id": 28720,
309 | "year_published": "2007"
310 | },
311 | {
312 | "id": 45,
313 | "name": "Vs. System 2PCG",
314 | "thumbnail": "//cf.geekdo-images.com/images/pic2630832_t.jpg",
315 | "bgg_id": 178892,
316 | "year_published": "2015"
317 | },
318 | {
319 | "id": 46,
320 | "name": "New York 1901",
321 | "thumbnail": "//cf.geekdo-images.com/images/pic2515532_t.jpg",
322 | "bgg_id": 174660,
323 | "year_published": "2015"
324 | },
325 | {
326 | "id": 47,
327 | "name": "Rum & Bones",
328 | "thumbnail": "//cf.geekdo-images.com/images/pic2299556_t.jpg",
329 | "bgg_id": 168788,
330 | "year_published": "2015"
331 | },
332 | {
333 | "id": 48,
334 | "name": "Shadowrun: Crossfire",
335 | "thumbnail": "//cf.geekdo-images.com/images/pic2060466_t.jpg",
336 | "bgg_id": 135382,
337 | "year_published": "2014"
338 | },
339 | {
340 | "id": 49,
341 | "name": "Roll for the Galaxy",
342 | "thumbnail": "//cf.geekdo-images.com/images/pic1473629_t.jpg",
343 | "bgg_id": 132531,
344 | "year_published": "2014"
345 | },
346 | {
347 | "id": 50,
348 | "name": "Robinson Crusoe: Adventures on the Cursed Island",
349 | "thumbnail": "//cf.geekdo-images.com/images/pic1715554_t.jpg",
350 | "bgg_id": 121921,
351 | "year_published": "2012"
352 | }
353 | ]
354 | }
355 |
--------------------------------------------------------------------------------
/src/shared/cache.js:
--------------------------------------------------------------------------------
1 | const cache = {};
2 |
3 | export default class Cache {
4 | constructor(data) {
5 | this.cache = cache;
6 | this.build(data);
7 | }
8 |
9 | add(key, data) {
10 | this.cache[key] = data;
11 | }
12 |
13 | delete(key) {
14 | delete this.cache[key];
15 | }
16 |
17 | get(key) {
18 | return this.cache[key];
19 | }
20 |
21 | exists(key) {
22 | return !! this.cache[key];
23 | }
24 |
25 | build(data) {
26 | if (data) {
27 | Object.keys(data).forEach( (item, index) => {
28 | this.add(item, data[item]);
29 | });
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/shared/components/app.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | Parent react component
3 |
4 | Header, footer and other components common to all
5 | routes go in here
6 | */
7 |
8 | import React from 'react'
9 | import { Link } from "react-router"
10 |
11 | export default class App extends React.Component {
12 |
13 | render() {
14 | return (
15 |
16 |
17 | Isomorphic React Example
18 |
19 |
20 | {this.props.children}
21 |
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/shared/components/card.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from "react-router"
3 |
4 | export default class Card extends React.Component {
5 | render() {
6 | return (
7 |
8 |
9 | {this.props.name}
10 |
11 | Published in {this.props.year_published}
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/shared/components/dataWrapper.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Store from '../store'
3 | import routeUtils from '../routeUtils'
4 |
5 | const store = new Store();
6 |
7 | export default class DataWrapper extends React.Component {
8 |
9 | constructor(props) {
10 | super();
11 | this.state = {data:{}};
12 | this.getData(props);
13 | }
14 |
15 | componentWillReceiveProps(nextProps) {
16 | this.getData(nextProps);
17 | }
18 |
19 | getData(props) {
20 | let requests = routeUtils.getListOfRequests([props.component])
21 | let newData = {}
22 | let count = requests.length;
23 | if (count) {
24 | requests.forEach((item, index)=> {
25 | // check item in case a component implements requestData
26 | // but does not return an item
27 | if (item) {
28 | let options = {};
29 | let key = ""
30 | if (item.hasOwnProperty("params")) {
31 | Object.keys(item.params).forEach((option, index) => {
32 | key += options[option] = props.params[option];
33 | });
34 | }
35 | this.updateState = this.updateState.bind(this)
36 | //TODO: handle cleanup in component will mount
37 | store[item.request](this.updateState, item.request, options);
38 | } else {
39 | console.error("static method requestData must return a valid store request")
40 | }
41 | });
42 | }
43 | }
44 |
45 | updateState(err, item) {
46 | let obj = this.state.data || {}
47 | obj[item.id] = item.result
48 | if (this.props) {
49 | this.setState({data: obj})
50 | } else {
51 | this.state.data = obj
52 | }
53 | }
54 |
55 | render() {
56 | return (
57 |
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/shared/components/detail.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import viewUtils from '../viewUtils'
3 |
4 | let GET_CARD = "getCard";
5 | let GET_RELATED = "getRelated";
6 |
7 | export default class Detail extends React.Component {
8 |
9 | static requestData() {
10 | return [{request: GET_CARD, params: {id: ":id"}}, {request: GET_RELATED, params: {id: ":id"}}];
11 | }
12 |
13 | render() {
14 | // TODO: check for error and/or loading states
15 | let item = this.props.data[GET_CARD + this.props.params.id];
16 | let related = this.props.data[GET_RELATED + this.props.params.id];
17 | if (item) {
18 | return (
19 |
20 |
21 |
22 |
{item.name}
23 |

24 |
25 |
26 |
27 | {viewUtils.renderCards(related)}
28 |
29 |
30 | )
31 | } else {
32 | return (
33 |
34 | Loading
35 |
36 | )
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/shared/components/home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from "react-router"
3 | import viewUtils from "../viewUtils"
4 |
5 | export default class Home extends React.Component {
6 |
7 | constructor(props) {
8 | super();
9 | }
10 |
11 | static requestData() {
12 | return [{request: "getCards"}];
13 | }
14 |
15 | render() {
16 | return (
17 |
18 |
22 |
23 | {viewUtils.renderCards(this.props.data.getCards)}
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/shared/routeUtils.js:
--------------------------------------------------------------------------------
1 | /*
2 | Some utility functions that are used by both the server and the browser
3 | routing logic
4 | */
5 |
6 | import Store from './store'
7 | const store = new Store();
8 |
9 | module.exports = {
10 | /*
11 | compiles a list of requests from the static method
12 | requestData
13 | */
14 | getListOfRequests: function(components) {
15 | let requests = [];
16 | components.forEach((item, index) => {
17 | // make sure this is really a component
18 | if (!item) return;
19 | // check for static method on parent components
20 | if (item.hasOwnProperty("requestData")) {
21 | requests = requests.concat(item.requestData());
22 | }
23 | });
24 | return requests;
25 | },
26 | /*
27 | makes a batch of calls to the store
28 | */
29 | batchRequests: function(requests, callback, params) {
30 | requests.forEach((item, index)=> {
31 | // check item in case a component implements requestData
32 | // but does not return an item
33 | if (item) {
34 | let options = {};
35 | if (item.hasOwnProperty("params")) {
36 | Object.keys(item.params).forEach((option, index) => {
37 | options[option] = params[option];
38 | });
39 | }
40 | store[item.request](callback, item.request, options);
41 | } else {
42 | console.error("static method requestData must return a valid store request")
43 | }
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/shared/routes.js:
--------------------------------------------------------------------------------
1 | /*
2 | Shared routes to be used on the server and in the browser
3 |
4 | - Must be included into an instance of react-router
5 | */
6 |
7 | import React from "react";
8 | import { Route, IndexRoute } from "react-router";
9 | import App from "./components/App";
10 | import Home from "./components/Home";
11 | import Detail from "./components/Detail";
12 |
13 | // console.log("history", createHistory)
14 |
15 | export default (
16 |
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/shared/store.js:
--------------------------------------------------------------------------------
1 | /*
2 | Sample API module
3 |
4 | - Keeping it simple using callbacks, could be done with promises instead
5 | - Handles pushing data into the cache when calls are made
6 | */
7 |
8 | import boardgames from './boardgames.json'
9 | import Cache from './cache'
10 | let cache = new Cache(); //TODO should we instantiate a better way?
11 |
12 | //stubbed data
13 | export default class Store {
14 |
15 | // fake a get call
16 | _get(callback, data, id) {
17 | let err = null; //placeholder for real calls
18 | //call API - for this example fake an async call with setTimeout
19 | setTimeout( () => {
20 | //set the response in the cache - cache exists across
21 | //multiple calls and this solves a problem with having
22 | //to make a state existence check in componentWillMount
23 | let dataToCache = data;
24 | if (err) {
25 | dataToCache = err;
26 | }
27 | cache.add(id, dataToCache);
28 | return callback(err, {result: dataToCache, id: id});
29 | }, 200);
30 | }
31 |
32 | _checkCache(id) {
33 | //check cache to see if ids exist
34 | if (cache.exists(id)) {
35 | console.info("LOG: cache hit");
36 | return true;
37 | }
38 | }
39 |
40 | getCards(callback, cardsId, options) {
41 | let err = null;
42 | if (this._checkCache(cardsId)) {
43 | callback(err, {result: cache.get(cardsId), id: cardsId});
44 | } else {
45 | this._get(callback, boardgames.items, cardsId);
46 | }
47 | }
48 |
49 | getCard(callback, cardId, options) {
50 | let err = null;
51 |
52 | if (!options || !options.id) {
53 | err = {err: "missing options or no id on request"}
54 | return callback(err, {id: cardId});
55 | }
56 |
57 | let cacheId = cardId + options.id;
58 | if (this._checkCache(cacheId)) {
59 | callback(err, {result: cache.get(cacheId), id: cacheId});
60 | } else {
61 | this._get(callback, boardgames.items[options.id-1], cacheId);
62 | }
63 | }
64 |
65 | getRelated(callback, relatedId, options) {
66 | let err = null;
67 |
68 | if (!options || !options.id) {
69 | err = {err: "missing options or no id on request"}
70 | return callback(err, {id: cardId});
71 | }
72 |
73 | let cacheId = relatedId + options.id;
74 | if (this._checkCache(cacheId)) {
75 | return callback(err, {result: cache.get(cacheId), id: cacheId});
76 | } else {
77 | //TODO: hook up to actual related ap
78 | this._get(callback, boardgames.items.slice(0,4), cacheId);
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/src/shared/viewUtils.js:
--------------------------------------------------------------------------------
1 | /*
2 | Some utility functions that are used by multiple react components
3 | */
4 | import React from 'react'
5 | import Card from './components/card'
6 |
7 | module.exports = {
8 | renderCards: function(cardData) {
9 | let cards = [];
10 | let items = cardData;
11 | if (items) {
12 | Object.keys(items).forEach( (item, index) => {
13 |
14 | let currentCard = items[item];
15 | cards.push(
16 |
17 |
18 | )
19 | });
20 | }
21 | return cards;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/views/pages/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= title%>
4 |
5 |
6 |
7 | <%- html%>
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var Webpack = require('webpack');
2 | var path = require('path');
3 | var nodeModulesPath = path.resolve(__dirname, 'node_modules');
4 | var buildPath = path.resolve(__dirname, 'public/build/');
5 | var serverPath = path.resolve(__dirname, 'src/server', 'app.js');
6 | var browserPath = path.resolve(__dirname, 'src/client', 'main.js');
7 |
8 | var definePlugin = new Webpack.DefinePlugin({
9 | __DEV__: JSON.stringify(
10 | JSON.parse(process.env.BUILD_DEV || 'true')
11 | ),
12 | __PROD__: JSON.stringify(
13 | JSON.parse(process.env.BUILD_PROD || 'false')
14 | )
15 | });
16 |
17 | module.exports = [
18 | {
19 | target: 'node',
20 | devtool: 'source-map',
21 | debug: true,
22 | entry: {
23 | app: [serverPath]
24 | },
25 | output: {
26 | path: buildPath,
27 | filename: 'server.js',
28 | libraryTarget: 'umd'
29 | },
30 | externals: [/^[a-z\-0-9]+$/, 'react-router/lib/Location.js', 'react-dom/server'],
31 | module: {
32 | loaders: [
33 | {
34 | test: /\.json$/,
35 | loader: 'json-loader'
36 | },
37 | {
38 | test: /\.jsx?$/,
39 | exclude: /nodeModulesPath/,
40 | loader: 'babel'
41 | },
42 | {
43 | test: /\.ejs$/,
44 | loader: 'ejs-loader?variable=data'
45 | }
46 | ]
47 | },
48 | resolve: {
49 | extensions: ['', '.js', '.json', '.jsx'],
50 | alias: {
51 | '$history': 'history/lib/createLocation'
52 | }
53 | },
54 | plugins: [definePlugin]
55 | },
56 | {
57 | devtool: 'eval',
58 | debug: true,
59 | entry: {
60 | app:['./src/client/main.js']
61 | },
62 | output: {
63 | path: buildPath,
64 | publicPath: '/build/',
65 | filename: 'browser.js'
66 | },
67 | module: {
68 | loaders: [
69 | {
70 | test: /\.json$/,
71 | loader: 'json-loader',
72 | exclude: /nodeModulesPath/
73 | },
74 | {
75 | test: /\.jsx?$/,
76 | exclude: /nodeModulesPath/,
77 | loader: 'babel-loader'
78 | },
79 | {
80 | test: /\.ejs$/,
81 | loader: 'ejs-loader?variable=data',
82 | exclude: /nodeModulesPath/
83 | }
84 | ]
85 | },
86 | resolve: {
87 | extensions: ['', '.js', '.json', '.jsx'],
88 | alias: {
89 | '$history':'history/lib/createBrowserHistory'
90 | }
91 | },
92 | plugins: [new Webpack.NoErrorsPlugin(), definePlugin]
93 | }
94 | ];
95 |
--------------------------------------------------------------------------------