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