├── .jshintrc
├── .gitignore
├── src
├── common
│ ├── reducers
│ │ ├── version.js
│ │ ├── layout.js
│ │ ├── counter.js
│ │ ├── index.js
│ │ ├── user.js
│ │ ├── todos.js
│ │ └── reddit.js
│ ├── components
│ │ ├── Home.js
│ │ ├── reddit
│ │ │ ├── Posts.js
│ │ │ └── Picker.js
│ │ ├── Todo.js
│ │ ├── 404.js
│ │ ├── todo
│ │ │ ├── Header.js
│ │ │ ├── TextInput.js
│ │ │ ├── Todo.js
│ │ │ ├── Item.js
│ │ │ ├── Footer.js
│ │ │ └── Section.js
│ │ ├── About.js
│ │ ├── Counter.js
│ │ ├── personal
│ │ │ └── Dashboard.js
│ │ ├── assets
│ │ │ └── TextInput.js
│ │ ├── layout
│ │ │ ├── Sidebar.js
│ │ │ └── Header.js
│ │ ├── Login.js
│ │ └── Reddit.js
│ ├── containers
│ │ ├── CounterPage.js
│ │ ├── TodoPage.js
│ │ ├── LoginPage.js
│ │ ├── DevTools.js
│ │ ├── RedditPage.js
│ │ └── App.js
│ ├── actions
│ │ ├── layout.js
│ │ ├── counter.js
│ │ ├── todos.js
│ │ ├── reddit.js
│ │ └── user.js
│ ├── api
│ │ ├── fetchComponentDataBeforeRender.js
│ │ ├── promiseMiddleware.js
│ │ └── user.js
│ ├── themes
│ │ └── personal.js
│ ├── routes.js
│ └── store
│ │ └── configureStore.js
├── server
│ ├── index.js
│ ├── webpack.js
│ ├── devtools.js
│ └── server.js
└── client
│ └── index.js
├── .travis.yml
├── test
├── setup.js
├── state
│ └── layout.spec.js
├── render
│ └── Sidebar.spec.js
└── behaviour
│ └── Sidebar.spec.js
├── styles
└── index.css
├── README.md
├── package.json
└── webpack.config.js
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | lib
4 | .DS_Store
5 | dist
--------------------------------------------------------------------------------
/src/common/reducers/version.js:
--------------------------------------------------------------------------------
1 | export default function version(state = 0, action) {
2 | return state;
3 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4.0"
4 | - "0.12"
5 | - "0.11"
6 | - "0.10"
7 | - "iojs"
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register')({
2 | presets: ['es2015', 'stage-2', 'react']
3 | });
4 | require('./server');
5 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | var jsdom = require('jsdom-no-contextify');
2 |
3 | global.document = jsdom.jsdom('
');
4 | global.window = document.parentWindow;
--------------------------------------------------------------------------------
/styles/index.css:
--------------------------------------------------------------------------------
1 | html{
2 | font-family: 'Roboto', sans-serif;
3 | -webkit-font-smoothing: antialiased;
4 | }
5 | body{
6 | margin:0px;
7 | }
8 |
9 | a{
10 | text-decoration: none;
11 | }
--------------------------------------------------------------------------------
/src/common/reducers/layout.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_SIDEBAR } from '../actions/layout';
2 |
3 | export default function layout(state = {sidebarOpen: false}, action) {
4 | switch (action.type) {
5 | case TOGGLE_SIDEBAR:
6 | return {
7 | sidebarOpen : action.value
8 | };
9 | default:
10 | return state;
11 | }
12 | }
--------------------------------------------------------------------------------
/src/common/components/Home.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Helmet from 'react-helmet';
3 |
4 | class Home extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
10 | Its a home
11 |
12 | );
13 | }
14 | }
15 |
16 | export default Home;
--------------------------------------------------------------------------------
/src/common/reducers/counter.js:
--------------------------------------------------------------------------------
1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter';
2 |
3 | export default function counter(state = 0, action) {
4 | switch (action.type) {
5 | case SET_COUNTER:
6 | return action.payload,10;
7 | case INCREMENT_COUNTER:
8 | return state + 1;
9 | case DECREMENT_COUNTER:
10 | return state - 1;
11 | default:
12 | return state;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/common/components/reddit/Posts.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 |
3 | export default class Posts extends Component {
4 | render () {
5 | return (
6 |
7 | {this.props.posts.map((post, i) =>
8 |
{post.title} Read
9 | )}
10 |
11 | );
12 | }
13 | }
14 |
15 | Posts.propTypes = {
16 | posts: PropTypes.array.isRequired
17 | };
--------------------------------------------------------------------------------
/src/server/webpack.js:
--------------------------------------------------------------------------------
1 | // Webpack dev server
2 | // Ran in parallel with the Express server
3 |
4 | import WebpackDevServer from "webpack-dev-server";
5 | import webpack from "webpack";
6 | import config from "../../webpack.config.dev";
7 |
8 | var server = new WebpackDevServer(webpack(config), {
9 | // webpack-dev-server options
10 | publicPath: config.output.publicPath,
11 | hot: true,
12 | stats: { colors: true },
13 | });
14 | server.listen(8080, "localhost", function() {});
15 |
--------------------------------------------------------------------------------
/src/common/containers/CounterPage.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 | import Counter from '../components/Counter';
4 | import * as CounterActions from '../actions/counter';
5 |
6 | function mapStateToProps(state) {
7 | return {
8 | counter: state.counter.present
9 | };
10 | }
11 |
12 | function mapDispatchToProps(dispatch) {
13 | return bindActionCreators(CounterActions, dispatch);
14 | }
15 |
16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter);
--------------------------------------------------------------------------------
/src/common/actions/layout.js:
--------------------------------------------------------------------------------
1 |
2 | import { ActionCreators } from 'redux-undo';
3 |
4 |
5 |
6 | export function undo() {
7 | return (dispatch, getState) => {
8 | dispatch(ActionCreators.undo());
9 | };
10 | }
11 |
12 | export function redo() {
13 | return (dispatch, getState) => {
14 | dispatch(ActionCreators.redo());
15 | };
16 | }
17 |
18 | /**
19 | * Bundle User into layout
20 | */
21 |
22 | import { GET_USER, getUser} from './user';
23 | export { getUser as getUser };
24 | export { GET_USER as GET_USER };
25 |
26 |
--------------------------------------------------------------------------------
/src/common/containers/TodoPage.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 | import Todo from '../components/Todo';
4 | import * as TodoActions from '../actions/todos';
5 |
6 | function mapStateToProps(state) {
7 | return {
8 | todos: state.todos.present
9 | };
10 | }
11 |
12 | function mapDispatchToProps(dispatch) {
13 | return {
14 | actions: bindActionCreators(TodoActions, dispatch)
15 | };
16 | }
17 |
18 |
19 | export default connect(mapStateToProps, mapDispatchToProps)(Todo);
--------------------------------------------------------------------------------
/src/common/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Header from './todo/Header';
3 | import Section from './todo/Section';
4 |
5 | class Todo extends Component {
6 | render() {
7 | const { todos, actions } = this.props;
8 | return (
9 |
10 |
11 |
12 |
13 | );
14 | }
15 | }
16 |
17 | Todo.propTypes = {
18 | todos: PropTypes.array.isRequired
19 | };
20 |
21 | export default Todo;
--------------------------------------------------------------------------------
/src/common/components/404.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 |
4 | class Error404 extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
404: Page not found
10 |
Sorry, we've misplaced that URL or it's pointing to something that does not exist.
11 |
> Head back home
12 |
13 | );
14 | }
15 | }
16 |
17 | export default Error404;
18 |
19 |
--------------------------------------------------------------------------------
/src/server/devtools.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import DevTools from '../common/containers/DevTools';
4 |
5 | export default function createDevToolsWindow(store) {
6 | const popup = window.open(null, 'Redux DevTools', 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no');
7 | // Reload in case it already exists
8 | popup.location.reload();
9 |
10 | setTimeout(() => {
11 | popup.document.write('
');
12 | render(
13 | ,
14 | popup.document.getElementById('react-devtools-root')
15 | );
16 | }, 10);
17 | }
--------------------------------------------------------------------------------
/src/common/containers/LoginPage.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import { connect } from 'react-redux';
3 | import Login from '../components/Login';
4 | import * as LoginActions from '../actions/user';
5 |
6 | function mapStateToProps(state) {
7 | return {
8 | username: state.user.username,
9 | userId: state.user.userId,
10 | error: state.user.error,
11 | token: state.user.token,
12 | logged: state.user.logged
13 | };
14 | }
15 |
16 | function mapDispatchToProps(dispatch) {
17 | return bindActionCreators(LoginActions, dispatch);
18 | }
19 |
20 | export default connect(mapStateToProps, mapDispatchToProps)(Login);
21 |
--------------------------------------------------------------------------------
/src/common/api/fetchComponentDataBeforeRender.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This looks at static needs parameter in components and waits for the promise to be fullfilled
3 | * It is used to make sure server side rendered pages wait for APIs to resolve before returning res.end()
4 | */
5 |
6 | export function fetchComponentDataBeforeRender(dispatch, components, params) {
7 | const needs = components.reduce( (prev, current) => {
8 | return (current.need || [])
9 | .concat((current.WrappedComponent ? current.WrappedComponent.need : []) || [])
10 | .concat(prev);
11 | }, []);
12 | const promises = needs.map(need => dispatch(need()));
13 | return Promise.all(promises);
14 | }
--------------------------------------------------------------------------------
/src/common/components/todo/Header.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import TodoTextInput from './TextInput';
3 |
4 | class Header extends Component {
5 | handleSave(text) {
6 | if (text.length !== 0) {
7 | this.props.addTodo(text);
8 | }
9 | }
10 |
11 | render() {
12 | return (
13 |
18 | );
19 | }
20 | }
21 |
22 | Header.propTypes = {
23 | addTodo: PropTypes.func.isRequired
24 | };
25 |
26 | export default Header;
--------------------------------------------------------------------------------
/src/common/api/promiseMiddleware.js:
--------------------------------------------------------------------------------
1 | export default function promiseMiddleware() {
2 | return next => action => {
3 | const { promise, type, ...rest } = action;
4 |
5 | if (!promise) return next(action);
6 |
7 | const SUCCESS = type + '_SUCCESS';
8 | const REQUEST = type + '_REQUEST';
9 | const FAILURE = type + '_FAILURE';
10 | next({ ...rest, type: REQUEST });
11 | return promise
12 | .then(req => {
13 | next({ ...rest, req, type: SUCCESS });
14 | return true;
15 | })
16 | .catch(error => {
17 | next({ ...rest, error, type: FAILURE });
18 | console.log(error);
19 | return false;
20 | });
21 | };
22 | }
--------------------------------------------------------------------------------
/src/common/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { routerStateReducer } from 'redux-router';
3 | import undoable from 'redux-undo';
4 |
5 | import user from './user';
6 | import counter from './counter';
7 | import layout from './layout';
8 | import todos from './todos';
9 | import version from './version';
10 | import { selectedReddit, postsByReddit } from './reddit';
11 |
12 | const rootReducer = combineReducers({
13 | user : user,
14 | version : version,
15 | counter : undoable(counter),
16 | layout : undoable(layout),
17 | todos : undoable(todos),
18 | selectedReddit : undoable(selectedReddit),
19 | postsByReddit : undoable(postsByReddit),
20 | router : routerStateReducer
21 | });
22 |
23 | export default rootReducer;
--------------------------------------------------------------------------------
/src/common/components/reddit/Picker.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | export default class Picker extends Component {
4 | render () {
5 | const { value, onChange, options } = this.props;
6 |
7 | return (
8 |
9 |
Reddit API
10 |
Posts for onChange(e.target.value)}
11 | value={value}>
12 | {options.map(option =>
13 |
14 | {option}
15 | )
16 | }
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | Picker.propTypes = {
24 | options: PropTypes.arrayOf(
25 | PropTypes.string.isRequired
26 | ).isRequired,
27 | value: PropTypes.string.isRequired,
28 | onChange: PropTypes.func.isRequired
29 | };
--------------------------------------------------------------------------------
/src/common/containers/DevTools.js:
--------------------------------------------------------------------------------
1 | import React, {PropTypes} from 'react';
2 |
3 | // Exported from redux-devtools
4 | import { createDevTools } from 'redux-devtools';
5 |
6 | // Monitors are separate packages, and you can make a custom one
7 | import LogMonitor from 'redux-devtools-log-monitor';
8 | import DockMonitor from 'redux-devtools-dock-monitor';
9 |
10 | // createDevTools takes a monitor and produces a DevTools component
11 | const DevTools = createDevTools(
12 | // Monitors are individually adjustable with props.
13 | // Consult their repositories to learn about those props.
14 | // Here, we put LogMonitor inside a DockMonitor.
15 |
19 |
20 |
21 | );
22 |
23 |
24 | export default DevTools;
25 |
--------------------------------------------------------------------------------
/src/common/components/About.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Helmet from 'react-helmet';
3 |
4 |
5 | class About extends Component {
6 |
7 | render() {
8 | return (
9 |
10 |
11 |
Why I coded this
12 |
Redux and React are rapidly developing code bases. I was having difficultly finding a simple boiler plate example to base my Redux app on. I decided to create my own stripped down version.
13 |
14 |
20 |
21 |
22 | );
23 | }
24 | }
25 |
26 | export default About;
--------------------------------------------------------------------------------
/src/common/actions/counter.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER';
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
4 |
5 | export function setCounter(value) {
6 | return {
7 | type: SET_COUNTER,
8 | payload: value
9 | };
10 | }
11 |
12 | export function incrementCounter() {
13 | return {
14 | type: INCREMENT_COUNTER
15 | };
16 | }
17 |
18 | export function decrementCounter() {
19 | return {
20 | type: DECREMENT_COUNTER
21 | };
22 | }
23 |
24 | export function incrementIfOdd() {
25 | return (dispatch, getState) => {
26 | const { counter } = getState();
27 | if (counter % 2 === 0) {
28 | return;
29 | }
30 | dispatch(increment());
31 | };
32 | }
33 |
34 | export function incrementAsync(delay = 1000) {
35 | return dispatch => {
36 | setTimeout(() => {
37 | dispatch(increment());
38 | }, delay);
39 | };
40 | }
41 |
--------------------------------------------------------------------------------
/src/common/actions/todos.js:
--------------------------------------------------------------------------------
1 | export const ADD_TODO = 'ADD_TODO';
2 | export const DELETE_TODO = 'DELETE_TODO';
3 | export const EDIT_TODO = 'EDIT_TODO';
4 | export const COMPLETE_TODO = 'COMPLETE_TODO';
5 | export const COMPLETE_ALL = 'COMPLETE_ALL';
6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED';
7 |
8 | export const SHOW_ALL = 'SHOW_ALL';
9 | export const SHOW_COMPLETED = 'SHOW_COMPLETED';
10 | export const SHOW_ACTIVE = 'SHOW_ACTIVE';
11 |
12 | export function addTodo(text) {
13 | return { type: ADD_TODO, text };
14 | }
15 |
16 | export function deleteTodo(id) {
17 | return { type: DELETE_TODO, id };
18 | }
19 |
20 | export function editTodo(id, text) {
21 | return { type: EDIT_TODO, id, text };
22 | }
23 |
24 | export function completeTodo(id) {
25 | return { type: COMPLETE_TODO, id };
26 | }
27 |
28 | export function completeAll() {
29 | return { type: COMPLETE_ALL };
30 | }
31 |
32 | export function clearCompleted() {
33 | return { type: CLEAR_COMPLETED };
34 | }
--------------------------------------------------------------------------------
/src/common/themes/personal.js:
--------------------------------------------------------------------------------
1 | import Colors from 'material-ui/lib/styles/colors';
2 | import ColorManipulator from 'material-ui/lib/utils/color-manipulator';
3 | import Spacing from 'material-ui/lib/styles/spacing';
4 | import zIndex from 'material-ui/lib/styles/zIndex';
5 |
6 | zIndex.appBar = 2000;
7 |
8 | export default {
9 | spacing: Spacing,
10 | zIndex: zIndex,
11 | fontFamily: 'Roboto, sans-serif',
12 | palette: {
13 | primary1Color: Colors.cyan500,
14 | primary2Color: Colors.cyan700,
15 | primary3Color: Colors.lightBlack,
16 | accent1Color: Colors.pinkA200,
17 | accent2Color: Colors.grey100,
18 | accent3Color: Colors.grey500,
19 | textColor: Colors.darkBlack,
20 | alternateTextColor: Colors.white,
21 | canvasColor: Colors.white,
22 | borderColor: Colors.grey300,
23 | disabledColor: ColorManipulator.fade(Colors.darkBlack, 0.3),
24 | pickerHeaderColor: Colors.cyan500,
25 | }
26 | };
--------------------------------------------------------------------------------
/src/common/components/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | class Counter extends Component {
4 |
5 | constructor(props) {
6 | super(props);
7 | }
8 |
9 | render() {
10 | const { incrementCounter, decrementCounter, counter } = this.props;
11 | return (
12 |
13 |
Counter
14 |
This counter is here to show the state is conserved as you navigate through the app.
15 |
16 | Counter: {counter} times
17 | {' '}
18 | +
19 | {' '}
20 | -
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | Counter.propTypes = {
29 | incrementCounter: PropTypes.func.isRequired,
30 | incrementIfOdd: PropTypes.func.isRequired,
31 | incrementAsync: PropTypes.func.isRequired,
32 | decrementCounter: PropTypes.func.isRequired,
33 | counter: PropTypes.number.isRequired
34 | };
35 |
36 | export default Counter;
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-core/register';
2 | import ReactDOM from 'react-dom';
3 | import React from 'react';
4 | import { Router } from 'react-router';
5 | import { Provider } from 'react-redux';
6 | import { ReduxRouter } from 'redux-router';
7 | import createBrowserHistory from 'history/lib/createBrowserHistory'
8 | import configureStore from '../common/store/configureStore';
9 | import routes from '../common/routes';
10 | import "../../styles/index.css";
11 | const history = createBrowserHistory();
12 | const initialState = window.__INITIAL_STATE__;
13 | const store = configureStore(initialState);
14 | const rootElement = document.getElementById('root');
15 | console.log(`suck it:`);
16 | console.log(routes);
17 | ReactDOM.render(
18 |
19 |
20 |
21 |
22 | ,
23 | document.getElementById('root')
24 | );
25 | if (process.env.NODE_ENV !== 'production') {
26 | var devtools = require('../server/devtools');
27 | devtools.default(store);
28 | }
--------------------------------------------------------------------------------
/src/common/components/personal/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import TextField from 'material-ui/lib/text-field';
3 | import RaisedButton from 'material-ui/lib/raised-button';
4 | import { bindActionCreators } from 'redux';
5 | import {connect} from 'react-redux';
6 |
7 | import {reduxForm} from 'redux-form';
8 | import Helmet from 'react-helmet'
9 | import * as UserActions from '../../actions/user';
10 |
11 |
12 | class Dashboard extends Component {
13 |
14 | constructor(props) {
15 | super(props);
16 | }
17 |
18 | render() {
19 | console.log(this.props.user);
20 | return (
21 |
22 |
23 | TVOI DASHBOARD SHATAL
24 |
25 | );
26 | }
27 | }
28 |
29 | Dashboard.propTypes = {
30 |
31 | };
32 |
33 | function mapStateToProps(state) {
34 | return {
35 | user : state.user
36 | };
37 | }
38 |
39 | function mapDispatchToProps(dispatch) {
40 | return bindActionCreators(UserActions,dispatch);
41 | }
42 |
43 |
44 | export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
45 |
--------------------------------------------------------------------------------
/src/common/containers/RedditPage.js:
--------------------------------------------------------------------------------
1 | import { bindActionCreators } from 'redux';
2 | import React, { Component} from 'react';
3 | import { connect } from 'react-redux';
4 | import Reddit from '../components/Reddit';
5 | import * as RedditActions from '../actions/reddit';
6 |
7 | //Data that needs to be called before rendering the component
8 | //This is used for server side rending via the fetchComponentDataBeforeRending() method
9 | Reddit.need = [
10 | RedditActions.fetchPosts
11 | ]
12 |
13 | function mapStateToProps(state) {
14 | let { selectedReddit, postsByReddit } = state;
15 | selectedReddit = selectedReddit.present;
16 | postsByReddit = postsByReddit.present;
17 | const {
18 | isFetching,
19 | lastUpdated,
20 | error,
21 | items: posts
22 | } = postsByReddit[selectedReddit] || {
23 | isFetching: true,
24 | error:{},
25 | items: []
26 | };
27 |
28 | return {
29 | selectedReddit,
30 | posts,
31 | isFetching,
32 | lastUpdated,
33 | error
34 | };
35 | }
36 |
37 | function mapDispatchToProps(dispatch) {
38 | return bindActionCreators(RedditActions, dispatch);
39 | }
40 |
41 | export default connect(mapStateToProps,mapDispatchToProps)(Reddit);
--------------------------------------------------------------------------------
/src/common/actions/reddit.js:
--------------------------------------------------------------------------------
1 | import request from 'axios';
2 |
3 | export const SELECT_REDDIT = 'SELECT_REDDIT';
4 | export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT';
5 |
6 | export const POSTS_GET = 'POSTS_GET';
7 | export const POSTS_GET_REQUEST = 'POSTS_GET_REQUEST';
8 | export const POSTS_GET_SUCCESS = 'POSTS_GET_SUCCESS';
9 | export const POSTS_GET_FAILURE = 'POSTS_GET_FAILURE';
10 |
11 | export function selectReddit(reddit) {
12 | return {
13 | type: SELECT_REDDIT,
14 | reddit
15 | };
16 | }
17 |
18 | export function invalidateReddit(reddit) {
19 | return {
20 | type: INVALIDATE_REDDIT,
21 | reddit
22 | };
23 | }
24 |
25 | export function fetchPosts(reddit = 'reactjs') {
26 | return {
27 | type: POSTS_GET,
28 | reddit,
29 | promise: request.get(`http://www.reddit.com/r/${reddit}.json`)
30 | };
31 | }
32 |
33 | function shouldFetchPosts(state, reddit) {
34 | const posts = state.postsByReddit[reddit];
35 | if (!posts) {
36 | return true;
37 | } else if (posts.isFetching) {
38 | return false;
39 | } else {
40 | return posts.didInvalidate;
41 | }
42 | }
43 |
44 | export function fetchPostsIfNeeded(reddit) {
45 | return (dispatch, getState) => {
46 | if (shouldFetchPosts(getState(), reddit)) {
47 | return dispatch(fetchPosts(reddit));
48 | }
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/common/reducers/user.js:
--------------------------------------------------------------------------------
1 | import {GET_USER, LOGIN_SUCCESS, LOGIN_FAILURE, GET_USER_INFO_FAILURE, GET_USER_INFO_SUCCESS, LOGOUT_SUCCESS, CLEAR_COOKIE } from '../actions/user';
2 |
3 | export default function user(state = {}, action) {
4 | switch (action.type) {
5 | case GET_USER:
6 | return state;
7 | case LOGIN_SUCCESS:
8 | return Object.assign({}, state, {
9 | error: null,
10 | token: action.req.data.id,
11 | userId: action.req.data.userId,
12 | updateCookie: true
13 | });
14 | case LOGIN_FAILURE:
15 | return Object.assign({}, state, {
16 | error: action.error.data.error.message
17 | });
18 | case GET_USER_INFO_SUCCESS:
19 | return Object.assign({}, state, {
20 | info: action.req.data
21 | });
22 | case LOGOUT_SUCCESS:
23 | return Object.assign({}, state, {
24 | info: null,
25 | token: null,
26 | userId: null,
27 | clearCookie: true
28 | });
29 | case CLEAR_COOKIE:
30 | return Object.assign({}, state, {
31 | clearCookie: false
32 | });
33 | default:
34 | return state;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/common/reducers/todos.js:
--------------------------------------------------------------------------------
1 | import { ADD_TODO, DELETE_TODO, EDIT_TODO, COMPLETE_TODO, COMPLETE_ALL, CLEAR_COMPLETED } from '../actions/todos';
2 |
3 | const initialState = [{
4 | text: 'Use Redux',
5 | completed: false,
6 | id: 0
7 | }];
8 |
9 | export default function todos(state = initialState, action) {
10 | switch (action.type) {
11 | case ADD_TODO:
12 | return [{
13 | id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
14 | completed: false,
15 | text: action.text
16 | }, ...state];
17 |
18 | case DELETE_TODO:
19 | return state.filter(todo =>
20 | todo.id !== action.id
21 | );
22 |
23 | case EDIT_TODO:
24 | return state.map(todo =>
25 | todo.id === action.id ?
26 | Object.assign({}, todo, { text: action.text }) :
27 | todo
28 | );
29 |
30 | case COMPLETE_TODO:
31 | return state.map(todo =>
32 | todo.id === action.id ?
33 | Object.assign({}, todo, { completed: !todo.completed }) :
34 | todo
35 | );
36 |
37 | case COMPLETE_ALL:
38 | const areAllMarked = state.every(todo => todo.completed);
39 | return state.map(todo => Object.assign({}, todo, {
40 | completed: !areAllMarked
41 | }));
42 |
43 | case CLEAR_COMPLETED:
44 | return state.filter(todo => todo.completed === false);
45 |
46 | default:
47 | return state;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/common/components/assets/TextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | class TextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context);
7 | this.state = {
8 | text: this.props.text || ''
9 | };
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim();
14 | if (e.which === 13) {
15 | this.props.onSave(text);
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' });
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value });
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value);
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | );
47 | }
48 | }
49 |
50 | TextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | };
57 |
58 | export default TextInput;
--------------------------------------------------------------------------------
/src/common/components/todo/TextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classnames from 'classnames';
3 |
4 | class TextInput extends Component {
5 | constructor(props, context) {
6 | super(props, context);
7 | this.state = {
8 | text: this.props.text || ''
9 | };
10 | }
11 |
12 | handleSubmit(e) {
13 | const text = e.target.value.trim();
14 | if (e.which === 13) {
15 | this.props.onSave(text);
16 | if (this.props.newTodo) {
17 | this.setState({ text: '' });
18 | }
19 | }
20 | }
21 |
22 | handleChange(e) {
23 | this.setState({ text: e.target.value });
24 | }
25 |
26 | handleBlur(e) {
27 | if (!this.props.newTodo) {
28 | this.props.onSave(e.target.value);
29 | }
30 | }
31 |
32 | render() {
33 | return (
34 |
46 | );
47 | }
48 | }
49 |
50 | TextInput.propTypes = {
51 | onSave: PropTypes.func.isRequired,
52 | text: PropTypes.string,
53 | placeholder: PropTypes.string,
54 | editing: PropTypes.bool,
55 | newTodo: PropTypes.bool
56 | };
57 |
58 | export default TextInput;
--------------------------------------------------------------------------------
/src/common/routes.js:
--------------------------------------------------------------------------------
1 | import { Route } from "react-router";
2 | import React from "react";
3 | import App from "./containers/App";
4 | //Redux Smart
5 | import CounterPage from "./containers/CounterPage";
6 | import RedditPage from "./containers/RedditPage";
7 | import TodoPage from "./containers/TodoPage";
8 | import LoginPage from "./containers/LoginPage";
9 |
10 | import Dashboard from './components/personal/Dashboard';
11 | //Redux Dumb
12 | import HomePage from "./components/Home";
13 | import AboutPage from "./components/About";
14 | import error404 from "./components/404";
15 | import cookie from 'react-cookie';
16 |
17 | function requireAuth() {
18 | if(cookie.load('token')){
19 | return true;
20 | }else{
21 | window.location = '/login'
22 | }
23 | }
24 |
25 | function unRequireAuth() {
26 | if(!cookie.load('token')){
27 | return true;
28 | }else{
29 | window.location = '/home'
30 | }
31 | }
32 |
33 |
34 | export default (
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 |
--------------------------------------------------------------------------------
/test/state/layout.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import addons from 'react/addons';
3 | import expect from 'expect';
4 |
5 | import * as LayoutActions from '../../src/common/actions/layout';
6 | import configureStore from '../../src/common/store/configureStore';
7 |
8 | describe('Layout State', function(){
9 |
10 | before('render and locate element', function() {
11 | const store = configureStore({});
12 | this.store = store;
13 | });
14 |
15 | it('layout state should exist', function() {
16 | expect(this.store.getState().layout).toExist();
17 | });
18 |
19 | it('layout state to instansiate with...', function() {
20 | expect(this.store.getState().layout.present).toEqual({
21 | sidebarOpen : false
22 | });
23 | });
24 |
25 | it('dispatch TOGGLE_SIDEBAR, state should change layout state', function() {
26 | expect(this.store.getState().layout.present).toEqual({
27 | sidebarOpen : false
28 | });
29 | this.store.dispatch(LayoutActions.toggleSidebar(true));
30 | expect(this.store.getState().layout.present).toEqual({
31 | sidebarOpen : true
32 | });
33 | });
34 |
35 | it('dispatch UNDO, state should change layout state', function() {
36 | this.store.dispatch(LayoutActions.undo());
37 | expect(this.store.getState().layout.present).toEqual({
38 | sidebarOpen : false
39 | });
40 | });
41 |
42 | it('dispatch REDO, state should change layout state', function() {
43 | this.store.dispatch(LayoutActions.redo());
44 | expect(this.store.getState().layout.present).toEqual({
45 | sidebarOpen : true
46 | });
47 | });
48 |
49 | });
--------------------------------------------------------------------------------
/src/common/api/user.js:
--------------------------------------------------------------------------------
1 | import request from 'axios';
2 | import config from '../../../package.json';
3 |
4 |
5 | export function getUser(token, callback) {
6 | if (!token) {
7 | return callback(false);
8 | }
9 | request
10 | .get(`http://${config.apiHost}:${config.apiPort}/api/users/check?access_token=${token}`)
11 | .then(function (response) {
12 | if (response.status === 200) {
13 | request
14 | .get(`http://${config.apiHost}:${config.apiPort}/api/users/${response.data.valid.userId}?access_token=${token}`)
15 | .then(function (response) {
16 | // console.log(response.data);
17 | callback(response.data);
18 | })
19 | .catch(function (err) {
20 | // console.log(err);
21 | callback(false);
22 | })
23 | } else {
24 | callback(false);
25 | }
26 | })
27 | .catch(function (err) {
28 | // console.log(err);
29 | callback(false);
30 | });
31 | }
32 | // return callback(false);
33 | //request()
34 | // Rather than immediately returning, we delay our code with a timeout to simulate asynchronous behavior
35 | // setTimeout(() => {
36 | // callback({
37 | // name : 'John Smith',
38 | // dept : 'Web Team',
39 | // lastLogin : new Date(),
40 | // email : 'john@smith.com',
41 | // id : 'abcde1234'
42 | // });
43 | // }, 500);
44 |
45 | // In the case of a real world API call, you'll normally run into a Promise like this:
46 | // API.getUser().then(user => callback(user));
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Isomorphic Redux App
2 |
3 | [](https://travis-ci.org/caljrimmer/isomorphic-redux-app)
4 |
5 | This project serves as a **simple** boilerplate to start building an isomorphic rendering application in React and Redux.
6 |
7 |
8 | # Features
9 |
10 | - Async server-side rendering
11 | - Hot reloading middleware
12 | - Redux DevTools and Logging
13 | - Redux Routing
14 | - Reddit API example
15 | - Counter example
16 | - Todo example
17 | - Static content example
18 |
19 | ## Stack
20 |
21 | - React.js
22 | - React-router
23 | - Webpack
24 | - Express
25 | - Redux
26 | - Redux-DevTools
27 | - Babel
28 | - Material UI
29 |
30 | ## Development Installation
31 |
32 | In the project's directory, run the following commands:
33 |
34 | ```
35 | $ npm install
36 | $ npm start
37 | ```
38 |
39 | Then Visit
40 |
41 | ```
42 | http://localhost:3002
43 | ```
44 |
45 | ## Releasing to Production
46 |
47 | Production has Devtools, logging and hot reloading middleware removed and the scripts/css compressed.
48 |
49 | In the project's directory, run the following commands:
50 |
51 | ```
52 | $ npm run build
53 | $ npm run start-prod
54 | ```
55 |
56 | Then Visit
57 |
58 | ```
59 | http://localhost:3002
60 | ```
61 |
62 | ## Run Test
63 | ```
64 | npm test
65 | ```
66 |
67 | ## Auth
68 | Look at /src/common/api/user.js
69 |
70 | You need to put token into cookie with name 'token' and check it into file via axios request.
71 |
72 | ## Credit
73 |
74 | Based on https://github.com/caljrimmer/isomorphic-redux-app
75 |
76 | Theme by Material UI Lib https://github.com/callemall/material-ui
77 |
--------------------------------------------------------------------------------
/src/common/actions/user.js:
--------------------------------------------------------------------------------
1 | import request from 'axios';
2 | import config from '../../../package.json';
3 | export const GET_USER = 'GET_USER';
4 | export const LOGIN = 'LOGIN';
5 | export const LOGIN_REQUEST = 'LOGIN_REQUEST';
6 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
7 | export const LOGIN_FAILURE = 'LOGIN_FAILURE';
8 |
9 | export const GET_USER_INFO = 'GET_USER_INFO';
10 | export const GET_USER_INFO_REQUEST = 'GET_USER_INFO_REQUEST';
11 | export const GET_USER_INFO_SUCCESS = 'GET_USER_INFO_SUCCESS';
12 | export const GET_USER_INFO_FAILURE = 'GET_USER_INFO_FAILURE';
13 |
14 | export const LOGOUT = 'LOGOUT';
15 | export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
16 | export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
17 | export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
18 |
19 | export const CLEAR_COOKIE = 'CLEAR_COOKIE';
20 |
21 | export function getUser(value) {
22 | return {
23 | type: GET_USER,
24 | payload: value
25 | };
26 | }
27 |
28 | export function getUserInfo(user) {
29 | console.log('From function getUserInfo');
30 | return {
31 | type: GET_USER_INFO,
32 | promise: request.get(`http://${config.apiHost}:${config.apiPort}/api/users/${user.userId}?access_token=${user.token}`)
33 | };
34 | }
35 |
36 |
37 | export function auth(username, password) {
38 | console.log('From function auth');
39 | return {
40 | type: LOGIN,
41 | promise: request.post(`http://${config.apiHost}:${config.apiPort}/api/users/login`, {username:username,password:password})
42 | };
43 | }
44 |
45 | export function logout(user) {
46 | return {
47 | type: LOGOUT,
48 | promise: request.post(`http://${config.apiHost}:${config.apiPort}/api/users/logout?access_token=${user.token}`)
49 | }
50 | }
51 |
52 | export function toogleClearCookie() {
53 | return {
54 | type: CLEAR_COOKIE
55 | };
56 | }
--------------------------------------------------------------------------------
/test/render/Sidebar.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import addons from 'react/addons';
3 | import expect from 'expect';
4 |
5 | import Sidebar from '../../src/common/components/layout/Sidebar';
6 |
7 | const TestUtils = React.addons.TestUtils;
8 |
9 | describe('Sidebar Rending', function(){
10 |
11 | const user = {
12 | name : 'John Smith',
13 | dept : 'Web Team',
14 | lastLogin : new Date(),
15 | email : 'john@smith.com',
16 | id : 'abcde1234'
17 | };
18 | const version = '0.0.1';
19 |
20 |
21 | before('render and locate element', function() {
22 | const renderedComponent = TestUtils.renderIntoDocument(
23 |
24 | );
25 |
26 | const username = TestUtils.findRenderedDOMComponentWithClass(
27 | renderedComponent,
28 | 'user-name'
29 | );
30 |
31 | const versionNumber = TestUtils.findRenderedDOMComponentWithClass(
32 | renderedComponent,
33 | 'version'
34 | );
35 |
36 | this.linkArray = TestUtils.scryRenderedDOMComponentsWithClass(
37 | renderedComponent,
38 | 'sidebar-nav-item'
39 | );
40 |
41 | this.firstLink = this.linkArray[0].getDOMNode();
42 | this.username = username.getDOMNode();
43 | this.versionNumber = versionNumber.getDOMNode();
44 |
45 | });
46 |
47 | it('user name should be "' + user.name+ '"', function() {
48 | expect(this.username.textContent).toBe(user.name);
49 | });
50 |
51 | it('version should not be ' + version , function() {
52 | expect(this.versionNumber.textContent).toBe('Currently version ' + version);
53 | });
54 |
55 | it('There should be 5 Navigation Links', function() {
56 | expect(this.linkArray.length - 1).toBe(5);
57 | });
58 |
59 | it('First link should be "Home [static]"', function() {
60 | expect(this.firstLink.textContent).toBe('Home [static]');
61 | });
62 |
63 | });
--------------------------------------------------------------------------------
/test/behaviour/Sidebar.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import addons from 'react/addons';
3 | import expect from 'expect';
4 |
5 | import App from '../../src/common/containers/App';
6 | import { Provider } from 'react-redux';
7 | import configureStore from '../../src/common/store/configureStore';
8 |
9 | const TestUtils = React.addons.TestUtils;
10 |
11 | describe('Sidebar behaviour', function(){
12 |
13 | before('render and locate element', function() {
14 | const store = configureStore({});
15 |
16 | const renderedComponent = TestUtils.renderIntoDocument(
17 |
18 | {()=>
19 |
20 | }
21 |
22 | );
23 |
24 | const wrapper = TestUtils.findRenderedDOMComponentWithClass(
25 | renderedComponent,
26 | 'wrapper'
27 | );
28 |
29 | const sidebar = TestUtils.findRenderedDOMComponentWithClass(
30 | renderedComponent,
31 | 'sidebar'
32 | );
33 |
34 | const sidebarToggle = TestUtils.findRenderedDOMComponentWithClass(
35 | renderedComponent,
36 | 'sidebar-toggle'
37 | );
38 |
39 | this.wrapper = wrapper.getDOMNode();
40 | this.sidebar = sidebar.getDOMNode();
41 | this.sidebarToggle = sidebarToggle.getDOMNode();
42 |
43 | });
44 |
45 | it('sidebar should exist', function() {
46 | expect(this.sidebar).toExist();
47 | });
48 |
49 | it('sidebar should be closed', function() {
50 | expect(this.sidebar.getAttribute('class')).toBe('sidebar');
51 | });
52 |
53 | it('sidebar toggle should exist', function() {
54 | expect(this.sidebarToggle).toExist();
55 | });
56 |
57 | it('clicking sidebar toggle should open sidebar', function() {
58 | expect(this.wrapper.getAttribute('class')).toBe('wrapper');
59 | TestUtils.Simulate.click(this.sidebarToggle);
60 | expect(this.wrapper.getAttribute('class')).toBe('wrapper open');
61 | });
62 |
63 | });
--------------------------------------------------------------------------------
/src/common/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import DevTools from '../containers/DevTools';
3 | import { reduxReactRouter } from 'redux-router';
4 | import thunk from 'redux-thunk';
5 | import createHistory from 'history/lib/createBrowserHistory';
6 | import createLogger from 'redux-logger';
7 | import promiseMiddleware from '../api/promiseMiddleware';
8 | import rootReducer from '../reducers';
9 |
10 | const middlewareBuilder = () => {
11 |
12 | let middleware = {};
13 | let universalMiddleware = [thunk,promiseMiddleware];
14 | let allComposeElements = [];
15 |
16 | if(process.browser){
17 | if(process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test'){
18 | middleware = applyMiddleware(...universalMiddleware);
19 | allComposeElements = [
20 | middleware,
21 | reduxReactRouter({
22 | createHistory
23 | })
24 | ]
25 | }else{
26 | middleware = applyMiddleware(...universalMiddleware,createLogger());
27 | allComposeElements = [
28 | middleware,
29 | reduxReactRouter({
30 | createHistory
31 | }),
32 | DevTools.instrument()
33 | ]
34 | }
35 | }else{
36 | middleware = applyMiddleware(...universalMiddleware);
37 | allComposeElements = [
38 | middleware
39 | ]
40 | }
41 |
42 | return allComposeElements;
43 |
44 | }
45 |
46 | const finalCreateStore = compose(...middlewareBuilder())(createStore);
47 |
48 | export default function configureStore(initialState) {
49 | const store = finalCreateStore(rootReducer, initialState);
50 |
51 | if (module.hot) {
52 | // Enable Webpack hot module replacement for reducers
53 | module.hot.accept('../reducers', () => {
54 | const nextRootReducer = require('../reducers');
55 | store.replaceReducer(nextRootReducer);
56 | });
57 | }
58 |
59 | return store;
60 | }
--------------------------------------------------------------------------------
/src/common/components/layout/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 | import classNames from 'classnames';
4 |
5 | class Sidebar extends Component {
6 |
7 | constructor(props){
8 | super(props);
9 | }
10 |
11 | render() {
12 |
13 | const {version,user} = this.props;
14 |
15 | return (
16 |
17 |
18 |
19 |
20 |
This is an example of a isomorphic website built with Redux and React
21 |
Logged in as {user.name}
22 |
23 |
24 |
25 | Home [static]
26 | Reddit [api]
27 | Todo [stateful]
28 | Counter [stateful]
29 | About [static]
30 | Login [static]
31 | {`Currently version ${version}`}
32 |
33 |
34 |
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | export default Sidebar;
47 |
--------------------------------------------------------------------------------
/src/common/components/todo/Todo.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classnames from 'classnames';
3 | import TextInput from './TextInput';
4 |
5 | class Item extends Component {
6 | constructor(props, context) {
7 | super(props, context);
8 | this.state = {
9 | editing: false
10 | };
11 | }
12 |
13 | handleDoubleClick() {
14 | this.setState({ editing: true });
15 | }
16 |
17 | handleSave(id, text) {
18 | if (text.length === 0) {
19 | this.props.deleteTodo(id);
20 | } else {
21 | this.props.editTodo(id, text);
22 | }
23 | this.setState({ editing: false });
24 | }
25 |
26 | render() {
27 | const {todo, completeTodo, deleteTodo} = this.props;
28 |
29 | let element;
30 | if (this.state.editing) {
31 | element = (
32 | this.handleSave(todo.id, text)} />
35 | );
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo.id)} />
43 |
44 | {todo.text}
45 |
46 | deleteTodo(todo.id)} />
48 |
49 | );
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | );
60 | }
61 | }
62 |
63 | Item.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | };
69 |
70 | export default Item;
--------------------------------------------------------------------------------
/src/common/components/todo/Item.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classnames from 'classnames';
3 | import TextInput from './TextInput';
4 |
5 | class TodoItem extends Component {
6 | constructor(props, context) {
7 | super(props, context);
8 | this.state = {
9 | editing: false
10 | };
11 | }
12 |
13 | handleDoubleClick() {
14 | this.setState({ editing: true });
15 | }
16 |
17 | handleSave(id, text) {
18 | if (text.length === 0) {
19 | this.props.deleteTodo(id);
20 | } else {
21 | this.props.editTodo(id, text);
22 | }
23 | this.setState({ editing: false });
24 | }
25 |
26 | render() {
27 | const {todo, completeTodo, deleteTodo} = this.props;
28 |
29 | let element;
30 | if (this.state.editing) {
31 | element = (
32 | this.handleSave(todo.id, text)} />
35 | );
36 | } else {
37 | element = (
38 |
39 | completeTodo(todo.id)} />
43 |
44 | {todo.text}
45 |
46 | deleteTodo(todo.id)} />
48 |
49 | );
50 | }
51 |
52 | return (
53 |
57 | {element}
58 |
59 | );
60 | }
61 | }
62 |
63 | TodoItem.propTypes = {
64 | todo: PropTypes.object.isRequired,
65 | editTodo: PropTypes.func.isRequired,
66 | deleteTodo: PropTypes.func.isRequired,
67 | completeTodo: PropTypes.func.isRequired
68 | };
69 |
70 | export default TodoItem;
--------------------------------------------------------------------------------
/src/common/components/todo/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react';
2 | import classnames from 'classnames';
3 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../../actions/todos';
4 |
5 | const FILTER_TITLES = {
6 | [SHOW_ALL]: 'All',
7 | [SHOW_ACTIVE]: 'Active',
8 | [SHOW_COMPLETED]: 'Completed'
9 | };
10 |
11 | class Footer extends Component {
12 | renderTodoCount() {
13 | const { activeCount } = this.props;
14 | const itemWord = activeCount === 1 ? 'item' : 'items';
15 |
16 | return (
17 |
18 | {activeCount || 'No'} {itemWord} left
19 |
20 | );
21 | }
22 |
23 | renderFilterLink(filter) {
24 | const title = FILTER_TITLES[filter];
25 | const { filter: selectedFilter, onShow } = this.props;
26 |
27 | return (
28 | onShow(filter)}>
31 | {title}
32 |
33 | );
34 | }
35 |
36 | renderClearButton() {
37 | const { completedCount, onClearCompleted } = this.props;
38 | if (completedCount > 0) {
39 | return (
40 |
42 | Clear completed
43 |
44 | );
45 | }
46 | }
47 |
48 | render() {
49 | return (
50 |
51 | {this.renderTodoCount()}
52 |
53 | {[SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED].map(filter =>
54 |
55 | {this.renderFilterLink(filter)}
56 |
57 | )}
58 |
59 | {this.renderClearButton()}
60 |
61 | );
62 | }
63 | }
64 |
65 | Footer.propTypes = {
66 | completedCount: PropTypes.number.isRequired,
67 | activeCount: PropTypes.number.isRequired,
68 | filter: PropTypes.string.isRequired,
69 | onClearCompleted: PropTypes.func.isRequired,
70 | onShow: PropTypes.func.isRequired
71 | };
72 |
73 | export default Footer;
--------------------------------------------------------------------------------
/src/common/reducers/reddit.js:
--------------------------------------------------------------------------------
1 | import {
2 | SELECT_REDDIT,
3 | INVALIDATE_REDDIT,
4 | POSTS_GET, POSTS_GET_REQUEST, POSTS_GET_SUCCESS, POSTS_GET_FAILURE
5 | } from '../actions/reddit';
6 |
7 | function posts(state = {
8 | error: {},
9 | isFetching: false,
10 | didInvalidate: false,
11 | items: []
12 | }, action) {
13 | switch (action.type) {
14 | case INVALIDATE_REDDIT:
15 | return Object.assign({}, state, {
16 | didInvalidate: true
17 | });
18 | case POSTS_GET_REQUEST:
19 | return Object.assign({}, state, {
20 | isFetching: true,
21 | didInvalidate: false
22 | });
23 | case POSTS_GET_SUCCESS:
24 | return Object.assign({}, state, {
25 | isFetching: false,
26 | didInvalidate: false,
27 | items: action.posts,
28 | lastUpdated: action.receivedAt
29 | });
30 | case POSTS_GET_FAILURE:
31 | return Object.assign({}, state, {
32 | isFetching: false,
33 | didInvalidate: false
34 | });
35 | default:
36 | return state;
37 | }
38 | }
39 |
40 | export function selectedReddit(state = 'reactjs', action) {
41 | switch (action.type) {
42 | case SELECT_REDDIT:
43 | return action.reddit;
44 | default:
45 | return state;
46 | }
47 | }
48 |
49 | export function postsByReddit(state = { }, action) {
50 | switch (action.type) {
51 | case INVALIDATE_REDDIT:
52 | case POSTS_GET_REQUEST:
53 | case POSTS_GET_SUCCESS:
54 | let postsArray = [];
55 | if(action.req && action.req.data){
56 | let data = action.req.data.data;
57 | postsArray = data.children.map(child => child.data);
58 | }
59 | return Object.assign({}, state, {
60 | [action.reddit]: posts(state[action.reddit], {
61 | type: action.type,
62 | reddit: action.reddit,
63 | posts: postsArray,
64 | receivedAt: Date.now()
65 | })
66 | });
67 |
68 | case POSTS_GET_FAILURE:
69 | return Object.assign({}, state, {
70 | [action.reddit]: posts(state[action.reddit], {
71 | type: action.type,
72 | reddit: action.reddit,
73 | posts: [],
74 | receivedAt: Date.now(),
75 | error : {
76 | status: action.error.status,
77 | statusText : action.error.statusText
78 | }
79 | })
80 | });
81 |
82 | default:
83 | return state;
84 | }
85 | }
--------------------------------------------------------------------------------
/src/common/components/Login.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import TextField from 'material-ui/lib/text-field';
3 | import RaisedButton from 'material-ui/lib/raised-button';
4 | import { bindActionCreators } from 'redux';
5 | import {connect} from 'react-redux';
6 |
7 | import {reduxForm} from 'redux-form';
8 | import Helmet from 'react-helmet'
9 | import * as UserActions from '../actions/user';
10 |
11 |
12 | class Login extends Component {
13 |
14 | constructor(props) {
15 | super(props);
16 | this.onSubmit = this.onSubmit.bind(this);
17 | }
18 |
19 | componentWillReceiveProps(nextProps) {
20 | console.log(nextProps);
21 | if(nextProps.user.logged){
22 | console.log('Ti avtorizovan, chert ti ebanij');
23 | }
24 | }
25 |
26 | onSubmit(event) {
27 | event.preventDefault();
28 | const login = this.refs.login.getValue();
29 | const password = this.refs.password.getValue();
30 | this.props.auth(login,password);
31 | console.log(`Login: ${login} Password: ${password}`);
32 | }
33 |
34 | render() {
35 | console.log(this.props.user);
36 | return (
37 |
53 | );
54 | }
55 | }
56 |
57 | Login.propTypes = {
58 | username: PropTypes.string.isRequired,
59 | password: PropTypes.string,
60 | logged: PropTypes.bool.isRequired,
61 | token: PropTypes.string,
62 | err: PropTypes.object
63 | };
64 |
65 | function mapStateToProps(state) {
66 | return {
67 | user : state.user
68 | };
69 | }
70 |
71 | function mapDispatchToProps(dispatch) {
72 | return bindActionCreators(UserActions,dispatch);
73 | }
74 |
75 |
76 | export default connect(mapStateToProps, mapDispatchToProps)(Login);
77 |
--------------------------------------------------------------------------------
/src/common/components/todo/Section.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import TodoItem from './Item';
3 | import Footer from './Footer';
4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../../actions/todos';
5 |
6 | const TODO_FILTERS = {
7 | [SHOW_ALL]: () => true,
8 | [SHOW_ACTIVE]: todo => !todo.completed,
9 | [SHOW_COMPLETED]: todo => todo.completed
10 | };
11 |
12 | class Section extends Component {
13 | constructor(props, context) {
14 | super(props, context);
15 | this.state = { filter: SHOW_ALL };
16 | }
17 |
18 | handleClearCompleted() {
19 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed);
20 | if (atLeastOneCompleted) {
21 | this.props.actions.clearCompleted();
22 | }
23 | }
24 |
25 | handleShow(filter) {
26 | this.setState({ filter });
27 | }
28 |
29 | renderToggleAll(completedCount) {
30 | const { todos, actions } = this.props;
31 | if (todos.length > 0) {
32 | return (
33 |
37 | );
38 | }
39 | }
40 |
41 | renderFooter(completedCount) {
42 | const { todos } = this.props;
43 | const { filter } = this.state;
44 | const activeCount = todos.length - completedCount;
45 |
46 | if (todos.length) {
47 | return (
48 |
53 | );
54 | }
55 | }
56 |
57 | render() {
58 | const { todos, actions } = this.props;
59 | const { filter } = this.state;
60 |
61 | const filteredTodos = todos.filter(TODO_FILTERS[filter]);
62 | const completedCount = todos.reduce((count, todo) =>
63 | todo.completed ? count + 1 : count,
64 | 0
65 | );
66 |
67 | return (
68 |
69 | {this.renderToggleAll(completedCount)}
70 |
71 | {filteredTodos.map(todo =>
72 |
73 | )}
74 |
75 | {this.renderFooter(completedCount)}
76 |
77 | );
78 | }
79 | }
80 |
81 | Section.propTypes = {
82 | todos: PropTypes.array.isRequired,
83 | actions: PropTypes.object.isRequired
84 | };
85 |
86 | export default Section;
--------------------------------------------------------------------------------
/src/common/containers/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { bindActionCreators } from 'redux';
3 | import request from 'axios';
4 | import { connect } from 'react-redux';
5 | import { Link } from 'react-router';
6 | import classNames from 'classnames';
7 | import * as LayoutActions from '../actions/layout';
8 | import * as UserActions from '../actions/user';
9 | import Helmet from 'react-helmet';
10 | import Home from '../components/Home'
11 | import Header from '../components/layout/Header'
12 | import Paper from 'material-ui/lib/paper';
13 | import cookie from 'react-cookie';
14 |
15 |
16 | class App extends Component {
17 |
18 | constructor(props){
19 | super(props);
20 | this.eventToggleSidebar = this.eventToggleSidebar.bind(this)
21 | this.eventUndo = this.eventUndo.bind(this)
22 | this.eventRedo = this.eventRedo.bind(this)
23 | }
24 |
25 | componentWillReceiveProps(nextState) {
26 | if(nextState.user.token && !cookie.load('token')) {
27 | console.log('Setting up token in cookie');
28 | cookie.save('token', nextState.user.token);
29 | }
30 | if(nextState.user.token && !nextState.user.info) {
31 | this.props.getUserInfo(nextState.user);
32 | }
33 |
34 | if(nextState.user.clearCookie && cookie.load('token')) {
35 | cookie.remove('token');
36 | this.props.toogleClearCookie();
37 | }
38 | }
39 |
40 | eventToggleSidebar(e) {
41 | e.preventDefault();
42 | this.props.toggleSidebar(!this.props.layout.sidebarOpen);
43 | }
44 |
45 | eventUndo(e) {
46 | e.preventDefault();
47 | this.props.undo();
48 | }
49 |
50 | eventRedo(e) {
51 | e.preventDefault();
52 | this.props.redo();
53 | }
54 |
55 | render() {
56 |
57 | const { user, version } = this.props;
58 |
59 |
60 | return (
61 |
62 |
63 |
64 |
Title
65 |
66 | {!this.props.children && }
67 | {this.props.children}
68 |
69 |
70 |
71 | );
72 | }
73 | }
74 |
75 | function mapStateToProps(state) {
76 | return {
77 | counter : state.counter.present,
78 | todos : state.todos.present,
79 | version : state.version,
80 | user : state.user,
81 | layout : state.layout.present
82 | };
83 | }
84 |
85 | function mapDispatchToProps(dispatch) {
86 | return bindActionCreators(Object.assign({}, LayoutActions, UserActions), dispatch);
87 | }
88 |
89 | export default connect(mapStateToProps, mapDispatchToProps)(App);
90 |
--------------------------------------------------------------------------------
/src/common/components/Reddit.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | // import { connect } from 'react-redux';
3 | import Picker from './reddit/Picker';
4 | import Posts from './reddit/Posts';
5 |
6 | class Reddit extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.handleChange = this.handleChange.bind(this);
10 | this.handleRefreshClick = this.handleRefreshClick.bind(this);
11 | }
12 |
13 | componentDidMount() {
14 | const { selectedReddit } = this.props;
15 | this.props.fetchPostsIfNeeded(selectedReddit);
16 | }
17 |
18 | componentWillReceiveProps(nextProps) {
19 | if (nextProps.selectedReddit !== this.props.selectedReddit) {
20 | const { selectedReddit } = nextProps;
21 | this.props.fetchPostsIfNeeded(selectedReddit);
22 | }
23 | }
24 |
25 | handleChange(nextReddit) {
26 | this.props.selectReddit(nextReddit);
27 | }
28 |
29 | handleRefreshClick(e) {
30 | e.preventDefault();
31 | const { selectedReddit } = this.props;
32 | this.props.invalidateReddit(selectedReddit);
33 | this.props.fetchPostsIfNeeded(selectedReddit);
34 | }
35 |
36 | render () {
37 | const { selectedReddit, posts, isFetching, lastUpdated, error } = this.props;
38 | return (
39 |
40 |
43 |
44 | {lastUpdated &&
45 |
46 | Last updated at {new Date(lastUpdated).toLocaleTimeString()}.
47 | {' '}
48 |
49 | }
50 | {!isFetching &&
51 |
53 | Refresh
54 |
55 | }
56 |
57 | {isFetching && posts.length === 0 &&
58 |
Loading...
59 | }
60 | {!isFetching && error && posts.length === 0 &&
61 |
There has been an Error
62 | }
63 | {!isFetching && !error && posts.length === 0 &&
64 |
Empty
65 | }
66 | {posts.length > 0 &&
67 |
70 | }
71 |
72 | );
73 | }
74 | }
75 |
76 | Reddit.propTypes = {
77 | selectedReddit: PropTypes.string.isRequired,
78 | posts: PropTypes.array.isRequired,
79 | error: PropTypes.object,
80 | isFetching: PropTypes.bool.isRequired,
81 | lastUpdated: PropTypes.number
82 | };
83 |
84 | export default Reddit;
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "isomorphic-redux-app",
3 | "version": "0.1.1",
4 | "apiHost": "10.0.0.9",
5 | "apiPort": "3000",
6 | "description": "An example of a isomorphic Redux application",
7 | "scripts": {
8 | "slate": "rm -rf node_modules && npm install",
9 | "clean": "rm -rf dist",
10 | "start": "set NODE_ENV=development && node src/server/index.js --colors --profile",
11 | "start-prod": "set NODE_ENV=production && node src/server/index.js --progress --colors --profile",
12 | "build": "npm run clean && set NODE_ENV=production && webpack -p --progress --colors --profile",
13 | "test": "set NODE_ENV=test && mocha --compilers js:babel-core/register --recursive"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/caljrimmer/isomorphic-redux-app.git"
18 | },
19 | "license": "MIT",
20 | "dependencies": {
21 | "axios": "0.7.0",
22 | "classnames": "^2.2.0",
23 | "cookie-parser": "^1.4.1",
24 | "express": "4.13.3",
25 | "history": "1.13.0",
26 | "inline-style-prefixer": "^0.6.3",
27 | "json-loader": "^0.5.4",
28 | "jsx-loader": "^0.13.2",
29 | "lodash.merge": "^4.0.1",
30 | "lodash.throttle": "^4.0.0",
31 | "material-ui": "^0.14.2",
32 | "react": "^0.14.6",
33 | "react-addons-create-fragment": "^0.14.6",
34 | "react-addons-pure-render-mixin": "^0.14.6",
35 | "react-addons-transition-group": "^0.14.6",
36 | "react-addons-update": "^0.14.6",
37 | "react-cookie": "^0.4.3",
38 | "react-dom": "^0.14.6",
39 | "react-helmet": "^2.3.1",
40 | "react-redux": "^4.0.0",
41 | "react-router": "1.0.0-rc1",
42 | "react-tap-event-plugin": "^0.2.1",
43 | "redux": "^3.0.5",
44 | "redux-form": "^4.1.5",
45 | "redux-router": "^1.0.0-beta7",
46 | "redux-thunk": "0.1.0",
47 | "redux-undo": "0.5.0",
48 | "warning": "^2.1.0"
49 | },
50 | "devDependencies": {
51 | "babel-core": "^6.4.0",
52 | "babel-loader": "^6.2.1",
53 | "babel-plugin-react-transform": "^2.0.0",
54 | "babel-preset-es2015": "^6.3.13",
55 | "babel-preset-react": "^6.3.13",
56 | "babel-preset-stage-2": "^6.3.13",
57 | "babel-runtime": "^6.3.19",
58 | "css-loader": "0.9.0",
59 | "expect": "1.12.1",
60 | "extract-text-webpack-plugin": "0.8.2",
61 | "file-loader": "0.8.5",
62 | "jsdom-no-contextify": "3.1.0",
63 | "merge": "1.2.0",
64 | "mocha": "2.1.0",
65 | "react-transform-catch-errors": "^1.0.1",
66 | "react-transform-hmr": "^1.0.1",
67 | "redbox-react": "^1.2.0",
68 | "redux-devtools": "^3.0.1",
69 | "redux-devtools-dock-monitor": "^1.0.1",
70 | "redux-devtools-log-monitor": "^1.0.2",
71 | "redux-logger": "2.0.2",
72 | "style-loader": "0.8.0",
73 | "url-loader": "0.5.6",
74 | "webpack": "1.11.0",
75 | "webpack-dev-middleware": "1.2.0",
76 | "webpack-hot-middleware": "2.2.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/common/components/layout/Header.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react';
2 | import LeftNav from 'material-ui/lib/left-nav';
3 | import {connect} from 'react-redux';
4 | import {Link} from 'react-router';
5 | import AppBar from 'material-ui/lib/app-bar';
6 | import RaisedButton from 'material-ui/lib/raised-button';
7 | import MenuItem from 'material-ui/lib/menus/menu-item';
8 | import IconButton from 'material-ui/lib/icon-button';
9 | import Helmet from 'react-helmet';
10 | import ThemeManager from 'material-ui/lib/styles/theme-manager';
11 | import PersonalTheme from '../../themes/personal';
12 | import { bindActionCreators } from 'redux';
13 |
14 | import * as UserActions from '../../actions/user';
15 |
16 |
17 | class Header extends Component {
18 |
19 | constructor(props) {
20 | super(props);
21 | this.state = {open: true};
22 | this.handleToggle = this.handleToggle.bind(this);
23 | this.handleLogout = this.handleLogout.bind(this);
24 | }
25 |
26 | handleLogout() {
27 | this.props.logout(this.props.user);
28 | }
29 |
30 | handleToggle() {
31 | console.log('blabla');
32 | this.setState({open: !this.state.open});
33 | }
34 |
35 | render() {
36 | const {user} = this.props;
37 | return (
38 |
39 |
Prototype}
42 | iconElementRight={}
46 | />
47 |
48 | Home Page
49 | {!user.info && Login }
50 | {!user.info && Register }
51 | {user.info && Hi, {user.info.username} }
52 | {user.info && Dashboard }
53 |
54 | About
55 | {user.info && Logout }
56 |
57 |
58 | );
59 | }
60 | }
61 |
62 | Header.getChildContext = {
63 | muiTheme: ThemeManager.getMuiTheme(PersonalTheme)
64 | };
65 |
66 | function mapStateToProps(state) {
67 | return {
68 | user : state.user
69 | };
70 | }
71 |
72 | function mapDispatchToProps(dispatch) {
73 | return bindActionCreators(UserActions,dispatch);
74 | }
75 |
76 |
77 | export default connect(mapStateToProps, mapDispatchToProps)(Header);
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var merge = require('merge');
4 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | var webpackConfig = {
7 | output: {
8 | path: path.join(__dirname, 'dist'),
9 | filename: 'bundle.js',
10 | publicPath: '/static/'
11 | },
12 | plugins: [
13 | new webpack.optimize.OccurenceOrderPlugin(),
14 | new webpack.NoErrorsPlugin()
15 | ]
16 | };
17 |
18 | if (process.env.NODE_ENV === 'production') {
19 |
20 | webpackConfig = merge(webpackConfig,{
21 | devtool: "source-map",
22 | entry : [
23 | './src/client/index.js'
24 | ],
25 | module: {
26 | loaders: [{
27 | test: /\.js$/,
28 | loader: 'babel',
29 | exclude: /node_modules/,
30 | include: __dirname
31 | },
32 | { test: /\.(png|jpg|gif|jpeg)$/, loader: 'url-loader?limit=8192'},
33 | { test: /\.json$/, loader: "json"},
34 | { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader?sourceMap') }
35 | ]},
36 | plugins : [
37 | new webpack.DefinePlugin({
38 | 'process.env': {
39 | NODE_ENV: JSON.stringify('production')
40 | }
41 | }),
42 | new ExtractTextPlugin("app.css"),
43 | new webpack.optimize.UglifyJsPlugin({minimize: true})
44 | ]
45 | });
46 |
47 | }else{
48 |
49 | webpackConfig = merge(webpackConfig,{
50 | devtool: 'inline-source-map',
51 | module: {
52 | loaders: [{
53 | test: /\.js$/,
54 | loader: 'babel',
55 | exclude: /node_modules/,
56 | include: __dirname,
57 | env: {
58 | development: {
59 | plugins: [
60 | 'react-transform'
61 | ],
62 | extra: {
63 | 'react-transform': {
64 | transforms: [{
65 | transform: 'react-transform-hmr',
66 | imports: ['react'],
67 | locals: ['module']
68 | },
69 | {
70 | transform: 'react-transform-catch-errors',
71 | imports: ['react','redbox-react' ]
72 | }
73 | ]}
74 | }
75 | }
76 | },//
77 | query: {
78 | // optional: ['runtime'],
79 | presets: ['es2015', 'stage-2', 'react'],
80 |
81 | }
82 | },
83 | { test: /\.(png|jpg|gif|jpeg)$/, loader: 'url-loader?limit=8192'},
84 | { test: /\.json$/, loader: "json"},
85 | // { test: /\.css$/, loader: 'style-loader!css-loader'}
86 | { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader?sourceMap') }
87 |
88 | ]},
89 | entry : [
90 | 'webpack-hot-middleware/client',
91 | './src/client/index.js'
92 | ],
93 | plugins : [
94 | new webpack.HotModuleReplacementPlugin(),
95 | new ExtractTextPlugin("app.css")
96 | ]
97 | });
98 |
99 | }
100 |
101 | module.exports = webpackConfig;
--------------------------------------------------------------------------------
/src/server/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cookieParser from 'cookie-parser';
3 |
4 | import webpack from 'webpack';
5 | import webpackConfig from '../../webpack.config';
6 | import webpackDevMiddleware from 'webpack-dev-middleware';
7 | import webpackHotMiddleware from 'webpack-hot-middleware';
8 |
9 | import React from 'react';
10 | import ReactDOMServer from 'react-dom/server';
11 | import { RoutingContext, match } from 'react-router';
12 | import { Provider } from 'react-redux';
13 | import createLocation from 'history/lib/createLocation';
14 | import { fetchComponentDataBeforeRender } from '../common/api/fetchComponentDataBeforeRender';
15 |
16 | import configureStore from '../common/store/configureStore';
17 | import { getUser } from '../common/api/user';
18 | import routes from '../common/routes';
19 | import packagejson from '../../package.json';
20 | import Helmet from 'react-helmet';
21 |
22 | import {connect} from 'react-redux';
23 | import {getUserInfo} from '../common/actions/user';
24 |
25 |
26 | const app = express();
27 |
28 |
29 | const renderFullPage = (html, initialState, head) => {
30 | return `
31 |
32 |
33 |
34 |
35 | ${head.title}
36 |
37 |
38 |
39 |
40 | ${html}
41 |
44 |
45 |
46 |
47 | `;
48 | };
49 |
50 | if(process.env.NODE_ENV !== 'production'){
51 | const compiler = webpack(webpackConfig);
52 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath }));
53 | app.use(webpackHotMiddleware(compiler));
54 | }else{
55 | app.use('/static', express.static(__dirname + '/../../dist'));
56 | }
57 |
58 | app.use(cookieParser());
59 |
60 | app.use(function(req, res, next) {
61 | GLOBAL.navigator = {
62 | userAgent: req.headers['user-agent']
63 | }
64 | next();
65 | });
66 |
67 | app.get('/*', function (req, res) {
68 | const location = createLocation(req.url);
69 |
70 | getUser(req.cookies.token || false, user => {
71 | // console.log(user);
72 | match({ routes, location }, (err, redirectLocation, renderProps) => {
73 |
74 | if(err) {
75 | console.error(err);
76 | return res.status(500).end('Internal server error');
77 | }
78 | if(!renderProps) {
79 | return res.status(404).end('Not found');
80 | }
81 |
82 | var store = null;
83 | if(user) {
84 | console.log('Insert with user information')
85 | store = configureStore({
86 | version: packagejson.version,
87 | user: {
88 | userId: user.id,
89 | info: user,
90 | token: req.cookies.token
91 | }
92 | });
93 | }else{
94 | console.log('Inser info without user')
95 | store = configureStore({version: packagejson.version});
96 | }
97 | const InitialView = (
98 |
99 |
100 |
101 | );
102 |
103 | //This method waits for all render component promises to resolve before returning to browser
104 | fetchComponentDataBeforeRender(store.dispatch, renderProps.components, renderProps.params)
105 | .then(html => {
106 | const componentHTML = ReactDOMServer.renderToString(InitialView);
107 | const initialState = store.getState();
108 | let head = Helmet.rewind();
109 | res.status(200).end(renderFullPage(componentHTML,initialState, head));
110 | })
111 | .catch(err => {
112 | console.log(err);
113 | res.end(renderFullPage("",{}));
114 | });
115 | });
116 |
117 | }
118 | );
119 |
120 | });
121 |
122 | const server = app.listen(3002, function () {
123 | const port = server.address().port;
124 | console.log('Example app listening at http://localhost:%s', port);
125 | });
126 |
--------------------------------------------------------------------------------