diff --git a/app/client.jsx b/app/client.jsx new file mode 100644 index 0000000..eb0c861 --- /dev/null +++ b/app/client.jsx @@ -0,0 +1,35 @@ + +import "babel/polyfill"; +import React from "react"; +import BrowserHistory from "react-router/lib/BrowserHistory"; +import Location from "react-router/lib/Location"; +import queryString from "query-string"; +import createStore from "./redux/store"; +import ApiClient from "./helpers/ApiClient"; +import universalRouter from "./helpers/universalRouter"; + +const history = new BrowserHistory(); +const client = new ApiClient(); + +const dest = document.getElementById('content'); +const store = createStore(client, window.__data); +const search = document.location.search; +const query = search && queryString.parse(search); // TODO: research the specifics of this +const location = new Location(document.location.pathname, query); + +universalRouter(location, history, store) + .then(({ component }) => { + React.render(component, dest); + if (__DEVTOOLS__) { +console.log("devtois"); + const { DevTools, DebugPanel, LogMonitor } = require('redux-devtools/lib/react'); + React.render(
+ { component } + + + +
, dest); + + } + }) + diff --git a/app/containers/Application.jsx b/app/containers/Application.jsx index 8bfc1bb..892f82c 100644 --- a/app/containers/Application.jsx +++ b/app/containers/Application.jsx @@ -1,16 +1,13 @@ import React from "react"; +import { Link } from "react-router"; import styles from "./Application.css"; -//TODO: this code sux -import { renderDevTools } from "../../config/mainRenderer"; - export default class Application extends React.Component { render() { return
-

This is an applidjfcation container

+

This is an application container

