Last active
August 29, 2015 14:25
-
-
Save smontlouis/fbcd61fe004771f3ca63 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// import favicon from 'serve-favicon'; | |
import compression from 'compression'; | |
import config from '../config'; | |
import esteHeaders from '../lib/estemiddleware'; | |
import express from 'express'; | |
import intlMiddleware from '../lib/intlmiddleware'; | |
import render from './render'; | |
import userState from './userstate'; | |
import fetchData from './fetchData'; | |
const app = express(); | |
// Add Este.js headers for React related routes only. | |
if (!config.isProduction) | |
app.use(esteHeaders()); | |
app.use(compression()); | |
// TODO: Add favicon. | |
// app.use(favicon('assets/img/favicon.ico')) | |
// TODO: Move to CDN. | |
app.use('/build', express.static('build')); | |
app.use('/assets', express.static('assets')); | |
// Load translations, fallback to defaultLocale if no translation is available. | |
app.use(intlMiddleware({ | |
defaultLocale: config.defaultLocale | |
})); | |
// Load state extras for current user. | |
//app.use(userState()); | |
/** | |
* 1. All the magic will be here | |
*/ | |
app.use(fetchData()); | |
app.get('*', (req, res, next) => { | |
render(req, res).catch(next); | |
}); | |
app.on('mount', () => { | |
console.log('App is available at %s', app.mountpath); | |
}); | |
export default app; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Promise from 'bluebird'; | |
import Immutable from 'immutable'; | |
import Router from 'react-router'; | |
import Flux from '../../client/lib/flux/flux'; | |
import store from '../../client/app/store'; | |
import initialState from '../initialstate'; | |
import stateMerger from '../lib/merger'; | |
import routes from '../../client/routes'; | |
import allActions from '../../client/app/allActions'; | |
/** | |
* 2. I'm doing shit like loading all actions, init flux, and init router. Maybe it's dirty, dunno. | |
*/ | |
export default function fetchData() { | |
return (req, res, next) => { | |
let appState = Immutable.fromJS(initialState).mergeWith(stateMerger, req.userState, {intl: req.intl}).toJS(); | |
const flux = new Flux(store, appState); | |
const actions = allActions.reduce((actions, {feature, create}) => { | |
const dispatch = (action, payload) => flux.dispatch(action, payload, {feature}); | |
const featureActions = create(dispatch); | |
return {...actions, [feature]: featureActions}; | |
}, {}); | |
Router.run(routes, req.originalUrl, (Root, routerState) => { | |
Promise.all(routerState.routes | |
.filter(route => route.handler.fetchData) | |
.map(route => { | |
return route.handler.fetchData(actions); //Pass actions here - Maybe there is a better solution. Honestly I've no idea what I'm doing :) | |
}) | |
).then(() => { | |
req.appState = flux.state.toJS(); //Get updated state | |
next(); | |
}); | |
}); | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import DocumentTitle from 'react-document-title'; | |
import Html from './html.react'; | |
import Promise from 'bluebird'; | |
import React from 'react'; | |
import Router from 'react-router'; | |
import config from '../config'; | |
import routes from '../../client/routes'; | |
export default function render(req, res) { | |
return renderPage(req, res); | |
} | |
function renderPage(req, res) { | |
return new Promise((resolve, reject) => { | |
const router = Router.create({ | |
routes, | |
location: req.originalUrl, | |
onError: reject, | |
onAbort: (abortReason) => { | |
// Some requireAuth higher order component requested redirect. | |
if (abortReason.constructor.name === 'Redirect') { | |
const {to, params, query} = abortReason; | |
const path = router.makePath(to, params, query); | |
res.redirect(path); | |
resolve(); | |
return; | |
} | |
reject(abortReason); | |
} | |
}); | |
router.run((Handler, routerState) => { | |
/** | |
* 3. We don't need appState anymore, so just pass req.appState here | |
*/ | |
const html = getPageHtml(Handler, req.appState); | |
const notFound = routerState.routes.some(route => route.name === 'not-found'); | |
const status = notFound ? 404 : 200; | |
res.status(status).send(html); | |
resolve(); | |
}); | |
}); | |
} | |
function getPageHtml(Handler, appState) { | |
const appHtml = `<div id="app">${ | |
React.renderToString(<Handler initialState={appState} />) | |
}</div>`; | |
const appScriptSrc = config.isProduction | |
? '/build/app.js?v=' + config.version | |
: '//localhost:8888/build/app.js'; | |
let scriptHtml = ` | |
<script> | |
(function() { | |
window._appState = ${JSON.stringify(appState)}; | |
var app = document.createElement('script'); app.type = 'text/javascript'; app.async = true; | |
var src = '${appScriptSrc}'; | |
// IE<11 and Safari need Intl polyfill. | |
if (!window.Intl) src = src.replace('.js', 'intl.js'); | |
app.src = src; | |
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(app, s); | |
})(); | |
</script>`; | |
if (config.isProduction && config.googleAnalyticsId !== 'UA-XXXXXXX-X') | |
scriptHtml += ` | |
<script> | |
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]= | |
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date; | |
e=o.createElement(i);r=o.getElementsByTagName(i)[0]; | |
e.src='//www.google-analytics.com/analytics.js'; | |
r.parentNode.insertBefore(e,r)}(window,document,'script','ga')); | |
ga('create','${config.googleAnalyticsId}');ga('send','pageview'); | |
</script>`; | |
const title = DocumentTitle.rewind(); | |
return '<!DOCTYPE html>' + React.renderToStaticMarkup( | |
<Html | |
bodyHtml={appHtml + scriptHtml} | |
isProduction={config.isProduction} | |
title={title} | |
version={config.version} | |
/> | |
); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as authActions from '../auth/actions'; | |
import * as todosActions from '../todos/actions'; | |
/** | |
* 4. Meh. | |
*/ | |
export default [authActions, todosActions]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Buttons from './buttons.react'; | |
import Component from '../components/component.react'; | |
import DocumentTitle from 'react-document-title'; | |
import NewTodo from './newtodo.react'; | |
import React from 'react'; | |
import ToCheck from './tocheck.react'; | |
import Todos from './todos.react'; | |
export default class Page extends Component { | |
static propTypes = { | |
actions: React.PropTypes.object.isRequired, | |
msg: React.PropTypes.object.isRequired, | |
todos: React.PropTypes.object.isRequired | |
}; | |
/** | |
* 5. I define a static fetchData method, taking actions as parameters, so I can use it server side too. | |
*/ | |
static fetchData(actions) { | |
return actions.todos.loadAllTodos(); | |
} | |
componentDidMount() { | |
const {todos, actions} = this.props; | |
if (!todos.list.size > 0) | |
Page.fetchData(actions); | |
} | |
render() { | |
const { | |
todos: {newTodo, list}, | |
actions: {todos: actions}, | |
msg: {todos: msg} | |
} = this.props; | |
return ( | |
<DocumentTitle title={msg.title}> | |
<div className="todos-page"> | |
<NewTodo {...{newTodo, actions, msg}} /> | |
{/* It's just shorter syntax for: | |
<NewTodo actions={actions} msg={msg} newTodo={newTodo} /> | |
*/} | |
<Todos {...{list, actions, msg}} /> | |
<Buttons clearAllEnabled={list.size > 0} {...{actions, msg}} /> | |
<ToCheck msg={msg.toCheck} /> | |
</div> | |
</DocumentTitle> | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import './app.styl'; | |
import Component from '../components/component.react'; | |
import Footer from './footer.react'; | |
import Header from './header.react'; | |
import React from 'react'; | |
import flux from '../lib/flux'; | |
import store from './store'; | |
import {RouteHandler} from 'react-router'; | |
import {createValidate} from '../validate'; | |
/** | |
* 6. Meh. | |
*/ | |
import actions from './allActions'; | |
@flux(store) | |
export default class App extends Component { | |
static propTypes = { | |
flux: React.PropTypes.object.isRequired, | |
msg: React.PropTypes.object.isRequired, | |
users: React.PropTypes.object.isRequired | |
}; | |
componentWillMount() { | |
this.createActions(); | |
} | |
createActions() { | |
const {flux, msg} = this.props; | |
const validate = createValidate(msg); | |
this.actions = actions.reduce((actions, {feature, create}) => { | |
const dispatch = (action, payload) => flux.dispatch(action, payload, {feature}); | |
const featureActions = create(dispatch, validate, msg[feature]); | |
return {...actions, [feature]: featureActions}; | |
}, {}); | |
} | |
render() { | |
const props = {...this.props, actions: this.actions}; | |
const {users: {viewer}, msg} = props; | |
return ( | |
<div className="page"> | |
{/* Pass only what's needed. Law of Demeter ftw. */} | |
<Header msg={msg} viewer={viewer} /> | |
<RouteHandler {...props} /> | |
<Footer msg={msg} /> | |
</div> | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Promise from 'bluebird'; | |
export const actions = create(); | |
export const feature = 'todos'; | |
// When everything is constant, who needs to SCREAM_CONSTANTS? | |
const maxTitleLength = 42; | |
//TESTING | |
var Api = { | |
get: function() { | |
return new Promise(function(resolve, reject) { | |
setTimeout(() => { | |
const response = [{id: 'sh5s3g5baqle', title: 'meow meow meow'}, {id:'fde4fyf7p87d', title: 'make make make'}]; | |
resolve(response); | |
}, 100); | |
}); | |
} | |
}; | |
export function create(dispatch, validate) { | |
return { | |
/** | |
* 7. Some fake loadTodos to test | |
*/ | |
loadAllTodos() { | |
return new Promise((resolve, reject) => { | |
Api.get() | |
.then(res => { | |
dispatch(actions.loadAllTodos, res); | |
resolve(res); | |
}) | |
.catch(err => { | |
reject(err); | |
}); | |
}); | |
}, | |
addHundredTodos() { | |
dispatch(actions.addHundredTodos); | |
}, | |
addTodo(todo) { | |
const title = todo.title.trim(); | |
if (!title) return; | |
dispatch(actions.addTodo, todo); | |
}, | |
clearAll() { | |
dispatch(actions.clearAll); | |
}, | |
deleteTodo(todo) { | |
dispatch(actions.deleteTodo, todo); | |
}, | |
setNewTodoField({target: {name, value}}) { | |
switch (name) { | |
case 'title': | |
value = value.slice(0, maxTitleLength); | |
break; | |
} | |
dispatch(actions.setNewTodoField, {name, value}); | |
} | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Todo from './todo'; | |
import getRandomString from '../lib/getrandomstring'; | |
import {Range, Record} from 'immutable'; | |
import {actions} from './actions'; | |
// Records are good. https://facebook.github.io/immutable-js/docs/#/Record | |
const initialState = new (Record({ | |
list: [], | |
newTodo: null | |
})); | |
const revive = state => initialState.merge({ | |
list: state.get('list').map(todo => new Todo(todo)), | |
newTodo: new Todo(state.get('newTodo')) | |
}); | |
function getRandomTodos(howMuch) { | |
return Range(0, howMuch).map(() => { | |
const id = getRandomString(); | |
return new Todo({id, title: `Item #${id}`}); | |
}).toArray(); | |
} | |
export default function(state = initialState, action, payload) { | |
if (!action) state = revive(state); | |
switch (action) { | |
/** | |
* 8. Update state | |
*/ | |
case actions.loadAllTodos: | |
const todos = payload.map((item) => new Todo(item)); | |
return state.update('list', list => list.push(...todos)); | |
case actions.addHundredTodos: | |
return state.update('list', list => list.push(...getRandomTodos(10))); | |
case actions.addTodo: | |
return state | |
.update('list', list => { | |
const newTodo = payload.merge({id: getRandomString()}); | |
return list.push(newTodo); | |
}) | |
.set('newTodo', new Todo); | |
case actions.clearAll: | |
return state | |
.update('list', list => list.clear()) | |
.set('newTodo', new Todo); | |
case actions.deleteTodo: | |
return state.update('list', list => list.delete(list.indexOf(payload))); | |
case actions.setNewTodoField: | |
return state.setIn(['newTodo', payload.name], payload.value); | |
} | |
return state; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment