├── .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 |
14 | 17 |
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

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 |
15 |

My name is Callum Rimmer.

16 |

You can find me at my
17 | Github account (@caljrimmer)
18 | Twitter account(@caljrimmer)

19 |
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 | [![build status](https://travis-ci.org/caljrimmer/isomorphic-redux-app.svg?branch=master&style=flat-square)](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 | 33 | 34 |
35 |

36 | Visit GitHub Repo
37 | Based on Lanyon Theme 38 |

39 |
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 | 46 |
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 | 46 |
    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 | 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 |
    38 | 39 |
    45 |
    51 | 52 | 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 |