├── server ├── favicon.ico ├── utilities │ ├── mysqlConnection.js │ └── socket.js ├── middlewares │ └── authentication.js ├── routes │ ├── users.js │ ├── index.js │ ├── todos.js │ └── authentication.js ├── config │ ├── config.js │ └── passport.js ├── package.json ├── readme.md ├── models │ ├── users.js │ └── todos.js ├── index.js ├── app.js └── scripts │ └── database_creation_script.js ├── .gitignore ├── client ├── index.js ├── components │ ├── static_pages │ │ ├── AboutUs.jsx │ │ ├── SettingsPage.jsx │ │ └── landing_page │ │ │ ├── LandingPage.css │ │ │ └── LandingPage.jsx │ ├── todo_components │ │ ├── AddTodo.jsx │ │ ├── Footer.jsx │ │ ├── TodoList.jsx │ │ ├── TodoWidget.jsx │ │ └── Todo.jsx │ ├── Navbar.jsx │ └── authentication │ │ ├── LoginForm.jsx │ │ └── SignUpForm.jsx ├── index.html ├── webpack.config.js ├── utilities │ ├── ServerSocket.js │ └── RegexValidators.js ├── package.json ├── reducers │ ├── RootReducer.js │ ├── ProfileReducer.js │ ├── TodoReducer.js │ └── AuthReducer.js ├── readme.md ├── actions │ ├── ProfileActions.js │ ├── TodoActions.js │ └── AuthActions.js └── containers │ ├── MainLoginPage.jsx │ ├── MainSignUpPage.jsx │ ├── App.jsx │ ├── Root.jsx │ ├── UserProfilePage.jsx │ └── Dashboard.jsx ├── docs └── settingUpMySql.md └── readme.md /server/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aybmab/express-redux-sample/HEAD/server/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Compiled binary addons (http://nodejs.org/api/addons.html) 6 | **/build 7 | 8 | # Dependency directory 9 | **/node_modules 10 | 11 | **/.DS_STORE 12 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-core/polyfill'; 2 | import React from 'react'; 3 | import Root from './containers/Root'; 4 | 5 | let rootElement = document.body; 6 | 7 | React.render( 8 | , 9 | rootElement 10 | ); 11 | -------------------------------------------------------------------------------- /client/components/static_pages/AboutUs.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class AboutUsPage extends Component { 4 | render() { 5 | return ( 6 |
7 |

About us

8 |
9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/components/static_pages/SettingsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class SettingsPage extends Component { 4 | render() { 5 | return ( 6 |
7 |

Settings

8 |

not implemented yet

9 |
10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/utilities/mysqlConnection.js: -------------------------------------------------------------------------------- 1 | var mysql = require('mysql'); 2 | var config = require('../config/config.js'); 3 | 4 | // TODO figure out best way to be handling connections. 5 | var createConnection = function(){ 6 | var connection = mysql.createConnection(config.mysqlParams); 7 | return connection; 8 | } 9 | 10 | 11 | module.exports = createConnection; -------------------------------------------------------------------------------- /server/middlewares/authentication.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | isLoggedIn: function(req, res, next) { 4 | if (req.isAuthenticated()) return next(); 5 | res.json({error: "Not signed in"}); 6 | }, 7 | 8 | isLoggedOut: function(req, res, next) { 9 | if (!req.isAuthenticated()) return next(); 10 | res.json({error: "Already signed in"}); 11 | } 12 | 13 | }; 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/routes/users.js: -------------------------------------------------------------------------------- 1 | var authenticationMiddleware = require('../middlewares/authentication.js'); //todo apply when needed 2 | var userModel = require('../models/users.js'); 3 | 4 | var setUserRoutes = function(router){ 5 | 6 | router.get('/api/v1/users/:id', 7 | function (req, res) { 8 | var userId = req.params.id; 9 | userModel.getUserProfile(userId, 10 | function(result){ 11 | return res.json(result); 12 | } 13 | ); 14 | } 15 | ); 16 | 17 | } 18 | 19 | module.exports = setUserRoutes; 20 | -------------------------------------------------------------------------------- /client/components/todo_components/AddTodo.jsx: -------------------------------------------------------------------------------- 1 | import React, { findDOMNode, Component, PropTypes } from 'react'; 2 | 3 | export default class AddTodo extends Component { 4 | render() { 5 | return ( 6 |
7 | 8 | 11 |
12 | ); 13 | } 14 | 15 | handleClick(e) { 16 | const node = findDOMNode(this.refs.input); 17 | const text = node.value.trim(); 18 | this.props.onAddClick(text); 19 | node.value = ''; 20 | } 21 | } 22 | 23 | AddTodo.propTypes = { 24 | onAddClick: PropTypes.func.isRequired 25 | }; -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router(); 2 | var path = require('path'); 3 | 4 | // Rest API 5 | require(path.join(__dirname, './', 'todos'))(router); 6 | require(path.join(__dirname, './', 'users'))(router); 7 | 8 | // Homepage/Client 9 | router.get('/', function(req, res, next) { 10 | // res.sendFile(path.join(__dirname, '../', 'client', 'index.html')); 11 | res.sendFile(path.join(__dirname, '../', 'client', 'index.html')); 12 | }); 13 | 14 | 15 | 16 | module.exports = function(app, passport) { 17 | // set authentication routes 18 | require('./authentication.js')(app, passport); 19 | 20 | // set other routes 21 | app.use('/', router); 22 | }; 23 | -------------------------------------------------------------------------------- /server/utilities/socket.js: -------------------------------------------------------------------------------- 1 | 2 | var socketio = require('socket.io'); 3 | var todoModel = require('../models/todos'); 4 | var io; 5 | 6 | module.exports = { 7 | 8 | setServer: function(server){ 9 | io = socketio(server); 10 | 11 | io.on('connection', function(socket){ 12 | console.log('a user connected'); 13 | 14 | socket.on('viewing', function(){ 15 | todoModel.getAllUniversalTodos(function(results){ 16 | socket.emit("current-universal-todos", results); 17 | }); 18 | }); 19 | 20 | socket.on('disconnect', function(){ 21 | console.log('user disconnected'); 22 | }); 23 | }); 24 | }, 25 | 26 | notifyAllListenersOfNewTodo: function(todo){ 27 | io.emit('new-todo', todo); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | devtool: 'eval', 5 | entry: [ 6 | "./index.js" 7 | ], 8 | output: { 9 | path: __dirname+"/build", 10 | filename: "main.js" 11 | }, 12 | plugins: [ 13 | new webpack.NoErrorsPlugin(), 14 | new webpack.HotModuleReplacementPlugin(), 15 | new webpack.NoErrorsPlugin() 16 | ], 17 | resolve: { 18 | extensions: ['', '.js', '.jsx'] 19 | }, 20 | module: { 21 | loaders: [ 22 | { test: /\.js$/, loaders: ['react-hot', 'babel-loader'], exclude: /node_modules/ }, 23 | { test: /\.jsx?$/, loaders: ['react-hot', 'babel-loader'], exclude: /node_modules/ }, 24 | { test: /\.css$/, loader: "style!css" }, 25 | { test: /\.svg$/, loader: "raw" } 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /server/routes/todos.js: -------------------------------------------------------------------------------- 1 | var sockets = require('../utilities/socket'); 2 | var authenticationMiddleware = require('../middlewares/authentication.js'); //todo apply to every route starting with /api/v1 3 | var todoModel = require('../models/todos.js'); 4 | 5 | var setTodoRoutes = function(router){ 6 | 7 | router.post('/api/v1/todos/universal/addTodo', authenticationMiddleware.isLoggedIn, 8 | function(req, res) { 9 | todoModel.createUniversalTodo(req.body.text, 10 | req.body.clientUUID, 11 | req.user.id, 12 | function(result){ 13 | sockets.notifyAllListenersOfNewTodo(result); 14 | return res.json(result); 15 | } 16 | ); 17 | } 18 | ); 19 | 20 | } 21 | 22 | module.exports = setTodoRoutes; 23 | -------------------------------------------------------------------------------- /client/components/static_pages/landing_page/LandingPage.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .landing-page-header-container { 4 | width: 100%; 5 | height: 230px; 6 | margin-top: -20px; 7 | background: #D0EBFF; 8 | text-align: center; 9 | } 10 | 11 | .landing-page-header-container-title { 12 | padding-top: 50px; 13 | font-size: 50px; 14 | } 15 | 16 | .landing-page-header-container-subtitle { 17 | font-size: 20px; 18 | } 19 | 20 | 21 | .landing-page-header-container-subtitle { 22 | font-size: 20px; 23 | } 24 | 25 | .landing-page-three-column-section { 26 | 27 | padding-top: 50px; 28 | } 29 | 30 | 31 | .landing-page-three-column-section .col-lg-4 { 32 | margin-bottom: 20px; 33 | text-align: center; 34 | } 35 | .landing-page-three-column-section h2 { 36 | font-weight: normal; 37 | } 38 | .landing-page-three-column-section .col-lg-4 p { 39 | margin-right: 10px; 40 | margin-left: 10px; 41 | } 42 | -------------------------------------------------------------------------------- /server/config/config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = { 4 | 5 | mysqlParams : { 6 | socketPath : '/tmp/mysql.sock', 7 | user : "angel", 8 | password : "bunnies", 9 | database : "testmysql", 10 | multipleStatements : true // allows for multiple queries. consider making this a different connection? 11 | }, 12 | 13 | sessionOptions : { 14 | secret: 'somesecretkeythatweshouldgenerateandstoresomewhere', //TODO make real secret 15 | saveUninitialized: true, // save new sessions 16 | resave: false, // do not automatically write to the session store 17 | cookie : { 18 | httpOnly: true, 19 | maxAge: 2419200000 20 | } // TODO set secure to true when https is used 21 | } 22 | 23 | }; -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sampleapp", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "supervisor ./index.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "babel-core": "^5.8.23", 14 | "bcryptjs": "^2.2.1", 15 | "body-parser": "^1.13.3", 16 | "connect-redis": "^2.4.1", 17 | "cookie-parser": "^1.3.5", 18 | "debug": "^2.2.0", 19 | "express": "^4.13.3", 20 | "express-session": "^1.11.3", 21 | "morgan": "^1.6.1", 22 | "mysql": "^2.8.0", 23 | "passport": "^0.2.2", 24 | "passport-local": "^1.0.0", 25 | "react": "^0.13.3", 26 | "react-router": "^0.13.3", 27 | "redux-logger": "^1.0.6", 28 | "redux-thunk": "^0.1.0", 29 | "serve-favicon": "^2.3.0", 30 | "socket.io": "^1.3.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/settingUpMySql.md: -------------------------------------------------------------------------------- 1 | ``` 2 | - Download from online (requires creating an Oracle account) and install. 3 | - Make sure you add the download to your path (PATH=$PATH:/usr/local/mysql/bin); 4 | - Add an account to your mysql server 5 | - run: mysql --user=root mysql 6 | - mysql> CREATE USER 'monty'@'localhost' IDENTIFIED BY 'some_pass'; 7 | mysql> GRANT ALL PRIVILEGES ON *.* TO 'monty'@'localhost' WITH GRANT OPTION; 8 | - reference: http://dev.mysql.com/doc/refman/5.1/en/adding-users.html 9 | - In the project, update params in the config file: 10 | mysqlParams : { 11 | socketPath: '/tmp/mysql.sock', // run 'mysqladmin variables' and copy the 'socket' variable 12 | user : "angel", // whatever name you choose 13 | password : "bunnies", // whatever password you choose 14 | database : "testmysql" // I used "Sequel Pro" to create this database 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /client/utilities/ServerSocket.js: -------------------------------------------------------------------------------- 1 | import { receivedAllUniversalTodos, optimisticUniversalAddSuccess } from '../actions/TodoActions'; 2 | 3 | var socket = io(); 4 | 5 | export function linkSocketToStore(dispatch) { 6 | // Add listeners that dispatch actions here. 7 | socket.on("current-universal-todos", function(result){ 8 | // console.log("got all todos!", result); 9 | if(result.error){ 10 | //TODO handle some how 11 | alert("something went wrong retrieving all todos"); 12 | } else { 13 | dispatch(receivedAllUniversalTodos(result.todos)); 14 | } 15 | }); 16 | 17 | socket.on("new-todo", function(result){ 18 | // console.log("got a new todo!", result); 19 | if(result.error){ 20 | //Do nothing 21 | } else { 22 | dispatch(optimisticUniversalAddSuccess(result.todo)); 23 | } 24 | }); 25 | 26 | 27 | } 28 | 29 | // Add functions that emit socket events: 30 | export function registerToUniversalTodo() { 31 | socket.emit("viewing"); 32 | } 33 | -------------------------------------------------------------------------------- /client/utilities/RegexValidators.js: -------------------------------------------------------------------------------- 1 | 2 | var emailRegex = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; 3 | var displayNameRegex = /^[a-zA-Z\-\_0-9]+$/; //alphanumerics, "-" and "_" 4 | var passwordRegex = /^[a-zA-Z0-9!@#$%^&*]{6,}$/; // At least 6 characters 5 | 6 | /** 7 | * Given a trimmed string, returns true if the string matches 8 | * a proper email format. 9 | */ 10 | export function validateEmail(email) { 11 | return emailRegex.test(email); 12 | } 13 | 14 | 15 | /** 16 | * Given a trimmed string, returns true if the string contains at 17 | * least one valid character (alphanumerics) 18 | */ 19 | export function validateDisplayName(displayName) { 20 | return displayNameRegex.test(displayName); 21 | } 22 | 23 | 24 | /** 25 | * Given a trimmed string, returns true if the string contains at 26 | * least 6 valid character (alphanumerics and !@#$%^&*) 27 | */ 28 | export function validatePassword(password) { 29 | return passwordRegex.test(password); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /client/components/todo_components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class Footer extends Component { 4 | renderFilter(filter, name) { 5 | if (filter === this.props.filter) { 6 | return name; 7 | } 8 | 9 | return ( 10 | { 11 | e.preventDefault(); 12 | this.props.onFilterChange(filter); 13 | }}> 14 | {name} 15 | 16 | ); 17 | } 18 | 19 | render() { 20 | return ( 21 |

22 | Show: 23 | {' '} 24 | {this.renderFilter('Show_All', 'All')} 25 | {', '} 26 | {this.renderFilter('Show_Completed', 'Completed')} 27 | {', '} 28 | {this.renderFilter('Show_Active', 'Active')} 29 | . 30 |

31 | ); 32 | } 33 | } 34 | 35 | Footer.propTypes = { 36 | onFilterChange: PropTypes.func.isRequired, 37 | filter: PropTypes.oneOf([ 38 | 'Show_All', 39 | 'Show_Completed', 40 | 'Show_Active' 41 | ]).isRequired 42 | }; -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sampleapp", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack --progress --color --watch" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "babel-core": "^5.6.20", 14 | "babel-loader": "^5.3.1", 15 | "codemirror": "^5.4.0", 16 | "core-js": "^1.1.1", 17 | "css-loader": "^0.15.4", 18 | "fixed-data-table": "^0.4.1", 19 | "flux": "^2.0.3", 20 | "node-libs-browser": "^0.5.2", 21 | "raw-loader": "^0.5.1", 22 | "react": "^0.13.3", 23 | "react-hot-loader": "^1.2.8", 24 | "react-loader": "^1.4.0", 25 | "react-redux": "^0.9.0", 26 | "react-router": "1.0.0-beta3", 27 | "redux": "^1.0.1", 28 | "redux-logger": "^1.0.6", 29 | "redux-thunk": "^0.1.0", 30 | "style-loader": "^0.12.3", 31 | "webpack": "^1.10.1", 32 | "webpack-dev-server": "^1.10.1" 33 | }, 34 | "devDependencies": { 35 | "babel-loader": "^5.3.2", 36 | "redux-devtools": "^1.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/reducers/RootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { updateUserInfo } from './AuthReducer'; 3 | import { updateUniversalTodoList, updateUniversalUnsavedTodoList } from './TodoReducer'; 4 | import { updateProfileData } from './ProfileReducer'; 5 | 6 | // import { Add_Todo, Complete_Todo, Set_Visibility_Filter, VisibilityFilters } from '../actions/TodoActions'; 7 | // const { Show_All } = VisibilityFilters; 8 | 9 | 10 | // function updateVisibilityFilter(visibilityFilterState = Show_All, action){ 11 | // switch (action.type){ 12 | 13 | // case Set_Visibility_Filter: 14 | // return action.filter; 15 | 16 | // default: 17 | // return visibilityFilterState; 18 | // } 19 | // } 20 | 21 | const RootReducer = combineReducers({ 22 | // visibilityFilter: updateVisibilityFilter, //TODO implement or remove... 23 | universalTodos: updateUniversalTodoList, 24 | unsavedUniversalTodos: updateUniversalUnsavedTodoList, 25 | userAuthSession: updateUserInfo, 26 | 27 | //For viewing profiles. 28 | userProfileData: updateProfileData 29 | 30 | }); 31 | 32 | export default RootReducer; 33 | 34 | -------------------------------------------------------------------------------- /client/components/todo_components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Todo from './Todo'; 3 | 4 | export default class TodoList extends Component { 5 | render() { 6 | var todos = this.props.todos; 7 | var unsavedTodos = this.props.unsavedTodos; 8 | var todoElements = []; 9 | 10 | Object.keys(todos).forEach( (todoId) => { 11 | todoElements.push(); 15 | } 16 | ); 17 | Object.keys(unsavedTodos).forEach( (todoId) => { 18 | todoElements.push(); 22 | } 23 | ); 24 | todoElements.reverse(); 25 | 26 | return ( 27 |
    28 | {todoElements} 29 |
30 | ); 31 | } 32 | } 33 | 34 | TodoList.propTypes = { 35 | onTodoClick: PropTypes.func.isRequired, 36 | todos: PropTypes.object.isRequired 37 | }; -------------------------------------------------------------------------------- /client/components/todo_components/TodoWidget.jsx: -------------------------------------------------------------------------------- 1 | //TODO move "isLoggedIn logic" out 2 | import React, { Component, PropTypes } from 'react'; 3 | 4 | import AddTodo from './AddTodo'; 5 | import TodoList from './TodoList'; 6 | import Footer from './Footer'; 7 | 8 | export default class TodoWidget extends Component { 9 | componentDidMount() { 10 | this.props.fetchInitialData(); 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 |

{this.props.title}

17 | 19 | 24 |
25 | ); 26 | } 27 | 28 | } 29 | 30 | // Footer.propTypes = { 31 | // onFilterChange: PropTypes.func.isRequired, 32 | // filter: PropTypes.oneOf([ 33 | // 'Show_All', 34 | // 'Show_Completed', 35 | // 'Show_Active' 36 | // ]).isRequired 37 | // }; 38 | 39 | 40 | //TODO add footer back: 41 | //