{ this.props.children } - { renderDevTools() }
} } diff --git a/app/helpers/ApiClient.js b/app/helpers/ApiClient.js new file mode 100644 index 0000000..08dc86b --- /dev/null +++ b/app/helpers/ApiClient.js @@ -0,0 +1,37 @@ +import superagent from 'superagent'; + +class ApiClient_ { + constructor(req) { + ['get', 'post', 'put', 'patch', 'del']. + forEach((method) => { + this[method] = (path, options) => { + return new Promise((resolve, reject) => { + const request = superagent[method](this.formatUrl(path)); + if (options && options.params) { + request.query(options.params); + } + if (options && options.data) { + request.send(options.data); + } + request.end((err, res) => { + if (err) { + reject((res && res.body) || err); + } else { + resolve(res.body); + } + }); + }); + }; + }); + } + + /* This was originally a standalone function outside of this class, but babel kept breaking, and this fixes it */ + formatUrl(path) { + const adjustedPath = path[0] !== '/' ? '/' + path : path; + //TODO this will need to be adjusted + return '/api' + adjustedPath; + } +} +const ApiClient = ApiClient_; + +export default ApiClient; diff --git a/app/helpers/universalRouter.js b/app/helpers/universalRouter.js new file mode 100644 index 0000000..389f4bc --- /dev/null +++ b/app/helpers/universalRouter.js @@ -0,0 +1,61 @@ +import React from 'react'; +import Router from 'react-router'; +import createRoutes from '../routes'; +import { Provider } from 'react-redux'; + +const getFetchData = (component = {}) => { + //TODO: research this WrappedComponent attribue + return component.WrappedComponent ? + getFetchData(component.WrappedComponent) : + component.fetchData; +}; + +export function createTransitionHook(store) { + return (nextState, transition, callback) => { + const { params, location: { query } } = nextState; + const promises = nextState.branch + .map(route => route.component) // pull out individual route components + .filter((component) => getFetchData(component)) // only look at ones with a static fetchData() + .map(getFetchData) // pull out fetch data methods + .map(fetchData => fetchData(store, params, query || {})); // call fetch data methods and save promises + Promise.all(promises) + .then(() => { + callback(); // can't just pass callback to then() because callback assumes first param is error + }, (error) => { + callback(error); + }); + }; +} + +export default function universalRouter(location, history, store) { + const routes = createRoutes(store); + return new Promise((resolve, reject) => { + Router.run(routes, location, [createTransitionHook(store)], (error, initialState, transition) => { + if (error) { + return reject(error); + } + + if (transition && transition.redirectInfo) { + return resolve({ + transition, + isRedirect: true + }); + } + + if (history) { // only on client side + initialState.history = history; + } + + const component = ( + + {() => } + + ); + + return resolve({ + component, + isRedirect: false + }); + }); + }); +} diff --git a/app/redux/middleware/clientMiddleware.js b/app/redux/middleware/clientMiddleware.js new file mode 100644 index 0000000..eea6c5f --- /dev/null +++ b/app/redux/middleware/clientMiddleware.js @@ -0,0 +1,25 @@ +export default function clientMiddleware(client) { + return ({dispatch, getState}) => { + return next => action => { + if (typeof action === 'function') { + return action(dispatch, getState); + } + + const { promise, types, ...rest } = action; + if (!promise) { + return next(action); + } + + const [REQUEST, SUCCESS, FAILURE] = types; + next({...rest, type: REQUEST}); + return promise(client).then( + (result) => next({...rest, result, type: SUCCESS}), + (error) => next({...rest, error, type: FAILURE}) + ).catch((error)=> { + console.error('MIDDLEWARE ERROR:', error); + next({...rest, error, type: FAILURE}); + }); + }; + }; +} + diff --git a/app/redux/store.js b/app/redux/store.js index e99860d..f4a2f4a 100644 --- a/app/redux/store.js +++ b/app/redux/store.js @@ -1,34 +1,30 @@ import { createStore, applyMiddleware, compose } from 'redux'; - -//TODO: implement middleware for async actions -// import createMiddleware from './middleware/clientMiddleware'; +import createMiddleware from './middleware/clientMiddleware'; export default function createApiClientStore(client, data) { - // const middleware = createMiddleware(client); + const middleware = createMiddleware(client); let finalCreateStore; - if ( __DEVTOOLS__) { - const { devTools, persistState } = require('redux-devtools'); - finalCreateStore = compose( - // applyMiddleware(middleware), - devTools(), - persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) - )(createStore); - } - else { - // finalCreateStore = applyMiddleware(middleware)(createStore); - finalCreateStore = creteStore + if (__DEVTOOLS__) { + const { devTools, persistState } = require('redux-devtools'); + finalCreateStore = compose( + applyMiddleware(middleware), + devTools(), + persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)) + )(createStore); + } else { + finalCreateStore = applyMiddleware(middleware)(createStore); } + const reducer = require('./modules/reducer'); const store = finalCreateStore(reducer, data); store.client = client; - - //TODO: set up hotmodule replacement if (__DEVELOPMENT__ && module.hot) { - module.hot.accept('./modules/reducer', () => { - store.replaceReducer(require('./modules/reducer')); - }); - } + module.hot.accept('./modules/reducer', () => { + store.replaceReducer(require('./modules/reducer')); + }); + } return store; } + diff --git a/app/route-handlers/Application.jsx b/app/route-handlers/Application.jsx index 367ae19..6220559 100644 --- a/app/route-handlers/Application.jsx +++ b/app/route-handlers/Application.jsx @@ -1,5 +1,8 @@ import Application from "containers/Application"; + +//TODO: revist the necessity of this route-handlers abstraction + //TODO: eventually we will want to connect to store using react-redux; // export default connect(mapStateToProps)(Application) diff --git a/app/routes.jsx b/app/routes.jsx index 9a5c62a..822bd2b 100644 --- a/app/routes.jsx +++ b/app/routes.jsx @@ -4,12 +4,11 @@ import { Route } from "react-router"; import Application from "route-handlers/Application"; import Login from "route-handlers/Login"; -//TODO: research necessity of Object.assign polyfill +export default function(store) { + return ( + + + + ) +} -//TODO: switch to es6 export - -module.exports = ( - - - -) diff --git a/config/mainRenderer.jsx b/config/mainRenderer.jsx deleted file mode 100644 index ac520b0..0000000 --- a/config/mainRenderer.jsx +++ /dev/null @@ -1,28 +0,0 @@ - -import React from "react"; -import { Router } from "react-router"; -import routes from "../app/routes"; - -import createStore from "../app/redux/store"; - - -const store = createStore(); - -export function renderDevTools() { - if (__DEVTOOLS__) { - const { DevTools, DebugPanel, LogMonitor } = require('redux-devtools/lib/react'); - return - - - - } -} -//TODO: will need to wrap in redux provider element - -React.render(( - - { routes } - -), document.getElementById("content")); - - diff --git a/make-webpack-config.js b/make-webpack-config.js index 7462268..ac739a1 100644 --- a/make-webpack-config.js +++ b/make-webpack-config.js @@ -8,7 +8,7 @@ var loadersByExtension = require("./config/loadersByExtension"); module.exports = function(options) { var entry = { - main: "./config/mainRenderer" + main: "./app/client" }; var loaders = { "jsx": options.hotComponents ? ["react-hot-loader", "babel-loader?stage=0"] : "babel-loader?stage=0", @@ -69,7 +69,7 @@ module.exports = function(options) { if (options.debug) { plugins.push(new webpack.DefinePlugin({ __DEVTOOLS__ : true, - __DEVELOPMENT__: true + __DEVELOPMENT__: true, })); } diff --git a/package.json b/package.json index ab08f4c..3e86c2a 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,13 @@ "author": "Matt Marcelo", "license": "ISC", "dependencies": { + "babel": "^5.8.23", "babel-core": "^5.8.25", "babel-loader": "^5.3.2", "css-loader": "^0.19.0", "extract-text-webpack-plugin": "^0.8.2", "file-loader": "^0.8.4", + "history": "^1.10.1", "html-loader": "^0.3.0", "json-loader": "^0.5.3", "json5": "^0.4.0", @@ -25,15 +27,17 @@ "less-loader": "^2.2.1", "markdown-loader": "^0.1.7", "node-sass": "^3.3.3", + "query-string": "^2.4.1", "raw-loader": "^0.5.1", "react": "^0.13.3", "react-hot-loader": "^1.3.0", - "react-router": "^1.0.0-rc1", + "react-redux": "^3.0.0", "redux": "^3.0.0", "sass-loader": "^2.0.1", "stats-webpack-plugin": "^0.2.1", "style-loader": "^0.12.4", "stylus-loader": "^1.3.0", + "superagent": "^1.4.0", "url-loader": "^0.5.6", "webpack": "^1.12.2" },