├── Procfile ├── .gitignore ├── app ├── alt.js ├── scss │ ├── components │ │ ├── _logout.scss │ │ ├── _animations.scss │ │ ├── _entrybox.scss │ │ ├── _navigation.scss │ │ ├── _dashboard.scss │ │ ├── _login.scss │ │ ├── _note.scss │ │ └── _vote.scss │ ├── main.scss │ └── _h5bp.scss ├── images │ ├── apple-touch-icon-precomposed.png │ ├── ms-touch-icon-144x144-precomposed.png │ └── chrome-touch-icon-192x192-precomposed.png ├── app.js ├── components │ ├── Profile.react.js │ ├── Logout.react.js │ ├── Statistics.react.js │ ├── App.react.js │ ├── TopicCountItem.react.js │ ├── MainSection.react.js │ ├── Scoreboard.react.js │ ├── Dashboard.react.js │ ├── About.react.js │ ├── EntryBox.react.js │ ├── TopicItem.react.js │ ├── Note.react.js │ ├── PriorityNotes.react.js │ ├── TextInput.react.js │ ├── Navigation.react.js │ ├── Vote.react.js │ └── Login.react.js ├── utils │ ├── NoteWebAPIUtils.js │ ├── initInitialImages.js │ ├── UserWebAPIUtils.js │ ├── IsomorphicSingleRenderer.js │ ├── IsomorphicRouterRenderer.js │ └── TopicWebAPIUtils.js ├── mixins │ └── AnimationMixin.js ├── routes.js ├── actions │ ├── NoteActions.js │ ├── UserActions.js │ └── TopicActions.js └── stores │ ├── UserStore.js │ ├── NoteStore.js │ └── TopicStore.js ├── server ├── models │ ├── notes.js │ ├── topics.js │ └── user.js ├── views │ ├── index.html │ └── layout.html ├── config │ ├── passport.js │ ├── secrets.js │ ├── passport │ │ ├── local.js │ │ └── google.js │ ├── sequelize.js │ ├── express.js │ └── routes.js ├── controllers │ ├── notes.js │ ├── users.js │ └── topics.js └── index.js ├── app.json ├── Changelog.md ├── .eslintrc ├── .jscsrc ├── package.json ├── webpack.config.js └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .tmp 4 | .idea 5 | public -------------------------------------------------------------------------------- /app/alt.js: -------------------------------------------------------------------------------- 1 | var Alt = require('alt'); 2 | // This creates the alt variable in a singleton way 3 | module.exports = new Alt(); 4 | -------------------------------------------------------------------------------- /app/scss/components/_logout.scss: -------------------------------------------------------------------------------- 1 | .logout { 2 | 3 | // .logout__header 4 | &__header { 5 | text-align: center; 6 | } 7 | 8 | } -------------------------------------------------------------------------------- /app/images/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choonkending/reap/HEAD/app/images/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /app/images/ms-touch-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choonkending/reap/HEAD/app/images/ms-touch-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /app/images/chrome-touch-icon-192x192-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/choonkending/reap/HEAD/app/images/chrome-touch-icon-192x192-precomposed.png -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | var isomorphicRouterRenderer = require('./utils/IsomorphicRouterRenderer'); 2 | var alt = require('./alt'); 3 | var routes = require('./routes'); 4 | 5 | module.exports = isomorphicRouterRenderer(alt, routes); 6 | -------------------------------------------------------------------------------- /app/components/Profile.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Profile = React.createClass({ 4 | render: function() { 5 | return ( 6 |

I am a profile

7 | ); 8 | } 9 | }); 10 | 11 | module.exports = Profile; 12 | -------------------------------------------------------------------------------- /app/components/Logout.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Logout = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 |

Hey m8, you have been logged out

8 |
9 | ); 10 | } 11 | }); 12 | 13 | module.exports = Logout; 14 | -------------------------------------------------------------------------------- /app/components/Statistics.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Statistics = React.createClass({ 3 | render: function(){ 4 | return ( 5 |
6 | {this.props.topTopic} 7 | {this.props.topStat + '%'} 8 |
9 | ); 10 | } 11 | }); 12 | 13 | module.exports = Statistics; -------------------------------------------------------------------------------- /app/scss/components/_animations.scss: -------------------------------------------------------------------------------- 1 | .opaque--false > span { 2 | opacity: 0; 3 | } 4 | 5 | .opaque--true > span { 6 | opacity: 1; 7 | } 8 | 9 | @-webkit-keyframes move { 10 | from { 11 | transfrom: translateY(0px); 12 | opacity: 0; 13 | } 14 | to { 15 | transform: translateY(20px); 16 | opacity: 1; 17 | } 18 | } 19 | 20 | .move { 21 | -webkit-animation: move 1s ease 2.5s; 22 | } 23 | -------------------------------------------------------------------------------- /server/models/notes.js: -------------------------------------------------------------------------------- 1 | var Sequelize = require('sequelize'); 2 | var sequelize = require('../config/sequelize'); 3 | 4 | var Note = sequelize.define('Note', { 5 | id: Sequelize.STRING, 6 | uid: Sequelize.STRING, 7 | title: Sequelize.STRING, 8 | description: Sequelize.STRING 9 | }, { 10 | freezeTableName: true // model tableName will be the same as the model name 11 | }); 12 | 13 | module.exports = Note; -------------------------------------------------------------------------------- /app/utils/NoteWebAPIUtils.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | module.exports = { 4 | /* 5 | @param {Promise} 6 | */ 7 | create: function(data) { 8 | return $.ajax({ 9 | url: '/note', 10 | type: 'POST', 11 | data: data 12 | }); 13 | }, 14 | 15 | getAll: function() { 16 | return $.ajax({ 17 | url: '/note', 18 | type: 'GET' 19 | }); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /app/mixins/AnimationMixin.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | const createSpanElementWithTransition = (letter, i) => { 4 | return React.createElement('span', { 5 | style: { 6 | WebkitTransition: 'opacity 0.25s ease ' + (i * 0.05) + 's' 7 | }, 8 | key: i 9 | }, letter); 10 | }; 11 | 12 | module.exports = { 13 | createTextTransition: text => text.split('').map(createSpanElementWithTransition) 14 | }; 15 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Node Express Mongo", 3 | "description": "A barebones Node.js app using Express 4 with MongoDB, and ReactJS in Flux, using Webpack as a module bundler", 4 | "repository": "https://github.com/choonkending/react-webpack-node", 5 | "logo": "https://node-js-sample.herokuapp.com/node.svg", 6 | "keywords": ["node", "express", "static", "react", "webpack", "mongodb"], 7 | "addons" : [ 8 | "mongohq" 9 | ] 10 | } -------------------------------------------------------------------------------- /server/models/topics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Schema Definitions 3 | * 4 | */ 5 | var mongoose = require('mongoose'); 6 | 7 | var TopicSchema = new mongoose.Schema({ 8 | id: String, 9 | text: String, 10 | count: { type: Number, min: 0 }, 11 | date: { type: Date, default: Date.now } 12 | }); 13 | 14 | // Compiles the schema into a model, opening (or creating, if 15 | // nonexistent) the 'Topic' collection in the MongoDB database 16 | Topic = mongoose.model('Topic', TopicSchema); 17 | 18 | -------------------------------------------------------------------------------- /app/components/App.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var RouteHandler = require('react-router').RouteHandler; 3 | var Navigation = require('../components/Navigation.react'); 4 | 5 | require('../utils/initInitialImages'); 6 | require('../scss/main.scss'); 7 | 8 | var App = React.createClass({ 9 | render: function() { 10 | return ( 11 |
12 | 13 | 14 |
15 | ); 16 | } 17 | }); 18 | 19 | module.exports = App; 20 | -------------------------------------------------------------------------------- /app/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import '_h5bp'; 2 | 3 | @import 'components/_vote'; 4 | @import 'components/_navigation'; 5 | @import 'components/_login'; 6 | @import 'components/_logout'; 7 | @import 'components/_dashboard'; 8 | @import 'components/_note'; 9 | 10 | /* 11 | Style Guide Lines. Remove if you prefer your own library 12 | */ 13 | body { 14 | font-family: 'Roboto Condensed', Helvetica, Arial, sans-serif; 15 | font-weight: normal; 16 | margin: 0; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } 22 | -------------------------------------------------------------------------------- /app/utils/initInitialImages.js: -------------------------------------------------------------------------------- 1 | /* 2 | Requiring images. Webpack will copy these images into the correct directory to be used 3 | by the template. 4 | 5 | Note: I am still experimenting with including images with webpack. Was thinking of using html-loader, 6 | but it did not seem to fit in with the webpack + express setup we have. 7 | */ 8 | require('../images/apple-touch-icon-precomposed.png'); 9 | require('../images/chrome-touch-icon-192x192-precomposed.png'); 10 | require('../images/ms-touch-icon-144x144-precomposed.png'); 11 | 12 | module.exports = {}; 13 | -------------------------------------------------------------------------------- /app/utils/UserWebAPIUtils.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | 3 | module.exports = { 4 | /* 5 | @param {Promise} 6 | */ 7 | manuallogin: function(data) { 8 | return $.ajax({ 9 | url: '/login', 10 | type: 'POST', 11 | data: data 12 | }); 13 | }, 14 | 15 | logout: function() { 16 | return $.ajax({ 17 | url: '/logout', 18 | type: 'GET' 19 | }); 20 | }, 21 | 22 | signUp: function(data) { 23 | return $.ajax({ 24 | url: '/signup', 25 | type: 'POST', 26 | data: data 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /app/components/TopicCountItem.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var TopicCountItem = React.createClass({ 4 | propTypes: { 5 | key: React.PropTypes.string, 6 | title: React.PropTypes.string, 7 | count: React.PropTypes.number 8 | }, 9 | render: function() { 10 | return ( 11 |
  • 12 | {this.props.title} 13 | {this.props.count} 14 |
  • 15 | ); 16 | } 17 | }); 18 | 19 | module.exports = TopicCountItem; 20 | -------------------------------------------------------------------------------- /server/views/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block styles %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
    {{isomorphic|safe}}
    10 | 11 | 12 | 13 | {# #} 14 | 15 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /app/scss/components/_entrybox.scss: -------------------------------------------------------------------------------- 1 | .entrybox { 2 | 3 | width: 100%; 4 | margin-bottom: 71.796875px; 5 | margin-bottom: 5vw; 6 | align-self: center; 7 | 8 | // .entrybox__header 9 | &__header { 10 | 11 | text-align: center; 12 | 13 | } 14 | 15 | // .entrybox__input 16 | &__input { 17 | 18 | display: block; 19 | margin: 0 auto; 20 | width: 718px; 21 | width: 50vw; 22 | height: 71.796875px; 23 | height: 5vw; 24 | line-height: 71.796875px; 25 | line-height: 5vw; 26 | font-size: 43.0800018310547px; 27 | font-size: 3vw; 28 | border-width: 0 0 2px 0; 29 | border-bottom: 2px solid #000; 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /server/config/passport.js: -------------------------------------------------------------------------------- 1 | /* Initializing passport.js */ 2 | var User = require('../models/user'); 3 | var local = require('./passport/local'); 4 | var google = require('./passport/google'); 5 | 6 | /* 7 | * Expose 8 | */ 9 | module.exports = function(app, passport, config) { 10 | // serialize sessions 11 | passport.serializeUser(function(user, done) { 12 | done(null, user.id); 13 | }); 14 | 15 | passport.deserializeUser(function(id, done) { 16 | User.findById(id, function(err, user) { 17 | done(err, user); 18 | }); 19 | }); 20 | 21 | //use the following strategies 22 | passport.use(local); 23 | passport.use(google); 24 | }; -------------------------------------------------------------------------------- /app/scss/components/_navigation.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Navigation Menu for the Navigation.react 3 | */ 4 | 5 | .navigation { 6 | 7 | // .navigation__item 8 | &__item { 9 | 10 | display: inline-block; 11 | text-decoration: none; 12 | padding: 14.3599996566772px 28.7199993133545px; 13 | padding: 1vw 1.5vw; 14 | color: #000; 15 | 16 | &:visited { 17 | color: #000; 18 | } 19 | 20 | // .navigation__item--logo 21 | &--logo { 22 | 23 | font-size: 24px; 24 | font-size: 5vh; 25 | line-height: 32px; 26 | line-height: 4.8vh; 27 | 28 | } 29 | 30 | // .navigation__item--active 31 | &--active { 32 | 33 | color: #2196F3; 34 | 35 | } 36 | 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /app/scss/components/_dashboard.scss: -------------------------------------------------------------------------------- 1 | // .dashboard 2 | .dashboard { 3 | 4 | display: table; 5 | height: 100%; 6 | width: 100%; 7 | 8 | // .dashboard__header 9 | &__header { 10 | 11 | text-align: center; 12 | 13 | } 14 | 15 | // .dashboard__navigation 16 | &__navigation { 17 | 18 | display: table-cell; 19 | 20 | // .dashboard__navigation--left-bar 21 | &--left-bar { 22 | 23 | width: 25%; 24 | 25 | } 26 | 27 | } 28 | 29 | // .dashboard__list 30 | &__list { 31 | 32 | list-style: none; 33 | 34 | } 35 | 36 | // .dashboard__list-item 37 | &__list-item { 38 | 39 | text-align: center; 40 | 41 | } 42 | 43 | // .dashboard__main 44 | &__main { 45 | 46 | display: table-cell; 47 | 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /app/components/MainSection.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TopicItem = require('./TopicItem.react'); 3 | var PropTypes = React.PropTypes; 4 | 5 | var MainSection = React.createClass({ 6 | propTypes: { 7 | topics: PropTypes.object 8 | }, 9 | /** 10 | * @return {object} 11 | */ 12 | render: function() { 13 | var topics = this.props.topics.map(function(topic) { 14 | return (); 15 | }); 16 | return ( 17 |
    18 |

    Vote for your favorite hack day idea

    19 | 20 |
    21 | ); 22 | } 23 | 24 | }); 25 | 26 | module.exports = MainSection; 27 | -------------------------------------------------------------------------------- /app/components/Scoreboard.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TopicCountItem = require('./TopicCountItem.react'); 3 | var PropTypes = React.PropTypes; 4 | 5 | var Scoreboard = React.createClass({ 6 | propTypes: { 7 | topics: PropTypes.object 8 | }, 9 | /** 10 | * @return {object} 11 | */ 12 | render: function() { 13 | var topicListItems = this.props.topics.map(function(topic) { 14 | return (); 15 | }); 16 | 17 | return ( 18 |
    19 |

    Vote count

    20 |
      21 | {topicListItems} 22 |
    23 |
    24 | ); 25 | } 26 | 27 | }); 28 | 29 | module.exports = Scoreboard; 30 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 1.1.0 2 | ====== 3 | 4 | - Added ImmutableJS 5 | - Using [alt](https://github.com/goatslacker/alt) and [iso](https://github.com/goatslacker/iso) 6 | - isomorphic [react-router](https://github.com/rackt/react-router) on the client and server 7 | - Structural changes: 8 | - Renamed: 9 | 1. SideSection.react -> Scoreboard.react 10 | 2. Header.react -> Entrybox.react 11 | 3. NavigationBar.react -> Navigation.react 12 | 4. _navbar.scss -> navigation.scss 13 | - Removed: 14 | - AppDispatcher 15 | - Constants 16 | - InputFormField.react.js 17 | - TopicStore to use alt's alt.createStore 18 | - TopicActions to use alt's alt.createAction 19 | - With alt, there won't need to be a dispatcher and constants 20 | - Using webpack to build client and serverside bundles 21 | - Removing `/** @jsx React.DOM */` 22 | - Temporarily commented out AnimationMixin -------------------------------------------------------------------------------- /app/components/Dashboard.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Router = require('react-router'); 3 | var Link = Router.Link; 4 | var RouteHandler = Router.RouteHandler; 5 | 6 | var Dashboard = React.createClass({ 7 | 8 | render: function() { 9 | return ( 10 |
    11 |
    12 |

    Your Dashboard

    13 |
      14 |
    • Profile
    • 15 |
    • My list
    • 16 |
    17 |
    18 |
    19 | 20 |
    21 |
    22 | ); 23 | } 24 | }); 25 | 26 | module.exports = Dashboard; 27 | -------------------------------------------------------------------------------- /server/config/secrets.js: -------------------------------------------------------------------------------- 1 | /** Important **/ 2 | /** You should not be committing this file to GitHub **/ 3 | /** Repeat: DO! NOT! COMMIT! THIS! FILE! TO! YOUR! REPO! **/ 4 | 5 | module.exports = { 6 | // Find the appropriate database to connect to, default to localhost if not found. 7 | db: { 8 | mongo: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/ReactWebpackNode', 9 | postgres: { 10 | uri: process.env.DATABASE_URL, 11 | name: 'ReactWebpackNode', 12 | username: process.env.PGUSER || 'root', 13 | password: 'password' 14 | } 15 | }, 16 | sessionSecret: process.env.SESSION_SECRET || 'Your Session Secret goes here', 17 | google: { 18 | clientID: process.env.GOOGLE_CLIENTID || '62351010161-eqcnoa340ki5ekb9gvids4ksgqt9hf48.apps.googleusercontent.com', 19 | clientSecret: process.env.GOOGLE_SECRET || '6cKCWD75gHgzCvM4VQyR5_TU', 20 | callbackURL: process.env.GOOGLE_CALLBACK || "/auth/google/callback" 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /app/components/About.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var AnimationMixin = require('../mixins/AnimationMixin'); 3 | var classnames = require('classnames'); 4 | 5 | var About = React.createClass({ 6 | mixins: [AnimationMixin], 7 | getInitialState: function() { 8 | return { 9 | opaque: false 10 | }; 11 | }, 12 | componentWillMount: function() { 13 | this.setState({ 14 | opaque: true 15 | }); 16 | }, 17 | 18 | render: function() { 19 | var text = 'About Ninja Ocean'; 20 | return ( 21 |
    22 |

    26 | {this.createTextTransition(text)} 27 |

    28 |

    Ninja Ocean is comprised of a team of passionate technology experts, aimed to do good.

    31 |
    32 | ); 33 | } 34 | }); 35 | 36 | module.exports = About; 37 | -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var { Route, DefaultRoute } = require('react-router'); 3 | var App = require('./components/App.react'); 4 | var Vote = require('./components/Vote.react'); 5 | var About = require('./components/About.react'); 6 | var Login = require('./components/Login.react'); 7 | var Logout = require('./components/Logout.react'); 8 | var Dashboard = require('./components/Dashboard.react'); 9 | var Profile = require('./components/Profile.react'); 10 | var PriorityNotes = require('./components/PriorityNotes.react'); 11 | 12 | var routes = ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | module.exports = routes; 27 | -------------------------------------------------------------------------------- /server/config/passport/local.js: -------------------------------------------------------------------------------- 1 | /* 2 | Configuring local strategy to authenticate strategies 3 | Code modified from : https://github.com/madhums/node-express-mongoose-demo/blob/master/config/passport/local.js 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var LocalStrategy = require('passport-local').Strategy; 8 | var User = require('../../models/user'); 9 | 10 | /* 11 | By default, LocalStrategy expects to find credentials in parameters named username and password. 12 | If your site prefers to name these fields differently, options are available to change the defaults. 13 | */ 14 | module.exports = new LocalStrategy({ 15 | usernameField : 'email' 16 | }, function(email, password, done) { 17 | User.findOne({ email: email}, function(err, user) { 18 | if(!user) return done(null, false, { message: 'Email ' + email + ' not found'}); 19 | user.comparePassword(password, function(err, isMatch) { 20 | if(isMatch) { 21 | return done(null, user); 22 | } else { 23 | return done(null, false, { message: 'Invalid email or password'}); 24 | } 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react"], 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "rules": { 10 | "quotes": [2, "single"], 11 | "eol-last": [2], 12 | "no-mixed-requires": [0], 13 | "no-underscore-dangle": [0], 14 | "camelcase": 2, 15 | "dot-notation": [2, {"allowSnakeCase": true}], 16 | "curly": false, 17 | "new-cap": 1, 18 | "no-unused-expressions": false, 19 | "no-unused-vars": 1, 20 | "no-undef": 1, 21 | 22 | "react/jsx-boolean-value": 2, 23 | "react/jsx-quotes": 2, 24 | "react/jsx-no-undef": 2, 25 | "react/jsx-uses-react": 2, 26 | "react/jsx-uses-vars": 2, 27 | "react/no-did-mount-set-state": 2, 28 | "react/no-did-update-set-state": 2, 29 | "react/no-multi-comp": 2, 30 | "react/no-unknown-property": 2, 31 | "react/prop-types": 2, 32 | "react/react-in-jsx-scope": 2, 33 | "react/self-closing-comp": 2, 34 | "react/wrap-multilines": 2 35 | }, 36 | "ecmaFeatures": { 37 | "jsx": true, 38 | "modules": true, 39 | "blockBindings": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/EntryBox.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TopicActions = require('../actions/TopicActions'); 3 | var TopicTextInput = require('./TextInput.react'); 4 | 5 | var EntryBox = React.createClass({ 6 | propTypes: { 7 | topic: React.PropTypes.string 8 | }, 9 | 10 | render: function() { 11 | 12 | return ( 13 |
    14 |

    Vote for your top hack idea

    15 | 16 |
    17 | ); 18 | }, 19 | 20 | /** 21 | * Event handler called within TopicTextInput. 22 | * Defining this here allows TopicTextInput to be used in multiple places 23 | * in different ways. 24 | * @param {string} text 25 | */ 26 | _onSave: function(text) { 27 | TopicActions.create(text); 28 | }, 29 | 30 | /** 31 | * @param {object} event 32 | */ 33 | _onChange: function(text) { 34 | TopicActions.typing(text); 35 | } 36 | 37 | }); 38 | 39 | module.exports = EntryBox; 40 | -------------------------------------------------------------------------------- /app/components/TopicItem.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var TopicActions = require('../actions/TopicActions'); 3 | var PropTypes = React.PropTypes; 4 | 5 | var TopicItem = React.createClass({ 6 | propTypes: { 7 | id: PropTypes.string, 8 | text: PropTypes.string 9 | }, 10 | 11 | render: function() { 12 | return ( 13 |
  • 14 | {this.props.text} 15 | 16 | 17 | 18 |
  • 19 | ); 20 | }, 21 | 22 | _onIncrement: function() { 23 | TopicActions.increment(this.props.id); 24 | }, 25 | 26 | _onDecrement: function() { 27 | TopicActions.decrement(this.props.id); 28 | }, 29 | 30 | _onDestroyClick: function() { 31 | TopicActions.destroy(this.props.id); 32 | } 33 | 34 | }); 35 | 36 | module.exports = TopicItem; 37 | -------------------------------------------------------------------------------- /app/components/Note.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var NoteActions = require('../actions/NoteActions'); 3 | 4 | var Note = React.createClass({ 5 | _onSave: function(evt) { 6 | var dom = React.findDOMNode(this); 7 | var title = dom.querySelector('.note__title--edit').value; 8 | var description = dom.querySelector('.note__description--edit').value; 9 | // Call NoteAction to save it to the database 10 | NoteActions.savenote({ 11 | title: title, 12 | description: description 13 | }); 14 | }, 15 | 16 | render: function() { 17 | var note; 18 | if(this.props.isEdit) { 19 | note = ( 20 |
    21 | 22 | 23 | 24 |
    ); 25 | } else { 26 | note = ( 27 |
    28 |
    {this.props.title}
    29 |
    {this.props.description}
    30 |
    31 | ); 32 | } 33 | 34 | return note; 35 | } 36 | }); 37 | 38 | module.exports = Note; 39 | -------------------------------------------------------------------------------- /server/controllers/notes.js: -------------------------------------------------------------------------------- 1 | var Note = require('../models/notes'); 2 | 3 | exports.create = function(req, res) { 4 | // sequelize.sync() will, based on your model definitions, create any missing tables. 5 | // If force: true it will first drop tables before recreating them. 6 | Note.sync().then(function() { 7 | return Note.findOrCreate({where: { 8 | uid: req.user.id, 9 | id: req.body.id 10 | }, defaults: { 11 | title: req.body.title, 12 | description: req.body.description 13 | }}) 14 | .spread(function(note, created) { 15 | console.log(note.get({ 16 | plain: true 17 | })); 18 | res.status(200).send('Successful'); 19 | }); 20 | }); 21 | }; 22 | 23 | exports.get = function(req, res) { 24 | Note.sync().then(function() { 25 | return Note.findAll({ 26 | where: { 27 | uid: req.user.id 28 | } 29 | }).then(function(notes) { 30 | res.json(notes); 31 | }); 32 | }); 33 | }; 34 | 35 | exports.remove = function(req, res) { 36 | Note.sync().then(function() { 37 | return Note.destroy({ 38 | where: { 39 | uid: req.user.id, 40 | id: req.body.id 41 | } 42 | }); 43 | }); 44 | }; -------------------------------------------------------------------------------- /app/components/PriorityNotes.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Note = require('./Note.react'); 3 | var NoteActions = require('../actions/NoteActions'); 4 | var NoteStore = require('../stores/NoteStore'); 5 | 6 | var PriorityNotes = React.createClass({ 7 | getInitialState: function() { 8 | return { 9 | notes: NoteStore.getState().notes 10 | }; 11 | }, 12 | 13 | componentDidMount: function() { 14 | NoteStore.listen(this.onChange); 15 | NoteActions.getnotes(); 16 | }, 17 | 18 | componentWillUnmount: function() { 19 | NoteStore.unlisten(this.onChange); 20 | }, 21 | 22 | onChange: function() { 23 | this.setState({ 24 | notes: NoteStore.getState().notes 25 | }); 26 | }, 27 | 28 | render: function() { 29 | // Optionally we can order the notes based on note.order 30 | var savedNotes = this.state.notes.map(function(note) { 31 | return (); 35 | }); 36 | var newNote = ; 37 | return ( 38 |
    39 |

    Add a note for your day m8

    40 | {newNote} 41 | {savedNotes} 42 |
    43 | ); 44 | } 45 | 46 | }); 47 | 48 | module.exports = PriorityNotes; 49 | -------------------------------------------------------------------------------- /app/scss/components/_login.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | 3 | // .login__header 4 | &__header { 5 | 6 | text-align: center; 7 | 8 | } 9 | 10 | // .login__fieldset 11 | &__fieldset { 12 | 13 | width: 359px; 14 | width: 25vw; 15 | margin: auto; 16 | 17 | } 18 | 19 | // .login__input 20 | &__input { 21 | 22 | display: inline-block; 23 | font-size: 24px; 24 | font-size: 1.6713092322vw; 25 | line-height: 48px; 26 | line-height: 3.3426184643vw; 27 | width: 100%; 28 | height: 48px; 29 | height: 3.3426184643vw; 30 | margin: 12px; 31 | margin: 0.8356546161vw; 32 | 33 | } 34 | 35 | // .login__button 36 | &__button { 37 | 38 | display: block; 39 | width: 240px; 40 | width: 15vw; 41 | height: 48px; 42 | height: 3.3426184643vw; 43 | font-size: 24px; 44 | font-size: 1.6713092322vw; 45 | line-height: 48px; 46 | line-height: 3.3426184643vw; 47 | border: none; 48 | text-align: center; 49 | margin: 12px auto; 50 | margin: 0.8356546161vw auto; 51 | border-radius: 2px; 52 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); 53 | text-decoration: none; 54 | color: #fff; 55 | 56 | // .login__button:hover, .login__button:visited 57 | &:hover, &:visited { 58 | color: #fff; 59 | } 60 | 61 | // .login__button--green 62 | &--green { 63 | 64 | background-color: #0f9d58; 65 | 66 | } 67 | 68 | } 69 | 70 | // .login__hint 71 | &__hint { 72 | text-align: center; 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /app/actions/NoteActions.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var alt = require('../alt'); 3 | var NoteWebAPIUtils = require('../utils/NoteWebAPIUtils'); 4 | 5 | /** 6 | * Declaring NoteActions using ES2015. This is equivalent to creating 7 | * function NoteActions() {}; 8 | * NoteActions.prototype.create() = function(data) {} 9 | */ 10 | class NoteActions { 11 | savenote(data) { 12 | // Make an optimistic update 13 | var id = Date.now().toString(); 14 | _.merge(data, { 15 | id: id, 16 | isEdit: false 17 | }); 18 | this.dispatch(data); 19 | 20 | // Make API call to server to add note 21 | NoteWebAPIUtils.create(data) 22 | .done(function(res) { 23 | console.log(res); 24 | }) 25 | .fail(function(jqXHR, textStatus, errorThrown) { 26 | // dispatch an event if fails to notify user that it has failed 27 | console.log(jqXHR); 28 | console.log(textStatus); 29 | console.log(errorThrown); 30 | }); 31 | } 32 | 33 | getnotes() { 34 | var _this = this; 35 | NoteWebAPIUtils.getAll() 36 | .done(function(res) { 37 | _this.dispatch(res); 38 | }) 39 | .fail(function(jqXHR, textStatus, errorThrown) { 40 | // dispatch an event if fails to notify user that it has failed 41 | console.log(jqXHR); 42 | console.log(textStatus); 43 | console.log(errorThrown); 44 | }); 45 | } 46 | } 47 | 48 | module.exports = alt.createActions(NoteActions); 49 | -------------------------------------------------------------------------------- /app/actions/UserActions.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt'); 2 | var UserWebAPIUtils = require('../utils/UserWebAPIUtils'); 3 | 4 | /* 5 | * Declaring UserActions using ES2015. This is equivalent to creating 6 | * function UserActions() {} 7 | * AND 8 | * UserActions.prototype.create() = function(data) {} 9 | */ 10 | class UserActions { 11 | 12 | manuallogin(data) { 13 | this.dispatch(); 14 | var _this = this; 15 | UserWebAPIUtils.manuallogin(data) 16 | .then(function(response, textStatus) { 17 | if (textStatus === 'success') { 18 | // Dispatch another event for successful login 19 | _this.actions.loginsuccess(data.email); 20 | } 21 | }, function() { 22 | // Dispatch another event for a bad login 23 | }); 24 | } 25 | 26 | loginsuccess(email) { 27 | this.dispatch(email); 28 | } 29 | 30 | // Leaving this here for future use 31 | register(data) { 32 | this.dispatch(data); 33 | } 34 | 35 | logout() { 36 | this.dispatch(); 37 | var _this = this; 38 | UserWebAPIUtils.logout() 39 | .then(function(response, textStatus) { 40 | if (textStatus === 'success') { 41 | // Dispatch another event for successful login 42 | _this.actions.logoutsuccess(); 43 | } 44 | }, function() { 45 | // Dispatch another event for a bad login 46 | }); 47 | } 48 | 49 | logoutsuccess() { 50 | this.dispatch(); 51 | } 52 | } 53 | 54 | module.exports = alt.createActions(UserActions); 55 | -------------------------------------------------------------------------------- /app/utils/IsomorphicSingleRenderer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * This is a modified version of https://github.com/goatslacker/alt/blob/master/utils/IsomorphicRenderer.js 4 | * IsomorphicRenderer(alt: AltInstance, App: ReactElement): mixed 5 | * Will work with webpack 6 | * 7 | * > The glue that it takes to render a react element isomorphically 8 | * 9 | * ** This util depends on iso and react ** 10 | * 11 | * Usage: 12 | * 13 | * ```js 14 | * var IsomorphicRenderer = require('alt/utils/IsomorphicRenderer'); 15 | * var React = require('react'); 16 | * var Alt = require('alt'); 17 | * var alt = new Alt(); 18 | * 19 | * var App = React.createClass({ 20 | * render() { 21 | * return ( 22 | *
    Hello World
    23 | * ); 24 | * } 25 | * }); 26 | * 27 | * module.exports = IsomorphicRenderer(alt, App); 28 | * ``` 29 | */ 30 | module.exports = IsomorphicSingleRenderer; 31 | 32 | var Iso = require('iso'); 33 | var React = require('react'); 34 | 35 | function IsomorphicSingleRenderer(alt, App) { 36 | if (typeof window === 'undefined') { 37 | return function (state) { 38 | alt.bootstrap(state); 39 | var app = React.renderToString(React.createElement(App)); 40 | var markup = Iso.render(app, alt.takeSnapshot()); 41 | alt.flush(); 42 | return markup; 43 | } 44 | } else { 45 | Iso.bootstrap(function (state, _, node) { 46 | var app = React.createElement(App); 47 | alt.bootstrap(state); 48 | React.render(app, node); 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/components/TextInput.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var ENTER_KEY_CODE = 13; 3 | 4 | // Code modified from https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TopicTextInput.react.js 5 | var TextInput = React.createClass({ 6 | propTypes: { 7 | className: React.PropTypes.string, 8 | placeholder: React.PropTypes.string, 9 | value: React.PropTypes.string, 10 | onSave: React.PropTypes.func, 11 | onChange: React.PropTypes.func 12 | }, 13 | render: function() { 14 | return ( 15 | 18 | ); 19 | }, 20 | 21 | /** 22 | * Invokes the callback passed in as onSave, allowing this component to be 23 | * used in different ways. I personally think this makes it more reusable. 24 | */ 25 | _save: function() { 26 | this.props.onSave(this.props.value); 27 | }, 28 | 29 | /** 30 | * Invokes the callback passed in as onSave, allowing this component to be 31 | * used in different ways. I personally think this makes it more reusable. 32 | */ 33 | _onChange: function(event) { 34 | this.props.onChange(event.target.value); 35 | }, 36 | 37 | /** 38 | * @param {object} event 39 | */ 40 | _onKeyDown: function(event) { 41 | if (event.keyCode === ENTER_KEY_CODE) { 42 | this._save(); 43 | } 44 | } 45 | 46 | }); 47 | 48 | module.exports = TextInput; 49 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var fs = require('fs'); 3 | var mongoose = require('mongoose'); 4 | var app = express(); 5 | var server = require('http').Server(app); 6 | var io = require('socket.io')(server); 7 | var passport = require('passport'); 8 | var secrets = require('./config/secrets'); 9 | var sequelize = require('./config/sequelize'); 10 | 11 | // Find the appropriate database to connect to, default to localhost if not found. 12 | var mongoConnect = function() { 13 | // Connecting to MongoDB 14 | mongoose.connect(secrets.db.mongo, function(err, res) { 15 | if(err) { 16 | console.log('Error connecting to: ' + secrets.db.mongo + '. ' + err); 17 | }else { 18 | console.log('Successfully connected to: ' + secrets.db.mongo); 19 | } 20 | }); 21 | }; 22 | mongoConnect(); 23 | // Requiring sequelize also creates a connection to postgres 24 | 25 | mongoose.connection.on('error', console.log); 26 | mongoose.connection.on('disconnected', mongoConnect); 27 | 28 | 29 | 30 | // Bootstrap models 31 | fs.readdirSync(__dirname + '/models').forEach(function(file) { 32 | // Not all of the models require sequelize 33 | // Currently still leaving mongoose models and postgresql models within the same folder 34 | if(~file.indexOf('.js')) require(__dirname + '/models/' + file); 35 | }); 36 | 37 | // Bootstrap passport config 38 | require('./config/passport')(app, passport); 39 | 40 | // Bootstrap application settings 41 | require('./config/express')(app, passport); 42 | // Bootstrap routes 43 | require('./config/routes')(app, io, passport); 44 | 45 | server.listen(app.get('port')); -------------------------------------------------------------------------------- /server/controllers/users.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var User = require('../models/user'); 3 | var passport = require('passport'); 4 | 5 | /** 6 | * POST /login 7 | */ 8 | exports.postLogin = function(req, res, next) { 9 | // Do email and password validation for the server 10 | passport.authenticate('local', function(err, user, info) { 11 | if(err) return next(err); 12 | if(!user) { 13 | req.flash('errors', {msg: info.message}); 14 | } 15 | // Passport exposes a login() function on req (also aliased as logIn()) that can be used to establish a login session 16 | req.logIn(user, function(err) { 17 | if(err) return next(err); 18 | req.flash('success', { msg: 'Success! You are logged in'}); 19 | res.end('Success'); 20 | }); 21 | })(req, res, next); 22 | }; 23 | 24 | 25 | /** 26 | * GET /logout 27 | */ 28 | exports.getLogout = function(req, res, next) { 29 | // Do email and password validation for the server 30 | req.logout(); 31 | next(); 32 | }; 33 | 34 | /** 35 | * POST /signup 36 | * Create a new local account 37 | */ 38 | exports.postSignUp = function(req, res, next) { 39 | var user = new User({ 40 | email: req.body.email, 41 | password: req.body.password 42 | }); 43 | 44 | User.findOne({email: req.body.email}, function(err, existingUser) { 45 | if(existingUser) { 46 | req.flash('errors', { msg: 'Account with that email address already exists' }); 47 | } 48 | user.save(function(err) { 49 | if(err) return next(err); 50 | req.logIn(user, function(err) { 51 | if(err) return next(err); 52 | console.log('Successfully created'); 53 | res.end('Success'); 54 | }); 55 | }); 56 | }); 57 | }; 58 | 59 | -------------------------------------------------------------------------------- /app/components/Navigation.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Link = require('react-router').Link; 3 | var UserActions = require('../actions/UserActions'); 4 | var UserStore = require('../stores/UserStore'); 5 | 6 | var Navigation = React.createClass({ 7 | getInitialState: function() { 8 | return { 9 | user: UserStore.getState().user 10 | }; 11 | }, 12 | 13 | componentDidMount: function() { 14 | UserStore.listen(this.onChange); 15 | }, 16 | 17 | componentWillUnmount: function() { 18 | UserStore.unlisten(this.onChange); 19 | }, 20 | 21 | onChange: function() { 22 | this.setState({ 23 | user: UserStore.getState().user 24 | }); 25 | }, 26 | 27 | onLogout: function() { 28 | UserActions.logout(); 29 | }, 30 | 31 | render: function() { 32 | var navBlock = []; 33 | if (this.state.user.get('authenticated')) { 34 | navBlock.push(Dashboard); 35 | navBlock.push(Logout); 36 | } else { 37 | navBlock.push(Log in); 38 | } 39 | navBlock.push(About); 40 | return ( 41 | 45 | ); 46 | } 47 | }); 48 | 49 | module.exports = Navigation; 50 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Defining a User Model in mongoose 3 | * Code modified from https://github.com/sahat/hackathon-starter 4 | */ 5 | 6 | var bcrypt = require('bcrypt-nodejs'); 7 | var mongoose = require('mongoose'); 8 | var crypto = require('crypto'); 9 | 10 | // Other oauthtypes to be added 11 | 12 | /* 13 | User Schema 14 | */ 15 | 16 | var UserSchema = new mongoose.Schema({ 17 | email: { type: String, unique: true, lowercase: true}, 18 | password: String, 19 | tokens: Array, 20 | profile: { 21 | name: { type: String, default: ''}, 22 | gender: { type: String, default: ''}, 23 | location: { type: String, default: ''}, 24 | website: { type: String, default: ''}, 25 | picture: { type: String, default: ''} 26 | }, 27 | resetPasswordToken: String, 28 | resetPasswordExpires: Date, 29 | google: {} 30 | }); 31 | 32 | 33 | /** 34 | * Password hash middleware. 35 | */ 36 | UserSchema.pre('save', function(next) { 37 | var user = this; 38 | if (!user.isModified('password')) return next(); 39 | bcrypt.genSalt(5, function(err, salt) { 40 | if (err) return next(err); 41 | bcrypt.hash(user.password, salt, null, function(err, hash) { 42 | if (err) return next(err); 43 | user.password = hash; 44 | next(); 45 | }); 46 | }); 47 | }); 48 | 49 | /* 50 | Defining our own custom document instance method 51 | */ 52 | UserSchema.methods = { 53 | comparePassword: function(candidatePassword, cb) { 54 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch) { 55 | if(err) return cb(err); 56 | cb(null, isMatch); 57 | }) 58 | } 59 | }; 60 | 61 | /** 62 | * Statics 63 | */ 64 | 65 | UserSchema.statics = {} 66 | 67 | 68 | 69 | module.exports = mongoose.model('User', UserSchema); 70 | -------------------------------------------------------------------------------- /app/utils/IsomorphicRouterRenderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * This is a modified version of https://github.com/goatslacker/alt/blob/master/utils/IsomorphicRenderer.js 4 | * IsomorphicRenderer(alt: AltInstance, App: ReactElement): mixed 5 | * Will work with webpack 6 | * 7 | * > The glue that it takes to render a react element isomorphically 8 | * 9 | * ** This util depends on iso and react ** 10 | * 11 | * Usage: 12 | * 13 | * ```js 14 | * var IsomorphicRenderer = require('alt/utils/IsomorphicRenderer'); 15 | * var React = require('react'); 16 | * var Alt = require('alt'); 17 | * var alt = new Alt(); 18 | * 19 | * var App = React.createClass({ 20 | * render() { 21 | * return ( 22 | *
    Hello World
    23 | * ); 24 | * } 25 | * }); 26 | * 27 | * module.exports = IsomorphicRenderer(alt, App); 28 | * ``` 29 | */ 30 | module.exports = IsomorphicRouterRenderer; 31 | 32 | var Iso = require('iso'); 33 | var React = require('react'); 34 | var Router = require('react-router'); 35 | var routes = require('../routes'); 36 | 37 | function IsomorphicRouterRenderer(alt) { 38 | if (typeof window === 'undefined') { 39 | return function (state, url) { 40 | var markup; 41 | Router.run(routes, url, function (Handler) { 42 | alt.bootstrap(state); 43 | var content = React.renderToString(React.createElement(Handler)); 44 | markup = Iso.render(content, alt.takeSnapshot()); 45 | alt.flush(); 46 | }); 47 | return markup; 48 | }; 49 | } else { 50 | Iso.bootstrap(function (state, _, container) { 51 | alt.bootstrap(state); 52 | Router.run(routes, Router.HistoryLocation, function (Handler) { 53 | var node = React.createElement(Handler); 54 | React.render(node, container); 55 | }); 56 | }); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/config/sequelize.js: -------------------------------------------------------------------------------- 1 | var Sequelize = require('sequelize'); 2 | var secrets = require('./secrets'); 3 | var sequelize; 4 | /* 5 | * Refer to http://sequelize.readthedocs.org/en/latest/api/sequelize/ for the sequelize API 6 | * The entry point to sequelize is by importing 7 | * var Sequelize = require('sequelize'); 8 | * new Sequelize(database, [username=null], [password=null], [options={}]); 9 | * In addition to sequelize, the connection library for the dialect you want to use should also 10 | * be installed in your project. You don't need to import it however, as sequelize will take care of that. 11 | */ 12 | 13 | 14 | // If postgres.uri exists, we are on Heroku! 15 | // Otherwise on a local dev branch, we can create a database with database name and username 16 | if(secrets.db.postgres.uri) { 17 | // Instantiate sequelize with uri 18 | // Sequelize will setup a connection pool on initialization so you should ideally 19 | // only ever create one instance per application. 20 | sequelize = new Sequelize(secrets.db.postgres.uri, { 21 | dialect: 'postgres' 22 | }); 23 | } else { 24 | // We can't use the same uri logic if the database does not already exist in your local env. 25 | // To make this easier for local devs without additional work with pgAdmin or pg shell, we'll 26 | // do it this way 27 | sequelize = new Sequelize(secrets.db.postgres.name, 28 | secrets.db.postgres.username, 29 | secrets.db.postgres.password, { 30 | dialect: 'postgres' 31 | }); 32 | } 33 | 34 | sequelize 35 | .authenticate() 36 | .then(function(err) { 37 | console.log('Successfully connected to postgres'); 38 | }, function (err) { 39 | console.log('Unable to connect to the postgres database:', err); 40 | }); 41 | 42 | module.exports = sequelize; -------------------------------------------------------------------------------- /app/utils/TopicWebAPIUtils.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var _ = require('lodash'); 3 | 4 | module.exports = { 5 | /* 6 | * @param topic provide a topic object {id: String, count: Number, text: String} 7 | * @return jqXHR object (which implements the Promise interface) 8 | */ 9 | addTopic: function(topic) { 10 | return $.ajax({ 11 | url: '/topic', 12 | data: JSON.stringify(topic), 13 | type: 'POST', 14 | contentType: 'application/json' 15 | }); 16 | }, 17 | 18 | /* 19 | * @param Object - partial topic or id 20 | * @param Boolean - if this is a full update then we have to specify it 21 | * @param Boolean - true if increment, false if decrement 22 | */ 23 | updateTopic: function(topic, isFull, isIncrement) { 24 | $.ajax({ 25 | url: '/topic', 26 | data: JSON.stringify(_.extend(topic, { 27 | isFull: isFull, 28 | isIncrement: isIncrement 29 | })), 30 | type: 'PUT', 31 | contentType: 'application/json' 32 | }) 33 | .then(function(data) { 34 | console.log(data); 35 | }, function(jqXHR, textStatus, errorThrown) { 36 | console.log(errorThrown); 37 | }); 38 | }, 39 | 40 | deleteTopic: function(topic) { 41 | return $.ajax({ 42 | url: '/topic', 43 | data: JSON.stringify(topic), 44 | contentType: 'application/json', 45 | type: 'DELETE' 46 | }); 47 | }, 48 | 49 | /** 50 | * Listens to the 'topic change' event emitted by the server 51 | * Whenever another client makes a change. This triggers us to call 52 | * the getAllTopics() function. 53 | */ 54 | listenToTopicChanges: function() { 55 | var hostname = document.location.hostname; 56 | var socket = io.connect('//' + hostname); 57 | var _this = this; 58 | socket.on('topic change', function() { 59 | _this.getAllTopics(); 60 | }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowSpacesInNamedFunctionExpression": { 3 | "beforeOpeningRoundBrace": true 4 | }, 5 | "disallowSpacesInFunctionDeclaration": { 6 | "beforeOpeningRoundBrace": true 7 | }, 8 | "disallowEmptyBlocks": true, 9 | "disallowSpacesInsideArrayBrackets": true, 10 | "disallowSpacesInsideParentheses": true, 11 | "disallowQuotedKeysInObjects": true, 12 | "disallowSpaceAfterObjectKeys": true, 13 | "disallowSpaceAfterPrefixUnaryOperators": true, 14 | "disallowSpaceBeforePostfixUnaryOperators": true, 15 | "disallowSpaceBeforeBinaryOperators": [ 16 | "," 17 | ], 18 | "disallowMixedSpacesAndTabs": true, 19 | "disallowTrailingWhitespace": true, 20 | "disallowTrailingComma": true, 21 | "disallowYodaConditions": true, 22 | "disallowKeywords": ["with"], 23 | "disallowMultipleLineBreaks": true, 24 | "disallowMultipleVarDecl": true, 25 | 26 | "requireSpaceBeforeBlockStatements": true, 27 | "requireParenthesesAroundIIFE": true, 28 | "requireSpacesInConditionalExpression": true, 29 | "requireBlocksOnNewline": 1, 30 | "requireCommaBeforeLineBreak": true, 31 | "requireSpaceBeforeBinaryOperators": true, 32 | "requireSpaceAfterBinaryOperators": true, 33 | "requireCamelCaseOrUpperCaseIdentifiers": true, 34 | "requireLineFeedAtFileEnd": true, 35 | "requireCapitalizedConstructors": true, 36 | "requireDotNotation": true, 37 | "requireSpacesInForStatement": true, 38 | "requireSpaceBetweenArguments": true, 39 | "requireKeywordsOnNewLine": [], 40 | "requireCurlyBraces": [ "do" ], 41 | "requireSpaceBeforeKeywords": ["if"], 42 | "requireSpaceAfterKeywords": [ 43 | "if", 44 | "else", 45 | "for", 46 | "while", 47 | "do", 48 | "switch", 49 | "case", 50 | "return", 51 | "try", 52 | "catch", 53 | "typeof" 54 | ], 55 | "safeContextKeyword": "_this", 56 | "validateQuoteMarks": "'", 57 | "validateIndentation": 2, 58 | "esnext": true, 59 | "esprima": "esprima-fb" 60 | } 61 | -------------------------------------------------------------------------------- /app/scss/components/_note.scss: -------------------------------------------------------------------------------- 1 | .note { 2 | 3 | // .note--tile 4 | &--tile { 5 | 6 | display: block; 7 | width: 300px; 8 | width: 20.8875vw; 9 | border: 1px solid black; 10 | border-radius: 4px; 11 | box-shadow: 0px 2px 5px 0px rgb(0, 0, 0); 12 | box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.26); 13 | 14 | } 15 | 16 | // .note__title 17 | &__title { 18 | 19 | font-size: 24px; 20 | font-size: 1.671vw; 21 | font-weight: 500; 22 | 23 | // .note__title--edit 24 | &--edit { 25 | 26 | display: block; 27 | margin: 0 auto; 28 | width: 718px; 29 | width: 50vw; 30 | height: 48px; 31 | height: 3.342vw; 32 | line-height: 32px; 33 | line-height: 2.228vw; 34 | font-size: 32px; 35 | font-size: 2.228vw; 36 | 37 | } 38 | 39 | } 40 | 41 | // .note__description 42 | &__description { 43 | 44 | font-size: 16px; 45 | font-size: 1.114vw; 46 | font-weight: 500; 47 | 48 | // .note__description--edit 49 | &--edit { 50 | 51 | display: block; 52 | margin: 0 auto; 53 | width: 718px; 54 | width: 50vw; 55 | font-size: 43.0800018310547px; 56 | font-size: 3vw; 57 | 58 | } 59 | 60 | } 61 | 62 | // .note__button 63 | &__button { 64 | 65 | display: block; 66 | width: 240px; 67 | width: 15vw; 68 | height: 48px; 69 | height: 3.3426184643vw; 70 | font-size: 24px; 71 | font-size: 1.6713092322vw; 72 | line-height: 48px; 73 | line-height: 3.3426184643vw; 74 | border: none; 75 | text-align: center; 76 | margin: 12px auto; 77 | margin: 0.8356546161vw auto; 78 | border-radius: 2px; 79 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); 80 | text-decoration: none; 81 | color: #fff; 82 | 83 | // .note__button:hover, .note__button:visited 84 | &:hover, &:visited { 85 | color: #fff; 86 | } 87 | 88 | // .note__button--green 89 | &--green { 90 | 91 | background-color: #0f9d58; 92 | 93 | } 94 | 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-webpack-node", 3 | "version": "1.1.0", 4 | "description": "A simple Node.js app using Express 4 with Webpack, React, alt, ImmutableJS", 5 | "repository": "https://github.com/choonkending/react-webpack-node", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node server/index.js", 9 | "build": "webpack --progress", 10 | "watch": "webpack --watch --progress", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "postinstall": "webpack -p" 13 | }, 14 | "author": "Choon Ken Ding", 15 | "license": "MIT", 16 | "devDependencies": {}, 17 | "engines": { 18 | "node": "0.10.x" 19 | }, 20 | "dependencies": { 21 | "alt": "^0.16.5", 22 | "bcrypt-nodejs": "0.0.3", 23 | "body-parser": "^1.9.3", 24 | "classnames": "^1.2.0", 25 | "connect-mongo": "^0.8.1", 26 | "cookie-parser": "^1.3.3", 27 | "cookie-session": "^1.1.0", 28 | "css-loader": "^0.14.3", 29 | "eslint": "^0.20.0", 30 | "eslint-loader": "^0.11.1", 31 | "eslint-plugin-react": "^2.2.0", 32 | "esprima-fb": "^15001.1.0-dev-harmony-fb", 33 | "express": "^4.12.3", 34 | "express-flash": "0.0.2", 35 | "express-session": "^1.10.2", 36 | "extract-text-webpack-plugin": "^0.7.0", 37 | "file-loader": "^0.8.1", 38 | "immutable": "^3.7.3", 39 | "iso": "^4.1.0", 40 | "jquery": "^2.1.3", 41 | "jscs-loader": "0.0.5", 42 | "jsx-loader": "^0.13.1", 43 | "keymirror": "^0.1.0", 44 | "lodash": "^3.7.0", 45 | "lusca": "^1.0.2", 46 | "method-override": "^2.3.1", 47 | "mongoose": "^4.0.2", 48 | "node-noop": "0.0.1", 49 | "node-sass": "^3.0.0-alpha.7", 50 | "passport": "^0.2.1", 51 | "passport-google-oauth": "^0.2.0", 52 | "passport-local": "^1.0.0", 53 | "pg": "^4.3.0", 54 | "pg-hstore": "^2.3.2", 55 | "react": "^0.13.1", 56 | "react-router": "^0.13.2", 57 | "sass-loader": "^1.0.1", 58 | "sequelize": "^2.1.3", 59 | "socket.io": "^1.2.1", 60 | "style-loader": "^0.12.0", 61 | "superagent": "^1.1.0", 62 | "swig": "^1.4.2", 63 | "url-loader": "^0.5.5", 64 | "webpack": "^1.4.13" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /server/controllers/topics.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var _ = require('lodash'); 3 | var Topic = mongoose.model('Topic'); 4 | 5 | 6 | /** 7 | * List 8 | */ 9 | exports.all = function(req, res) { 10 | Topic.find({}).exec(function(err, topics) { 11 | if(!err) { 12 | res.json(topics); 13 | }else { 14 | console.log('Error in first query'); 15 | } 16 | }); 17 | }; 18 | 19 | /** 20 | * Add a Topic 21 | */ 22 | exports.add = function(req, res) { 23 | Topic.create(req.body, function (err) { 24 | if (err) { 25 | console.log(err); 26 | res.status(400).send(err); 27 | } 28 | res.status(200).send('Added successfully'); 29 | }); 30 | }; 31 | 32 | /** 33 | * Update a topic 34 | */ 35 | exports.update = function(req, res) { 36 | var query = { id: req.body.id }; 37 | var isIncrement = req.body.isIncrement; 38 | var isFull = req.body.isFull; 39 | var omitKeys = ['id', '_id', '_v', 'isIncrement', 'isFull']; 40 | var data = _.omit(req.body, omitKeys); 41 | 42 | if(isFull) { 43 | Topic.findOneAndUpdate(query, data, function(err, data) { 44 | if(err) { 45 | console.log('Error on save!'); 46 | res.status(500).send('We failed to save to due some reason'); 47 | } 48 | res.status(200).send('Updated successfully'); 49 | }); 50 | } else { 51 | Topic.findOneAndUpdate(query, { $inc: { count: isIncrement ? 1: -1 } }, function(err, data) { 52 | if(err) { 53 | console.log('Error on save!'); 54 | // Not sure if server status is the correct status to return 55 | res.status(500).send('We failed to save to due some reason'); 56 | } 57 | res.status(200).send('Updated successfully'); 58 | }); 59 | } 60 | 61 | }; 62 | 63 | /** 64 | * 65 | */ 66 | exports.increment = function(req, res) { 67 | var query = { id: req.body.id }; 68 | 69 | }; 70 | 71 | /** 72 | * Remove a topic 73 | */ 74 | exports.remove = function(req, res) { 75 | var query = { id: req.body.id }; 76 | Topic.findOneAndRemove(query, function(err, data) { 77 | if(err) console.log('Error on delete'); 78 | res.status(200).send('Removed Successfully'); 79 | }); 80 | }; -------------------------------------------------------------------------------- /app/actions/TopicActions.js: -------------------------------------------------------------------------------- 1 | var TopicWebAPIUtils = require('../utils/TopicWebAPIUtils'); 2 | var alt = require('../alt'); 3 | /* 4 | * Declaring TopicActions using ES2015. This is equivalent to creating 5 | * function TopicActions() {} 6 | * AND 7 | * TopicActions.prototype.create() = function(data) {} 8 | */ 9 | class TopicActions { 10 | 11 | /* 12 | * @param text that user wishes to save 13 | */ 14 | create(text) { 15 | var id; 16 | var data; 17 | // Remove whitespace 18 | if (text.trim().length > 0) { 19 | // Using the current timestamp in place of a real id. 20 | id = Date.now().toString(); 21 | data = { 22 | id: id, 23 | count: 1, 24 | text: text 25 | }; 26 | 27 | // This dispatches for views to make optimistic updates 28 | this.dispatch(data); 29 | // Makes an additional call to the server API and actually adds the topic 30 | TopicWebAPIUtils.addTopic(data) 31 | .done(function(res) { 32 | // We might not need to do anything it successfully added due to optimistic updates. 33 | console.log(res); 34 | }) 35 | .fail(function(jqXHR, textStatus, errorThrown) { 36 | // dispatch an event if fails to notify user that it has failed 37 | console.log(jqXHR); 38 | console.log(textStatus); 39 | console.log(errorThrown); 40 | }); 41 | } 42 | } 43 | 44 | /* 45 | * @param String topic id to increment with 46 | */ 47 | increment(id) { 48 | this.dispatch(id); 49 | 50 | TopicWebAPIUtils.updateTopic({ id: id }, false, true); 51 | } 52 | 53 | /* 54 | * @param String topic id to decrement with 55 | */ 56 | decrement(id) { 57 | this.dispatch(id); 58 | 59 | TopicWebAPIUtils.updateTopic({ id: id }, false, false); 60 | } 61 | 62 | /* 63 | * @param String topic id to destroy 64 | */ 65 | destroy(id) { 66 | this.dispatch(id); 67 | 68 | // Keeping it consistent with the above 69 | TopicWebAPIUtils.deleteTopic({id: id}); 70 | } 71 | 72 | /* 73 | * @param String text that user is typing in input field 74 | */ 75 | typing(text) { 76 | this.dispatch(text); 77 | } 78 | 79 | } 80 | 81 | module.exports = alt.createActions(TopicActions); 82 | -------------------------------------------------------------------------------- /app/components/Vote.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var EntryBox = require('./EntryBox.react'); 3 | var MainSection = require('./MainSection.react'); 4 | var Scoreboard = require('./Scoreboard.react'); 5 | var TopicStore = require('../stores/TopicStore'); 6 | /* 7 | * This component operates as a "Controller-View". It listens for changes in the 8 | * Store and passes the new data to its children. 9 | * 10 | * React provides the kind of composable views we need for the view layer. Close to the top of the nested view hierarchy, 11 | * a special kind of view listens for events that are broadcast by the stores that it depends on. One could call this a 12 | * controller-view, as it provides the glue code to get the data from the stores and to pass this data down the chain of its 13 | * descendants. We might have one of these controller views governing any significant section of the page. 14 | * 15 | * When it receives an event from the store, it first requires the new data via the store's public getter methods. It then calls 16 | * its own setState() or forceUpdate() methods, causing its own render() method and the render() method of all its descendants to run. 17 | * 18 | * We often pass the entire state of the store down the chain of views in a single object, allowing different descendants to use 19 | * what they need. In addition to keeping the controller-like behavior at the top of the hierarchy, and thus keeping our descendant 20 | */ 21 | var Vote = React.createClass({ 22 | getInitialState: function() { 23 | return { 24 | allTopics: TopicStore.getState().topics, 25 | newTopic: TopicStore.getState().newTopic 26 | }; 27 | }, 28 | 29 | componentDidMount: function() { 30 | TopicStore.listen(this.onChange); 31 | }, 32 | 33 | componentWillUnmount: function() { 34 | TopicStore.unlisten(this.onChange); 35 | }, 36 | 37 | onChange: function() { 38 | this.setState({ 39 | allTopics: TopicStore.getState().topics, 40 | newTopic: TopicStore.getState().newTopic 41 | }); 42 | }, 43 | 44 | render: function() { 45 | return ( 46 |
    47 | 48 | 49 | 50 |
    51 | ); 52 | } 53 | }); 54 | 55 | module.exports = Vote; 56 | -------------------------------------------------------------------------------- /app/scss/components/_vote.scss: -------------------------------------------------------------------------------- 1 | @import 'entrybox'; 2 | 3 | .vote { 4 | 5 | display: flex; 6 | flex-direction: row; 7 | flex-wrap: wrap; 8 | justify-content: space-around; 9 | 10 | } 11 | 12 | .main-section { 13 | 14 | box-shadow: 0 4px 8px rgba(0,0,0,0.16), 0 4px 8px rgba(0,0,0,0.23); 15 | border-radius: 4px; 16 | border: 2px solid #000; 17 | margin: 14.36px; 18 | margin: 1vw; 19 | width: 50%; 20 | 21 | // .main-section__header 22 | &__header { 23 | text-align: center; 24 | font-size: 43.08px; 25 | font-size: 3vw; 26 | } 27 | 28 | // .main-section__list 29 | &__list { 30 | list-style: none; 31 | } 32 | 33 | } 34 | 35 | .topic-item { 36 | 37 | // .topic-item__topic 38 | &__topic { 39 | display: inline-block; 40 | min-width: 143.59375px; 41 | min-width: 10vw; 42 | } 43 | 44 | // .topic-item__button 45 | &__button { 46 | 47 | display: inline-block; 48 | position: relative; 49 | width: 43.078125px; 50 | width: 3vw; 51 | height: 43.078125px; 52 | height: 3vw; 53 | line-height:43.078125px; 54 | line-height: 3vw; 55 | border-radius: 50%; 56 | border: none; 57 | margin: 8px; 58 | margin: 0.5571273123vw; 59 | font-size: 14.3599996566772px; 60 | font-size: 1vw; 61 | color: #fff; 62 | 63 | // .topic-item__button--increment 64 | &--increment { 65 | background-color: #0f9d58; 66 | } 67 | 68 | // .topic-item__button--decrement 69 | &--decrement { 70 | background-color: #4285f4; 71 | } 72 | 73 | // .topic-item__button--destroy 74 | &--destroy { 75 | background-color: #db4437; 76 | } 77 | 78 | } 79 | 80 | } 81 | 82 | .scoreboard { 83 | 84 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 85 | border-radius: 4px; 86 | border: 1px solid #000; 87 | margin: 14.36px; 88 | margin: 1vw; 89 | width: 40%; 90 | 91 | // .scoreboard__header 92 | &__header { 93 | text-align: center; 94 | font-size: 43.08px; 95 | font-size: 3vw; 96 | } 97 | 98 | // .scoreboard__list 99 | &__list { 100 | list-style: none; 101 | } 102 | 103 | // .scoreboard__list-item 104 | &__list-item { 105 | 106 | } 107 | 108 | // .scoreboard__topic 109 | &__topic { 110 | display: inline-block; 111 | min-width: 143.59375px; 112 | min-width: 10vw; 113 | } 114 | 115 | // .scoreboard__count 116 | &__count { 117 | 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /server/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | {% block styles %}{% endblock %} 33 | 34 | 35 | 36 | {% block content %}{% endblock %} 37 | 38 | 39 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /app/components/Login.react.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var UserActions = require('../actions/UserActions'); 3 | var UserStore = require('../stores/UserStore'); 4 | 5 | var Login = React.createClass({ 6 | getInitialState: function() { 7 | return { 8 | user: UserStore.getState().user 9 | }; 10 | }, 11 | 12 | componentDidMount: function() { 13 | UserStore.listen(this.onChange); 14 | }, 15 | 16 | componentWillUnmount: function() { 17 | UserStore.unlisten(this.onChange); 18 | }, 19 | 20 | onChange: function() { 21 | this.setState({ 22 | user: UserStore.getState().user 23 | }); 24 | }, 25 | 26 | loginSubmit: function() { 27 | var email; 28 | var pswd; 29 | email = this.refs.email.getDOMNode().value; 30 | pswd = this.refs.password.getDOMNode().value; 31 | UserActions.manuallogin({ 32 | email: email, 33 | password: pswd 34 | }); 35 | }, 36 | 37 | // * 38 | // * Keeping this function here for reference purposes. Will refactor this later to work with registering 39 | // * @param evt 40 | // * @private 41 | 42 | // _registerSubmit: function(evt) { 43 | // var email, pswd; 44 | // email = this.refs.emailForm.getDOMNode().value; 45 | // pswd = this.refs.passwordForm.getDOMNode().value; 46 | // cpswd = this.refs.passwordConfirmForm.getDOMNode().value; 47 | // UserActionCreators.submitSignUpCredentials({ 48 | // email: email, 49 | // password: pswd 50 | // }); 51 | // }, 52 | 53 | render: function() { 54 | if (this.state.user.get('authenticated')) { 55 | return ( 56 |
    57 |

    You are logged in amigo

    58 |
    59 | ); 60 | } else { 61 | if (this.state.user.get('isWaiting')) { 62 | return ( 63 |
    64 |

    Waiting ...

    65 |
    66 | ); 67 | } else { 68 | return ( 69 |
    70 |

    Email Login Demo

    71 |
    72 | 73 | 74 | 75 |

    Hint: email: example@ninja.com password: ninja

    76 |
    77 |

    Google Login Demo

    78 |
    79 | Login with Google 80 |
    81 |
    82 | ); 83 | } 84 | } 85 | } 86 | }); 87 | 88 | module.exports = Login; 89 | -------------------------------------------------------------------------------- /server/config/passport/google.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 3 | var User = require('../../models/user'); 4 | var secrets = require('../secrets'); 5 | 6 | /* 7 | * OAuth Strategy taken modified from https://github.com/sahat/hackathon-starter/blob/master/config/passport.js 8 | * 9 | * - User is already logged in. 10 | * - Check if there is an existing account with a provider id. 11 | * - If there is, return an error message. (Account merging not supported) 12 | * - Else link new OAuth account with currently logged-in user. 13 | * - User is not logged in. 14 | * - Check if it's a returning user. 15 | * - If returning user, sign in and we are done. 16 | * - Else check if there is an existing account with user's email. 17 | * - If there is, return an error message. 18 | * - Else create a new account. 19 | * 20 | * The Google OAuth 2.0 authentication strategy authenticates users using a Google account and OAuth 2.0 tokens. 21 | * The strategy requires a verify callback, which accepts these credentials and calls done providing a user, as well 22 | * as options specifying a client ID, client secret, and callback URL. 23 | */ 24 | module.exports = new GoogleStrategy({ 25 | clientID: secrets.google.clientID, 26 | clientSecret: secrets.google.clientSecret, 27 | callbackURL: secrets.google.callbackURL 28 | }, function(req, accessToken, refreshToken, profile, done) { 29 | if (req.user) { 30 | User.findOne({ google: profile.id }, function(err, existingUser) { 31 | if (existingUser) { 32 | return done(null, false, { message: 'There is already a Google account that belongs to you. Sign in with that account or delete it, then link it with your current account.'}) 33 | } else { 34 | User.findById(req.user.id, function(err, user) { 35 | user.google = profile.id; 36 | user.tokens.push({ kind: 'google', accessToken: accessToken}); 37 | user.profile.name = user.profile.name || profile.displayName; 38 | user.profile.gender = user.profile.gender || profile._json.gender; 39 | user.profile.picture = user.profile.picture || profile._json.picture; 40 | user.save(function(err) { 41 | done(err, user, { message: 'Google account has been linked.' }); 42 | }); 43 | }) 44 | } 45 | }); 46 | } else { 47 | User.findOne({ google: profile.id }, function(err, existingUser) { 48 | if (existingUser) return done(null, existingUser); 49 | User.findOne({ email: profile._json.emails[0] }, function(err, existingEmailUser) { 50 | if (existingEmailUser) { 51 | return done(null, false, { message: 'There is already an account using this email address. Sign in to that account and link it with Google manually from Account Settings.'}); 52 | } else { 53 | var user = new User(); 54 | user.email = profile._json.emails[0]; 55 | user.google = profile.id; 56 | user.tokens.push({ kind: 'google', accessToken: accessToken }); 57 | user.profile.name = profile.displayName; 58 | user.profile.gender = profile._json.gender; 59 | user.profile.picture = profile._json.picture; 60 | user.save(function(err) { 61 | done(err, user); 62 | }); 63 | } 64 | }); 65 | }); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | var webpack = require('webpack'); 4 | 5 | var assetsPath = path.join(__dirname, 'public', 'assets'); 6 | var publicPath = 'assets/'; 7 | 8 | var commonLoaders = [ 9 | { test: /(\.js$|\.jsx$)/, loader: 'jsx-loader?harmony' }, 10 | { test: /\.png$/, loader: 'url-loader' }, 11 | // Copy precomposed image files over to assets path 12 | { test: /.*precomposed\.png$/, loader: 'file-loader?name=images/[name].[ext]'}, 13 | { test: /\.jpg$/, loader: 'file-loader' } 14 | ]; 15 | 16 | module.exports = [ 17 | { 18 | // The configuration for the client 19 | name: 'browser', 20 | /* The entry point of the bundle 21 | * Entry points for multi page app could be more complex 22 | * A good example of entry points would be: 23 | * entry: { 24 | * pageA: './pageA', 25 | * pageB: './pageB', 26 | * pageC: './pageC', 27 | * adminPageA: './adminPageA', 28 | * adminPageB: './adminPageB', 29 | * adminPageC: './adminPageC' 30 | * } 31 | * 32 | * We can then proceed to optimize what are the common chunks 33 | * plugins: [ 34 | * new CommonsChunkPlugin('admin-commons.js', ['adminPageA', 'adminPageB']), 35 | * new CommonsChunkPlugin('common.js', ['pageA', 'pageB', 'admin-commons.js'], 2), 36 | * new CommonsChunkPlugin('c-commons.js', ['pageC', 'adminPageC']); 37 | * ] 38 | */ 39 | entry: { 40 | app: './app/app.js' 41 | }, 42 | output: { 43 | // The output directory as absolute path 44 | path: assetsPath, 45 | // The filename of the entry chunk as relative path inside the output.path directory 46 | filename: '[name].js', 47 | // The output path from the view of the Javascript 48 | publicPath: publicPath 49 | 50 | }, 51 | module: { 52 | preLoaders: [{ 53 | test: /\.js$|.jsx$/, 54 | exclude: /node_modules/, 55 | loaders: ['eslint', 'jscs'] 56 | }], 57 | loaders: commonLoaders.concat([ 58 | { test: /\.css$/, loader: 'style!css' }, 59 | { test: /\.scss$/, 60 | loader: ExtractTextPlugin.extract('css?sourceMap!sass?sourceMap&outputStyle=expanded' + 61 | '&includePaths[]=' + (path.resolve(__dirname, './bower_components')) + 62 | '&includePaths[]=' + (path.resolve(__dirname, './node_modules'))) 63 | } 64 | ]) 65 | }, 66 | plugins: [ 67 | // extract inline css from modules into separate files 68 | new ExtractTextPlugin('styles/main.css') 69 | ] 70 | }, { 71 | // The configuration for the server-side rendering 72 | name: 'server-side rendering', 73 | entry: { 74 | app: './app/app.js' 75 | }, 76 | target: 'node', 77 | output: { 78 | // The output directory as absolute path 79 | path: assetsPath, 80 | // The filename of the entry chunk as relative path inside the output.path directory 81 | filename: '[name].server.js', 82 | // The output path from the view of the Javascript 83 | publicPath: publicPath, 84 | libraryTarget: 'commonjs2' 85 | }, 86 | externals: /^[a-z\-0-9]+$/, 87 | module: { 88 | loaders: commonLoaders 89 | }, 90 | plugins: [ 91 | new webpack.NormalModuleReplacementPlugin(/\.(css|scss)$/, 'node-noop') 92 | ] 93 | } 94 | ]; 95 | -------------------------------------------------------------------------------- /server/config/express.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var session = require('express-session'); 3 | var bodyParser = require('body-parser'); 4 | var cookieParser = require('cookie-parser'); 5 | var MongoStore = require('connect-mongo')(session); 6 | var path = require('path'); 7 | var swig = require('swig'); 8 | var secrets = require('./secrets'); 9 | var flash = require('express-flash'); 10 | var methodOverride = require('method-override'); 11 | var lusca = require('lusca'); 12 | var Iso = require('iso'); 13 | 14 | 15 | 16 | module.exports = function (app, passport) { 17 | app.set('port', (process.env.PORT || 3000)); 18 | 19 | app.engine('html', swig.renderFile); 20 | app.set('view engine', 'html'); 21 | app.set('views', path.join(__dirname, '..', 'views')); 22 | 23 | // Swig will cache templates for you, but you can disable 24 | // that and use Express's caching instead, if you like: 25 | app.set('view cache', false); 26 | // To disable Swig's cache, do the following: 27 | swig.setDefaults({cache: false}); 28 | // NOTE: You should always cache templates in a production environment. 29 | // Don't leave both of these to `false` in production! 30 | 31 | 32 | app.use(bodyParser.json()); 33 | app.use(bodyParser.urlencoded({extended: true})); // for parsing application/x-www-form-urlencoded 34 | app.use(methodOverride()); 35 | app.use('/static', express.static(path.join(__dirname, '../..', 'public'))); 36 | 37 | // Cookie parser should be above session 38 | // cookieParser - Parse Cookie header and populate req.cookies with an object keyed by cookie names 39 | // Optionally you may enable signed cookie support by passing a secret string, which assigns req.secret 40 | // so it may be used by other middleware 41 | app.use(cookieParser()); 42 | // Create a session middleware with the given options 43 | // Note session data is not saved in the cookie itself, just the session ID. Session data is stored server-side. 44 | // Options: resave: forces the session to be saved back to the session store, even if the session was never 45 | // modified during the request. Depending on your store this may be necessary, but it can also 46 | // create race conditions where a client has two parallel requests to your server and changes made 47 | // to the session in one request may get overwritten when the other request ends, even if it made no 48 | // changes(this behavior also depends on what store you're using). 49 | // saveUnitialized: Forces a session that is uninitialized to be saved to the store. A session is uninitialized when 50 | // it is new but not modified. Choosing false is useful for implementing login sessions, reducing server storage 51 | // usage, or complying with laws that require permission before setting a cookie. Choosing false will also help with 52 | // race conditions where a client makes multiple parallel requests without a session 53 | // secret: This is the secret used to sign the session ID cookie. 54 | app.use(session({ 55 | resave: true, 56 | saveUninitialized: true, 57 | secret: secrets.sessionSecret, 58 | store: new MongoStore({ url: secrets.db.mongo, autoReconnect: true}) 59 | })); 60 | 61 | app.use(passport.initialize()); 62 | app.use(passport.session()); 63 | 64 | app.use(flash()); 65 | 66 | }; 67 | -------------------------------------------------------------------------------- /app/stores/UserStore.js: -------------------------------------------------------------------------------- 1 | var Immutable = require('immutable'); 2 | var UserActions = require('../actions/UserActions'); 3 | var alt = require('../alt'); 4 | 5 | /** 6 | * Flux Explanation of Store: 7 | * Stores contain the application state and logic. Their role is somewhat similar to a model in a traditional MVC, but 8 | * they manage the state of many objects. Nor are they the same as Backbone's collections. More than simply managing a 9 | * collection of ORM-style objects, stores manage the application state for a particular domain within the application. 10 | * 11 | * A store registers itself with the dispatcher and provides it with a callback. This callback receives a data payload 12 | * as a parameter. The payload contains an action with an attribute identifying the action's type. Within the store's 13 | * registered callback, a switch statement based on the action's type is used to interpret the payload and to provide the 14 | * proper hooks into the store's internal methods. This allows an action to result in an update to the state of the store, 15 | * via the dispatcher. After all the stores are updated, they broadcast an event declaring that their state has changed, 16 | * so the views may query the new state and update themselves. 17 | * 18 | * Alt Implementation of Stores: 19 | * These are the stores returned by alt.createStore, they will not have the methods defined in your StoreModel because flux 20 | * stores do not have direct setters. However, any static methods defined in your StoreModel will be transferred to this object. 21 | * 22 | * Please note: Static methods defined on a store model are nothing more than synthetic sugar for exporting the method as a public 23 | * method of your alt instance. This means that `this` will be bound to the store instance. It is recommended to explicitly export 24 | * the methods in the constructor using StoreModel#exportPublicMethods. 25 | * 26 | */ 27 | class UserStore { 28 | 29 | /* 30 | * The constructor of your store definition receives the alt instance as its first and only argument. All instance variables, 31 | * values assigned to `this`, in any part of the StoreModel will become part of state. 32 | */ 33 | constructor() { 34 | // Instance variables defined anywhere in the store will become the state. You can initialize these in the constructor and 35 | // then update them directly in the prototype methods 36 | this.user = Immutable.Map({}); 37 | 38 | // (lifecycleMethod: string, handler: function): undefined 39 | // on: This method can be used to listen to Lifecycle events. Normally they would set up in the constructor 40 | this.on('init', this.bootstrap); 41 | this.on('bootstrap', this.bootstrap); 42 | 43 | // (listenersMap: object): undefined 44 | // bindListeners accepts an object where the keys correspond to the method in your 45 | // StoreModel and the values can either be an array of action symbols or a single action symbol. 46 | // Remember: alt generates uppercase constants for us to reference 47 | this.bindListeners({ 48 | handleLoginAttempt: UserActions.MANUALLOGIN, 49 | handleLoginSuccess: UserActions.LOGINSUCCESS, 50 | handleLogoutAttempt: UserActions.LOGOUT, 51 | handleLogoutSuccess: UserActions.LOGOUTSUCCESS 52 | }); 53 | } 54 | 55 | bootstrap() { 56 | if (!Immutable.Map.isMap(this.user)) { 57 | this.user = Immutable.fromJS(this.user); 58 | } 59 | } 60 | 61 | handleLoginAttempt() { 62 | this.user = this.user.set('isWaiting', true); 63 | this.emitChange(); 64 | } 65 | 66 | handleLoginSuccess() { 67 | this.user = this.user.merge({ isWaiting: false, authenticated: true }); 68 | this.emitChange(); 69 | } 70 | 71 | handleLogoutAttempt() { 72 | this.user = this.user.set('isWaiting', true); 73 | this.emitChange(); 74 | } 75 | 76 | handleLogoutSuccess() { 77 | this.user = this.user.merge({ isWaiting: false, authenticated: false }); 78 | this.emitChange(); 79 | } 80 | 81 | } 82 | 83 | // Export our newly created Store 84 | module.exports = alt.createStore(UserStore, 'UserStore'); 85 | -------------------------------------------------------------------------------- /server/config/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Routes for express app 3 | */ 4 | var express = require('express'); 5 | var topics = require('../controllers/topics'); 6 | var users = require('../controllers/users'); 7 | var notes = require('../controllers/notes'); 8 | var mongoose = require('mongoose'); 9 | var _ = require('lodash'); 10 | var Topic = mongoose.model('Topic'); 11 | var App = require('../../public/assets/app.server'); 12 | 13 | module.exports = function(app, io, passport) { 14 | // user routes 15 | app.post('/login', users.postLogin); 16 | app.post('/signup', users.postSignUp); 17 | app.get('/logout', users.getLogout); 18 | 19 | // google auth 20 | // Redirect the user to Google for authentication. When complete, Google 21 | // will redirect the user back to the application at 22 | // /auth/google/return 23 | // Authentication with google requires an additional scope param, for more info go 24 | // here https://developers.google.com/identity/protocols/OpenIDConnect#scope-param 25 | app.get('/auth/google', passport.authenticate('google', { scope: [ 26 | 'https://www.googleapis.com/auth/userinfo.profile', 27 | 'https://www.googleapis.com/auth/userinfo.email' 28 | ] })); 29 | 30 | // Google will redirect the user to this URL after authentication. Finish the 31 | // process by verifying the assertion. If valid, the user will be logged in. 32 | // Otherwise, the authentication has failed. 33 | app.get('/auth/google/callback', 34 | passport.authenticate('google', { 35 | successRedirect: '/', 36 | failureRedirect: '/login' 37 | })); 38 | 39 | // topic routes 40 | app.get('/topic', topics.all); 41 | 42 | app.post('/topic', function(req, res) { 43 | topics.add(req, res); 44 | // Emitting an event so clients will update themselves 45 | io.sockets.emit('topic change'); 46 | }); 47 | 48 | app.put('/topic', function(req, res) { 49 | topics.update(req, res); 50 | io.sockets.emit('topic change'); 51 | }); 52 | 53 | app.post('/note', notes.create); 54 | 55 | app.get('/note', notes.get) 56 | 57 | app.delete('/topic', function(req, res) { 58 | topics.remove(req, res); 59 | io.sockets.emit('topic change'); 60 | }); 61 | 62 | // This is where the magic happens. We take the locals data we have already 63 | // fetched and seed our stores with data. 64 | // App is a function that requires store data and url to initialize and return the React-rendered html string 65 | // Exclude any image files or map files 66 | app.get('*', function (req, res, next) { 67 | if (/(\.png$|\.map$|\.jpg$|\.js$|\.css$)/.test(req.url)) return; 68 | Topic.find({}).exec(function(err, topics) { 69 | if(!err) { 70 | var topicmap = _.indexBy(topics, 'id'); 71 | // We don't want to be seeding and generating markup with user information 72 | var user = req.user ? { authenticated: true, isWaiting: false } : { authenticated: false, isWaiting: false }; 73 | // An object that contains response local variables scoped to the request, and therefore available only to the view(s) rendered during 74 | // that request/response cycle (if any). Otherwise, this property is identical to app.locals 75 | // This property is useful for exposing request-level information such as request path name, authenticated user, user settings, and so on. 76 | // pass in data to be seeded into the TopicStore 77 | res.locals.data = { 78 | TopicStore: { topics: topicmap}, 79 | UserStore: { user: user } 80 | }; 81 | 82 | var content = App(JSON.stringify(res.locals.data || {}), req.url); 83 | res.render('index', { 84 | isomorphic: content 85 | }); 86 | }else { 87 | console.log('Error in first query'); 88 | } 89 | }); 90 | }); 91 | 92 | // Adding this in as an example 93 | io.on('connection', function(socket) { 94 | socket.emit('news', { hello: 'world'}); 95 | socket.on('my other event', function(data) { 96 | console.log(data); 97 | }) 98 | }); 99 | };; 100 | -------------------------------------------------------------------------------- /app/stores/NoteStore.js: -------------------------------------------------------------------------------- 1 | var Immutable = require('immutable'); 2 | var NoteActions = require('../actions/NoteActions'); 3 | var alt = require('../alt'); 4 | var _ = require('lodash'); 5 | 6 | /** 7 | * Flux Explanation of Store: 8 | * Stores contain the application state and logic. Their role is somewhat similar to a model in a traditional MVC, but 9 | * they manage the state of many objects. Nor are they the same as Backbone's collections. More than simply managing a 10 | * collection of ORM-style objects, stores manage the application state for a particular domain within the application. 11 | * 12 | * A store registers itself with the dispatcher and provides it with a callback. This callback receives a data payload 13 | * as a parameter. The payload contains an action with an attribute identifying the action's type. Within the store's 14 | * registered callback, a switch statement based on the action's type is used to interpret the payload and to provide the 15 | * proper hooks into the store's internal methods. This allows an action to result in an update to the state of the store, 16 | * via the dispatcher. After all the stores are updated, they broadcast an event declaring that their state has changed, 17 | * so the views may query the new state and update themselves. 18 | * 19 | * Alt Implementation of Stores: 20 | * These are the stores returned by alt.createStore, they will not have the methods defined in your StoreModel because flux 21 | * stores do not have direct setters. However, any static methods defined in your StoreModel will be transferred to this object. 22 | * 23 | * Please note: Static methods defined on a store model are nothing more than synthetic sugar for exporting the method as a public 24 | * method of your alt instance. This means that `this` will be bound to the store instance. It is recommended to explicitly export 25 | * the methods in the constructor using StoreModel#exportPublicMethods. 26 | * 27 | */ 28 | class NoteStore { 29 | 30 | /* 31 | * The constructor of your store definition receives the alt instance as its first and only argument. All instance variables, 32 | * values assigned to `this`, in any part of the StoreModel will become part of state. 33 | */ 34 | constructor() { 35 | // Instance variables defined anywhere in the store will become the state. You can initialize these in the constructor and 36 | // then update them directly in the prototype methods 37 | this.notes = Immutable.OrderedMap({}); 38 | // Do not think we need an Immutable object here 39 | 40 | // (lifecycleMethod: string, handler: function): undefined 41 | // on: This method can be used to listen to Lifecycle events. Normally they would set up in the constructor 42 | this.on('init', this.bootstrap); 43 | this.on('bootstrap', this.bootstrap); 44 | // (listenersMap: object): undefined 45 | // bindListeners accepts an object where the keys correspond to the method in your 46 | // StoreModel and the values can either be an array of action symbols or a single action symbol. 47 | // Remember: alt generates uppercase constants for us to reference 48 | this.bindListeners({ 49 | handleSaveNote: NoteActions.SAVENOTE, 50 | handleGetNotes: NoteActions.GETNOTES 51 | }); 52 | } 53 | 54 | bootstrap() { 55 | if (!Immutable.OrderedMap.isOrderedMap(this.notes)) { 56 | this.notes = Immutable.fromJS(this.notes); 57 | } 58 | } 59 | 60 | handleSaveNote(data) { 61 | var id = data.id; 62 | var length = this.notes.size; 63 | // Adding order so we can easily sort the order 64 | _.merge(data, { 65 | order: length 66 | }); 67 | this.notes = this.notes.set(id, Immutable.fromJS(data)); 68 | this.emitChange(); 69 | } 70 | 71 | handleGetNotes(data) { 72 | var newNotes = _.chain(data) 73 | .map(function(note) { 74 | return [note.id, note]; 75 | }) 76 | .zipObject() 77 | .value(); 78 | console.log(newNotes); 79 | this.notes = Immutable.fromJS(newNotes); 80 | 81 | this.emitChange(); 82 | } 83 | 84 | } 85 | 86 | // Export our newly created Store 87 | module.exports = alt.createStore(NoteStore, 'NoteStore'); 88 | -------------------------------------------------------------------------------- /app/scss/_h5bp.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * HTML5 Boilerplate 3 | * 4 | * What follows is the result of much research on cross-browser styling. 5 | * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, 6 | * Kroc Camen, and the H5BP dev community and team. 7 | */ 8 | 9 | /* ========================================================================== 10 | Base styles: opinionated defaults 11 | ========================================================================== */ 12 | 13 | html, 14 | button, 15 | input, 16 | select, 17 | textarea { 18 | color: #222; 19 | } 20 | 21 | body { 22 | font-size: 1em; 23 | line-height: 1.4; 24 | } 25 | 26 | a { 27 | color: #00e; 28 | } 29 | 30 | a:visited { 31 | color: #551a8b; 32 | } 33 | 34 | a:hover { 35 | color: #06e; 36 | } 37 | 38 | /* 39 | * Remove the gap between images and the bottom of their containers: h5bp.com/i/440 40 | */ 41 | 42 | img { 43 | vertical-align: middle; 44 | } 45 | 46 | /* 47 | * Remove default fieldset styles. 48 | */ 49 | 50 | fieldset { 51 | border: 0; 52 | margin: 0; 53 | padding: 0; 54 | } 55 | 56 | /* 57 | * Allow only vertical resizing of textareas. 58 | */ 59 | 60 | textarea { 61 | resize: vertical; 62 | } 63 | 64 | /* ========================================================================== 65 | Helper classes 66 | ========================================================================== */ 67 | 68 | /* Prevent callout */ 69 | 70 | .nocallout { 71 | -webkit-touch-callout: none; 72 | } 73 | 74 | .pressed { 75 | background-color: rgba(0, 0, 0, 0.7); 76 | } 77 | 78 | /* A hack for HTML5 contenteditable attribute on mobile */ 79 | 80 | textarea[contenteditable] { 81 | -webkit-appearance: none; 82 | } 83 | 84 | /* A workaround for S60 3.x and 5.0 devices which do not animated gif images if 85 | they have been set as display: none */ 86 | 87 | .gifhidden { 88 | position: absolute; 89 | left: -100%; 90 | } 91 | 92 | /* 93 | * Image replacement 94 | */ 95 | 96 | .ir { 97 | background-color: transparent; 98 | background-repeat: no-repeat; 99 | border: 0; 100 | direction: ltr; 101 | display: block; 102 | overflow: hidden; 103 | text-align: left; 104 | text-indent: -999em; 105 | } 106 | 107 | .ir br { 108 | display: none; 109 | } 110 | 111 | /* 112 | * Hide from both screenreaders and browsers: h5bp.com/u 113 | */ 114 | 115 | .hidden { 116 | display: none !important; 117 | visibility: hidden; 118 | } 119 | 120 | /* 121 | * Hide only visually, but have it available for screenreaders: h5bp.com/v 122 | */ 123 | 124 | .visuallyhidden { 125 | border: 0; 126 | clip: rect(0 0 0 0); 127 | height: 1px; 128 | margin: -1px; 129 | overflow: hidden; 130 | padding: 0; 131 | position: absolute; 132 | width: 1px; 133 | } 134 | 135 | /* 136 | * Extends the .visuallyhidden class to allow the element to be focusable 137 | * when navigated to via the keyboard: h5bp.com/p 138 | */ 139 | 140 | .visuallyhidden.focusable:active, 141 | .visuallyhidden.focusable:focus { 142 | clip: auto; 143 | height: auto; 144 | margin: 0; 145 | overflow: visible; 146 | position: static; 147 | width: auto; 148 | } 149 | 150 | /* 151 | * Hide visually and from screenreaders, but maintain layout 152 | */ 153 | 154 | .invisible { 155 | visibility: hidden; 156 | } 157 | 158 | /** 159 | * Clearfix helper 160 | * Used to contain floats: h5bp.com/q 161 | */ 162 | 163 | .clearfix::before, 164 | .clearfix::after { 165 | content: ""; 166 | display: table; 167 | } 168 | 169 | .clearfix::after { 170 | clear: both; 171 | } 172 | 173 | /* ========================================================================== 174 | EXAMPLE Media Queries for Responsive Design. 175 | Theses examples override the primary ('mobile first') styles. 176 | Modify as content requires. 177 | ========================================================================== */ 178 | 179 | @media only screen and (min-width: 800px) { 180 | /* Style adjustments for viewports that meet the condition */ 181 | } 182 | 183 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5), 184 | only screen and (min-resolution: 144dpi) { 185 | /* Style adjustments for viewports that meet the condition */ 186 | } 187 | -------------------------------------------------------------------------------- /app/stores/TopicStore.js: -------------------------------------------------------------------------------- 1 | var Immutable = require('immutable'); 2 | var TopicActions = require('../actions/TopicActions'); 3 | var alt = require('../alt'); 4 | 5 | /** 6 | * Flux Explanation of Store: 7 | * Stores contain the application state and logic. Their role is somewhat similar to a model in a traditional MVC, but 8 | * they manage the state of many objects. Nor are they the same as Backbone's collections. More than simply managing a 9 | * collection of ORM-style objects, stores manage the application state for a particular domain within the application. 10 | * 11 | * A store registers itself with the dispatcher and provides it with a callback. This callback receives a data payload 12 | * as a parameter. The payload contains an action with an attribute identifying the action's type. Within the store's 13 | * registered callback, a switch statement based on the action's type is used to interpret the payload and to provide the 14 | * proper hooks into the store's internal methods. This allows an action to result in an update to the state of the store, 15 | * via the dispatcher. After all the stores are updated, they broadcast an event declaring that their state has changed, 16 | * so the views may query the new state and update themselves. 17 | * 18 | * Alt Implementation of Stores: 19 | * These are the stores returned by alt.createStore, they will not have the methods defined in your StoreModel because flux 20 | * stores do not have direct setters. However, any static methods defined in your StoreModel will be transferred to this object. 21 | * 22 | * Please note: Static methods defined on a store model are nothing more than synthetic sugar for exporting the method as a public 23 | * method of your alt instance. This means that `this` will be bound to the store instance. It is recommended to explicitly export 24 | * the methods in the constructor using StoreModel#exportPublicMethods. 25 | * 26 | */ 27 | class TopicStore { 28 | 29 | /* 30 | * The constructor of your store definition receives the alt instance as its first and only argument. All instance variables, 31 | * values assigned to `this`, in any part of the StoreModel will become part of state. 32 | */ 33 | constructor() { 34 | // Instance variables defined anywhere in the store will become the state. You can initialize these in the constructor and 35 | // then update them directly in the prototype methods 36 | this.topics = Immutable.OrderedMap({}); 37 | // Do not think we need an Immutable object here 38 | this.newTopic = ''; 39 | 40 | // (lifecycleMethod: string, handler: function): undefined 41 | // on: This method can be used to listen to Lifecycle events. Normally they would set up in the constructor 42 | this.on('init', this.bootstrap); 43 | this.on('bootstrap', this.bootstrap); 44 | // (listenersMap: object): undefined 45 | // bindListeners accepts an object where the keys correspond to the method in your 46 | // StoreModel and the values can either be an array of action symbols or a single action symbol. 47 | // Remember: alt generates uppercase constants for us to reference 48 | this.bindListeners({ 49 | handleIncrement: TopicActions.INCREMENT, 50 | handleDecrement: TopicActions.DECREMENT, 51 | handleCreate: TopicActions.CREATE, 52 | handleDestroy: TopicActions.DESTROY, 53 | handleTyping: TopicActions.TYPING 54 | }); 55 | } 56 | 57 | bootstrap() { 58 | if (!Immutable.OrderedMap.isOrderedMap(this.topics)) { 59 | this.topics = Immutable.fromJS(this.topics); 60 | } 61 | this.newTopic = ''; 62 | } 63 | 64 | handleIncrement(id) { 65 | var topic = this.topics.get(id); 66 | var count = topic.get('count'); 67 | this.topics = this.topics.set(id, topic.set('count', count + 1)); 68 | this.emitChange(); 69 | } 70 | 71 | handleDecrement(id) { 72 | var topic = this.topics.get(id); 73 | var count = topic.get('count'); 74 | this.topics = this.topics.set(id, topic.set('count', count - 1)); 75 | this.emitChange(); 76 | } 77 | 78 | handleCreate(data) { 79 | var id = data.id; 80 | this.topics = this.topics.set(id, Immutable.fromJS(data)); 81 | this.emitChange(); 82 | } 83 | 84 | handleDestroy(id) { 85 | this.topics = this.topics.delete(id); 86 | this.emitChange(); 87 | } 88 | 89 | handleTyping(text) { 90 | // Check if it already exists 91 | this.newTopic = text; 92 | this.emitChange(); 93 | // otherwise, it is unchanged. 94 | } 95 | 96 | } 97 | 98 | // Export our newly created Store 99 | module.exports = alt.createStore(TopicStore, 'TopicStore'); 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REAP 2 | 3 | | React + Express + Alt + Postgresql | ... and Mongo 4 | 5 | 6 | 7 | Boilerplate for React application with webpack using alt's Flux running on a node express server with sequelize to connect to postgresl and mongoose for mongoDB. That was a mouthful. 8 | 9 | This is based off my original [React+Webpack+Node](https://github.com/choonkending/react-webpack-node). Having sequelize with postgresql might seem like a small extra dependency, but I did not wish to overcomplicate the mission of that repo (plus it would be hard for beginners to deal with an extra database). 10 | 11 | ## Why postgresql? 12 | 13 | I am all for using MongoDB for a boilerplate (which is why I am leaving it in). But being a postgres fanboy myself, this repo appeared! 14 | 15 | ## Demo site: 16 | 17 | [https://react-express-alt-postgres.herokuapp.com/](https://react-express-alt-postgres.herokuapp.com/) 18 | 19 | ## Features: 20 | 21 | 1. Isomorphic Flux using: 22 | - [alt](https://github.com/goatslacker/alt) as my Flux implementation 23 | - [iso](https://github.com/goatslacker/iso) to help with bootstrapping data for isomorphic Flux 24 | - [react-router](https://github.com/rackt/react-router) 25 | 2. Stores storing data using [ImmutableJS](https://github.com/facebook/immutable-js) data structures 26 | 3. Webpack [config file](https://github.com/choonkending/react-webpack-node/blob/master/webpack.config.js) 27 | 4. Express server 28 | 5. Mongoose for MongoDB 29 | 6. Sequelize for Postgresql 30 | 7. Includes a Procfile to enable deployment to Heroku. 31 | 32 | ## Why alt? 33 | 34 | Having isomorphic React was one of my key criteria when choosing a Flux library, which helped narrow down the scope of libraries. 35 | 36 | I found alt's implementation to be clean and simple, and like the option of allowing us to create alt instances or using singletons (and flushing the stores). I also like the direction in which alt is heading. 37 | 38 | ## Mission 39 | 40 | The aim of this repo is to incorporate the best practices to building a non-trivial apps with Reactjs and Node. 41 | I am working to document this repo extensively so it would be easy for both beginners and experts to begin dev-ing on it without pulling your hair out. 42 | 43 | 44 | ## Instructions 45 | 46 | 1. `npm install` 47 | 2. `npm start` to run locally 48 | 49 | ### Bundling with webpack 50 | 51 | 1. `npm run build` runs `webpack` will run configurations within webpack.config.js. 52 | 2. `npm run watch` runs `webpack --watch` to watch and recompile for changes. 53 | 54 | #### Where do you compile your scss? 55 | We use [ExtractTextPlugin](https://github.com/webpack/extract-text-webpack-plugin) to extract compiled css in our [webpack config file](https://github.com/choonkending/react-webpack-node/blob/master/webpack.config.js) 56 | 57 | ### Setting up your Database 58 | 59 | #### Postgresql 60 | 61 | 1. `npm install --save sequelize` 62 | 2. `npm install --save pg pg-hstore` 63 | 64 | #### MongoDB 65 | 66 | 1. `brew update` 67 | 2. `brew install mongodb` 68 | 3. `mongod` (Make sure you have the permissions to the directory /data/db) 69 | 70 | Note: 71 | 72 | For local dev, you have to create your database locally, by following either steps: 73 | 1. `createdb ReactWebpackNode` on command line, read more [here](http://www.postgresql.org/docs/9.3/static/app-createdb.html) 74 | 2. Creating it manually using pgadmin 75 | 3. psql magic 76 | 77 | ### Deploying to Heroku 78 | 79 | 1. `heroku create` 80 | 2. `heroku app:rename newname` if you need to 81 | 3. `git push heroku master` 82 | 83 | Note: If you are working from a different machine and get `heroku does not appear to be a remote repository` message, be sure to run `git remote add heroku git@heroku.com:appname.git`. 84 | 4. `heroku open` to open the link 85 | 5. If you wish to have a database setup on Heroku, remember to use the commands below for the following databases: 86 | MongoDB: 87 | - `heroku addons:add mongohq` or `heroku addons:add mongolab` 88 | Postgresql: 89 | - `heroku addons:create heroku-postgresql` 90 | 91 | Note: For Google Auth, read [Setting up Google Authentication](https://github.com/choonkending/react-webpack-node/tree/feature/passport_google_auth#setting-up-google-authentication) below 92 | 93 | ### Deploying to Digital Ocean 94 | 95 | 1. Create a Droplet 96 | 2. Follow [this](https://www.digitalocean.com/community/tutorials/how-to-set-up-a-node-js-application-for-production-on-ubuntu-14-04) or 97 | [this](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-an-ubuntu-14-04-server) tutorial 98 | to set up nodejs 99 | 3. Follow [this](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-12-04) tutorial to install mongodb 100 | 4. git clone this repo 101 | 5. `npm install` 102 | 6. `sudo npm install pm2 -g` 103 | 7. `pm2 start server/index.js` 104 | 8. `pm2 startup ubuntu` 105 | 9. `sudo env PATH=$PATH:/usr/local/bin pm2 startup ubuntu -u sammy` 106 | 107 | ## Component Hierarchy 108 | 109 | - app.js 110 | - App.react 111 | - NavigationBar.react 112 | - RouteHandler 113 | - Vote.react 114 | - EntryBox.react 115 | - MainSection.react 116 | - Scoreboard.react 117 | - Login.react 118 | - Logout.react 119 | - About.react 120 | - Dashboard.react 121 | - PriorityNotes.react 122 | - Profile.react 123 | 124 | ## IsomorphicRouterRenderer 125 | 126 | This is a modified version of alt's IsomorphicRenderer. I wished to use webpack to build my server and client side code, but wanted to easily bootstrap data into the stores, and render the correct component using react-router. This takes into account the fact that we're using a singleton store and flushing it everytime (as opposed to creating an instance everytime). 127 | 128 | ## Questions: 129 | 130 | - Google Authentication does not work locally or on heroku! 131 | 132 | #### Setting up Google Authentication 133 | 134 | 1. Follow [these steps from Google](https://developers.google.com/identity/protocols/OpenIDConnect) to create your API keys on [Google Developers Console](https://console.developers.google.com/) 135 | 2. Under APIs & Auth, Copy your Client ID and Client Secret 136 | 137 | #### Dev 138 | For Google Auth to work locally, you need to do the following in your terminal before starting the server: 139 | `export GOOGLE_CLIENTID=YOUR_CLIENTID` 140 | `export GOOGLE_SECRET=YOUR_SECRET` 141 | 142 | #### Heroku 143 | 144 | Fret not! Heroku's covered [this](https://devcenter.heroku.com/articles/config-vars) pretty well. 145 | 146 | 1. `heroku config:set GOOGLE_CLIENTID=YOUR_CLIENTID` 147 | 148 | 2. `heroku config:set GOOGLE_SECRET=YOUR_SECRET` 149 | 150 | 3. `heroku config:set GOOGLE_CALLBACK=YOUR_CALLBACK` 151 | 152 | - Postgresql does not work locally. It throws a role "root" does not exist error! 153 | 154 | You might not have sufficient permissions for the database. A quick way to fix this is to: 155 | 156 | 1. `export PGUSER=`whoami`` (secrets.js defaults to process.env.PGUSER) 157 | 158 | ## Todo: 159 | 160 | 1. My efforts will be focused primarily on [React+Webpack+Node](https://github.com/choonkending/react-webpack-node). However, so if you have any questions/issues with this repo, feel free to create an issue! 161 | 162 | ## How to Contribute: 163 | 164 | 1. As this repo is still in its baby stages, any suggestions/improvements/bugs can be in the form of Pull Requests, or creating an issue. 165 | 166 | Credits to [webpack-server-side-example](https://github.com/webpack/react-webpack-server-side-example), [example-app](https://github.com/webpack/example-app), [flux-examples](https://github.com/facebook/flux/tree/master/examples), [node-express-mongo-demo](https://github.com/madhums/node-express-mongoose-demo), [hackathon-starter](https://github.com/sahat/hackathon-starter/), [web-starter-kit](https://github.com/google/web-starter-kit), [awesome material-ui](https://github.com/callemall/material-ui), [alt and iso](https://github.com/goatslacker/iso/tree/master/examples/react-router-flux), and [React+Webpack+Node](https://github.com/choonkending/react-webpack-node) 167 | 168 | License 169 | =============== 170 | MIT 171 | --------------------------------------------------------------------------------