├── .gitignore
├── .jshintrc
├── .nvmrc
├── LICENSE
├── README.md
├── app
├── controllers
│ ├── index.js
│ ├── strategies
│ │ ├── facebook.js
│ │ ├── google.js
│ │ ├── local.js
│ │ └── twitter.js
│ └── users.js
├── models
│ └── user.js
├── routes.js
├── utils
│ └── strategy_common.js
└── views
│ └── index.ejs
├── client
├── assets
│ ├── crossdomain.xml
│ ├── humans.txt
│ └── robots.txt
├── html
│ ├── 404.html
│ ├── index.html
│ └── partials
│ │ ├── _google_analytics.html
│ │ └── _header.html
├── js
│ ├── actions
│ │ ├── api.js
│ │ ├── settings.js
│ │ └── user.js
│ ├── app.jsx
│ ├── components
│ │ ├── common
│ │ │ ├── message.jsx
│ │ │ └── messages.jsx
│ │ ├── index.jsx
│ │ ├── layout
│ │ │ └── left_nav.jsx
│ │ ├── main
│ │ │ ├── about.jsx
│ │ │ ├── dashboard.jsx
│ │ │ └── home.jsx
│ │ ├── mixins
│ │ │ └── store_keeper.js
│ │ ├── not_found.jsx
│ │ ├── sessions
│ │ │ ├── login.jsx
│ │ │ └── logout.jsx
│ │ └── users
│ │ │ ├── connect.jsx
│ │ │ ├── connections.jsx
│ │ │ └── register.jsx
│ ├── constants.js
│ ├── dispatcher.js
│ ├── routes.jsx
│ ├── stores
│ │ ├── messages.js
│ │ ├── settings.js
│ │ ├── store_common.js
│ │ └── user.js
│ └── utils
│ │ └── query_string.js
└── styles
│ ├── .csscomb.json
│ ├── .csslintrc
│ ├── _common.scss
│ ├── _custom.less
│ ├── _defines.scss
│ ├── font-icons
│ ├── Read Me.txt
│ ├── demo-files
│ │ ├── demo.css
│ │ └── demo.js
│ ├── demo.html
│ ├── fonts
│ │ ├── icomoon.eot
│ │ ├── icomoon.svg
│ │ ├── icomoon.ttf
│ │ └── icomoon.woff
│ ├── selection.json
│ └── style.css
│ ├── layout
│ ├── _footer.less
│ └── _full_width.less
│ ├── styles.js
│ ├── styles.less
│ └── styles.scss
├── config
├── passport.js
├── secrets.example.js
├── settings.js
└── webpack.config.js
├── gulpfile.js
├── karma.conf.js
├── package.json
├── s3_website.example.yml
├── server.js
├── specs
├── components
│ ├── index.spec.js
│ ├── main
│ │ └── about.spec.js
│ ├── not_found.spec.js
│ ├── sessions
│ │ └── login.spec.js
│ └── users
│ │ └── register.spec.js
├── routes.spec.js
├── spec_helper.js
├── stores
│ ├── messages.spec.js
│ └── user.spec.js
├── support
│ ├── stub_router_context.js
│ └── utils.js
└── tests.webpack.js
└── webpack_server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Git uses this file to determine which files and directories to ignore
2 | # https://help.github.com/articles/ignoring-files
3 |
4 | build
5 | bower_components
6 | node_modules
7 | npm-debug.log
8 | s3_website.yml
9 | coverage
10 |
11 | config/secrets.js
12 | .DS_Store
13 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globalstrict": true,
3 | "globals": {
4 | "require": false,
5 | "__dirname": false,
6 | "setTimeout": false,
7 | "DEFAULT_SETTINGS": false
8 | },
9 | "esnext" : true
10 | }
11 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 0.12
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Justin Ball
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NOTE this project is not being actively maintained and contains bugs. It can be used as a starting point, but it needs work. As an alternative check out the [React Client Starter App](https://github.com/atomicjolt/react_client_starter_app) which is the newer and maintained version.
2 |
3 |
4 | # React.s Kindling
5 |
6 | This is a React.js starter project. You can use it to build a client only application or a full stack node.js application.
7 |
8 | ## What's included
9 | - [Gulp](http://gulpjs.com/) - For building assets, launching development servers and deploying
10 | - [Webpack](http://webpack.github.io/) - For bundling javascript and other assets as required
11 | - [React Hot Loader](https://github.com/gaearon/react-hot-loader) - For quick reloads of css and js changes
12 | - [Express](http://expressjs.com/) - For middleware and serving pages
13 | - [Passport.js](http://passportjs.org/) - For authentication
14 | - [React.js](http://facebook.github.io/react/) - For sheer awesomeness
15 | - [React Router](https://github.com/rackt/react-router) - For managing the routes of the sheer awesomeness
16 | - [Mongoose](http://mongoosejs.com/) - For talking to MongoDB
17 | - [MaterialUi](http://callemall.github.io/material-ui/#/) - Because not everything has to be Bootstrap
18 |
19 |
20 | ## Usage
21 |
22 | __Clone this repository:__
23 |
24 | `git clone git@github.com:jbasdf/react-kindling.git`
25 |
26 | __Install the dependencies:__
27 |
28 | `npm install`
29 |
30 | __Configuration:__
31 |
32 | Change `config/secrets.example.js` to `config/secrets.js`. Change `sessionSecret` and set your database. If you want
33 | to use Facebook, Twitter or Google for authentication setup your keys and secrets.
34 |
35 | __Development mode with livereload:__
36 |
37 | To start node and the webpack server just type:
38 | `gulp`
39 |
40 | Or start each one individually
41 | node server.js
42 | gulp serve:hot
43 |
44 |
45 | ## Development
46 | You can use React-Kindling to build a client only application or a client-server application.
47 |
48 | ### Client Only
49 | Open up settings. Change 'projectType' to 'client'. When you run 'gulp' all of the assets in the 'client' directory
50 | will be built into a client only html/js/css application. The development files will be found in /build and the
51 | production files will be in /public.
52 |
53 | ### Client Server
54 | Open up settings.
55 |
56 | Change 'projectType' to 'client-server'.
57 | Set applicationUrl to your domain.
58 |
59 | Pass values from the server to the client via `DEFAULT_SETTINGS`. This code can be found in index.ejs.
60 |
61 | #### ngrok
62 | Ngrok makes it easy to provide a public url to an application running on your local machine. This
63 | comes in handy when dealing with OAuth providers that don't permit localhost. Install ngrok - https://ngrok.com/
64 | and then run two instances one for the node server and another for the webpack server:
65 |
66 | `ngrok --subdomain myserversubdomain 8888`
67 | `ngrok --subdomain myassetssubdomain 8080`
68 |
69 | Change the subdomains to be a value you prefer and then update applicationUrl and assetsUrl
70 | in secrets.js to match your changes.
71 |
72 |
73 | ## Testing
74 | React Kindling uses [Jest](https://facebook.github.io/jest/) for tests
75 |
76 | Run `npm test`
77 |
78 | ## Database
79 |
80 | ### Migrations
81 | React Kindling uses MongoDB. PostGreSQL support is coming. Here are some resources:
82 |
83 | - http://www.quora.com/What-are-my-options-for-SQL-database-migrations-with-Node-js
84 | - https://github.com/kunklejr/node-db-migrate
85 | - https://github.com/thuss/standalone-migrations
86 | - https://github.com/rosenfeld/active_record_migrations
87 |
88 | ## Setup
89 |
90 | Sensitive values live in:
91 | config/secrets.js
92 |
93 | Build configuration lives in:
94 | config/settings.js
95 |
96 | ## Deployment
97 |
98 |
99 | ### Client Only
100 |
101 | React.js Kindling can be used to build a client only application that can be deployed to Amazon S3 or Github pages or
102 | any other static hosting service
103 |
104 | __Create a production ready version of the JS bundle:__
105 |
106 | `gulp build --release`
107 |
108 | Then upload the contents of the 'public' folder to your favorite service.
109 |
110 | __Deploy to Amazon__
111 | React.js Kindling uses the Ruby gem [s3_website](https://github.com/laurilehmijoki/s3_website) to deploy.
112 | run gulp deploy:amazon
113 |
114 |
115 | __Deploy to Github Page__
116 | run gulp deploy:github
117 |
118 | ### Client Server
119 |
120 |
121 |
122 |
123 |
124 | License and attribution
125 | -----------------------
126 | MIT
127 |
--------------------------------------------------------------------------------
/app/controllers/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var React = require('react');
4 | var Router = require('react-router');
5 | var UAParser = require('ua-parser-js');
6 |
7 | var RouteHandler = Router.RouteHandler;
8 |
9 | var settings = require('../../config/settings.js');
10 | var routes = require('../../client/js/routes.jsx');
11 |
12 | function deviceType(req){
13 | // In order to handle "media queries" server-side (preventing FOUT), we parse the user agent string,
14 | // and pass a string down through the router that lets components style and render themselves
15 | // For the correct viewport. Client.js uses window width, which resolves any problems with
16 | // browser sniffing.
17 | var parser = new UAParser();
18 | var ua = parser.setUA(req.headers['user-agent']).getResult();
19 | if (ua.device.type === undefined) {
20 | return "desktop";
21 | } else {
22 | return ua.device.type;
23 | }
24 | }
25 |
26 | module.exports = function(app){
27 |
28 | return {
29 |
30 | index: function(req, res){
31 | var content = "";
32 |
33 | // Customize the onAbort method in order to handle redirects
34 | var router = Router.create({
35 | routes: routes,
36 | location: req.path,
37 | onAbort: function defaultAbortHandler(abortReason, location) {
38 | if (abortReason && abortReason.to) {
39 | res.redirect(301, abortReason.to);
40 | } else {
41 | res.redirect(404, "404");
42 | }
43 | }
44 | });
45 |
46 | // Run the router, and render the result to string
47 | router.run(function(Handler, state){
48 | content = React.renderToString(React.createElement(Handler, {
49 | routerState: state,
50 | deviceType: deviceType,
51 | environment: "server"
52 | }), null);
53 | });
54 |
55 | var scriptPath,
56 | cssPath,
57 | apiPath;
58 |
59 | if(process.env.NODE_ENV === "production"){
60 | scriptPath = '/app.js';
61 | cssPath = '/styles.css';
62 | apiPath = '/';
63 | } else {
64 | scriptPath = settings.devAssetsUrl + settings.devRelativeOutput + 'app.js';
65 | cssPath = settings.devAssetsUrl + settings.devRelativeOutput + 'styles.css';
66 | apiPath = settings.devApplicationUrl + '/';
67 | }
68 |
69 | var displayName = "";
70 | var email = "";
71 | var loggedIn = false;
72 | if(req.user){
73 | displayName = req.user.displayName();
74 | loggedIn = true;
75 | }
76 |
77 | res.render('index.ejs', {
78 | displayName: displayName,
79 | email: email,
80 | loggedIn: loggedIn,
81 | content: content,
82 | scriptPath: scriptPath,
83 | cssPath: cssPath,
84 | apiPath: apiPath
85 | });
86 | }
87 |
88 | };
89 |
90 | };
91 |
--------------------------------------------------------------------------------
/app/controllers/strategies/facebook.js:
--------------------------------------------------------------------------------
1 | var StrategyCommon = require("../../utils/strategy_common");
2 | var User = require("../../models/user");
3 |
4 | module.exports = function(app, passport){
5 | return {
6 |
7 | // Send the user to Facebook to be authenticated
8 | start: function(req, res, next){
9 | passport.authenticate('facebook', { scope: 'email' })(req, res);
10 | },
11 |
12 | // Handle the callback after Facebook has authenticated the user
13 | callback: function(req, res, next){
14 | StrategyCommon.finishAuth('facebook', passport, req, res, next);
15 | },
16 |
17 | unlink: function(req, res){
18 | var user = req.user;
19 | user.facebook.token = undefined;
20 | user.save(function(err){
21 | res.json({"user": {}});
22 | });
23 | },
24 |
25 | // Called by passport when the call to Facebook returns
26 | strategyCallback: function(req, token, refreshToken, profile, done){
27 | // asynchronous
28 | process.nextTick(function(){
29 | // check if the user is already logged in
30 | if(!req.user){
31 | User.findOne({
32 | 'facebook.id': profile.id
33 | }, function(err, user){
34 | if(err){ return done(err); }
35 | if(user){
36 | if(!user.facebook.token){
37 | // There is a user id already but no token (user was linked at one point and then removed)
38 | user.facebook.token = token;
39 | user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
40 | user.facebook.email = (profile.emails[0].value || '').toLowerCase();
41 | user.save(function(err){
42 | if(err){
43 | return done(err);
44 | }
45 | return done(null, user);
46 | });
47 | }
48 | return done(null, user); // user found, return that user
49 | } else {
50 | // if there is no user, create them
51 | var user = new User();
52 | user.facebook.id = profile.id;
53 | user.facebook.token = token;
54 | user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
55 | user.facebook.email = (profile.emails[0].value || '').toLowerCase();
56 | user.save(function(err){
57 | if(err){
58 | return done(err);
59 | }
60 | return done(null, user);
61 | });
62 | }
63 | });
64 | } else {
65 | // user already exists and is logged in, we have to link accounts
66 | var user = req.user; // pull the user out of the session
67 | user.facebook.id = profile.id;
68 | user.facebook.token = token;
69 | user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
70 | user.facebook.email = (profile.emails[0].value || '').toLowerCase();
71 | user.save(function(err){
72 | if(err){
73 | return done(err);
74 | }
75 | return done(null, user);
76 | });
77 | }
78 | });
79 | }
80 |
81 | };
82 | }
83 |
--------------------------------------------------------------------------------
/app/controllers/strategies/google.js:
--------------------------------------------------------------------------------
1 | var StrategyCommon = require("../../utils/strategy_common");
2 | var User = require("../../models/user");
3 |
4 | module.exports = function(app, passport) {
5 | return {
6 |
7 | // Send the user to Google to be authenticated
8 | start: function(req, res) {
9 | passport.authenticate('google', { scope: ['profile', 'email'] })(req, res);
10 | },
11 |
12 | // Handle the callback after Google has authenticated the user
13 | callback: function(req, res, next) {
14 | StrategyCommon.finishAuth('local-signup', passport, req, res, next);
15 | },
16 |
17 | unlink: function(req, res) {
18 | var user = req.user;
19 | user.google.token = undefined;
20 | user.save(function(err) {
21 | res.json({"user": {}});
22 | });
23 | },
24 |
25 | // Called by passport when the call to Google returns
26 | strategyCallback: function(req, email, password, done) {
27 | // asynchronous
28 | process.nextTick(function() {
29 | // check if the user is already logged in
30 | if(!req.user) {
31 | User.findOne({
32 | 'google.id': profile.id
33 | }, function(err, user) {
34 | if(err) return done(err);
35 | if(user) {
36 | // if there is a user id already but no token (user was linked at one point and then removed)
37 | if(!user.google.token) {
38 | user.google.token = token;
39 | user.google.name = profile.displayName;
40 | user.google.email = (profile.emails[0].value || '').toLowerCase(); // pull the first email
41 | user.save(function(err) {
42 | if(err) return done(err);
43 | return done(null, user);
44 | });
45 | }
46 | return done(null, user);
47 | } else {
48 | var newUser = new User();
49 | newUser.google.id = profile.id;
50 | newUser.google.token = token;
51 | newUser.google.name = profile.displayName;
52 | newUser.google.email = (profile.emails[0].value || '').toLowerCase(); // pull the first email
53 | newUser.save(function(err) {
54 | if(err) return done(err);
55 | return done(null, newUser);
56 | });
57 | }
58 | });
59 | } else {
60 | // user already exists and is logged in, we have to link accounts
61 | var user = req.user; // pull the user out of the session
62 | user.google.id = profile.id;
63 | user.google.token = token;
64 | user.google.name = profile.displayName;
65 | user.google.email = (profile.emails[0].value || '').toLowerCase(); // pull the first email
66 | user.save(function(err) {
67 | if(err) return done(err);
68 | return done(null, user);
69 | });
70 | }
71 | });
72 | }
73 |
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/app/controllers/strategies/local.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var StrategyCommon = require("../../utils/strategy_common");
3 | var User = require("../../models/user");
4 |
5 | module.exports = function(app, passport){
6 |
7 | return {
8 |
9 | create: function(req, res, next){
10 | StrategyCommon.finishAuth('local-login', passport, req, res, next);
11 | },
12 |
13 | destroy: function(req, res){
14 | req.logout();
15 | res.json({"user": {}});
16 | },
17 |
18 | unlink: function(req, res){
19 | var user = req.user;
20 | user.email = undefined;
21 | user.password = undefined;
22 | user.save(function(err) {
23 | res.json({"user": {}});
24 | });
25 | },
26 |
27 | // Called by passport when a user logs in
28 | strategyCallback: function(req, email, password, done){
29 | if(email){
30 | email = email.toLowerCase(); // Use lower-case e-mails to avoid case-sensitive e-mail matching
31 | }
32 | process.nextTick(function() {
33 | User.findOne({ 'email' : email }, function(err, user) {
34 | if(err){ return done(err); }
35 |
36 | if(!user){
37 | return done(null, false, { 'message' : 'No user found.' });
38 | }
39 |
40 | if(!user.validPassword(password)){
41 | return done(null, false, { 'message' : 'Wrong password.' } );
42 | } else {
43 | // user logged in successfully
44 | return done(null, user);
45 | }
46 |
47 | });
48 | });
49 | }
50 |
51 | };
52 |
53 | }
--------------------------------------------------------------------------------
/app/controllers/strategies/twitter.js:
--------------------------------------------------------------------------------
1 | var StrategyCommon = require("../../utils/strategy_common");
2 | var User = require("../../models/user");
3 |
4 | module.exports = function (app, passport){
5 | return {
6 |
7 | // Send the user to Twitter to be authenticated
8 | start: function (req, res, next){
9 | passport.authenticate('twitter', { scope: 'email' })(req, res, next);
10 | },
11 |
12 | // Handle the callback after Twitter has authenticated the user
13 | callback: function (req, res, next){
14 | StrategyCommon.finishAuth('twitter', passport, req, res, next);
15 | },
16 |
17 | unlink: function (req, res){
18 | var user = req.user;
19 | user.twitter.token = undefined;
20 | user.save(function (err){
21 | res.json({"user": {}});
22 | });
23 | },
24 |
25 | // Called by passport when the call to Twitter returns
26 | strategyCallback: function (req, token, tokenSecret, profile, done){
27 | // asynchronous
28 | process.nextTick(function (){
29 | // check if the user is already logged in
30 | if (!req.user){
31 | User.findOne({
32 | 'twitter.id': profile.id
33 | }, function (err, user){
34 | if (err) return done(err);
35 | if (user){
36 | // if there is a user id already but no token (user was linked at one point and then removed)
37 | if (!user.twitter.token){
38 | user.twitter.token = token;
39 | user.twitter.username = profile.username;
40 | user.twitter.displayName = profile.displayName;
41 | user.save(function (err){
42 | if (err) return done(err);
43 | return done(null, user);
44 | });
45 | }
46 | return done(null, user); // user found, return that user
47 | } else {
48 | // if there is no user, create them
49 | var newUser = new User();
50 | newUser.twitter.id = profile.id;
51 | newUser.twitter.token = token;
52 | newUser.twitter.username = profile.username;
53 | newUser.twitter.displayName = profile.displayName;
54 | newUser.save(function (err){
55 | if (err) return done(err);
56 | return done(null, newUser);
57 | });
58 | }
59 | });
60 | } else {
61 | // user already exists and is logged in, we have to link accounts
62 | var user = req.user; // pull the user out of the session
63 | user.twitter.id = profile.id;
64 | user.twitter.token = token;
65 | user.twitter.username = profile.username;
66 | user.twitter.displayName = profile.displayName;
67 | user.save(function (err){
68 | if (err) return done(err);
69 | return done(null, user);
70 | });
71 | }
72 | });
73 | }
74 |
75 | };
76 | }
77 |
--------------------------------------------------------------------------------
/app/controllers/users.js:
--------------------------------------------------------------------------------
1 | var User = require("../models/user");
2 | var StrategyCommon = require("../utils/strategy_common");
3 |
4 | module.exports = function(app, passport){
5 |
6 | return {
7 |
8 | create: function(req, res, next){
9 | StrategyCommon.finishAuth('local-signup', passport, req, res, next);
10 | },
11 |
12 | // Called by passport when a user signs up
13 | strategyCallback: function(req, email, password, done){
14 | if(email){
15 | email = email.toLowerCase(); // Use lower-case e-mails to avoid case-sensitive e-mail matching
16 | }
17 |
18 | // asynchronous
19 | process.nextTick(function(){
20 | // if the user is not already logged in:
21 | if(!req.user){
22 |
23 | User.find({ 'email' : email }, function(err, user){
24 | if(err){ return done(err); }
25 |
26 | // Check for existing email
27 | if(user){
28 | return done(null, false, { 'message' : 'That email is already taken.' });
29 | } else {
30 | // create the user
31 | var user = new User();
32 | user.email = email;
33 | user.password = user.generateHash(password);
34 | user.save(function(err){
35 | if(err){
36 | return done(err);
37 | }
38 | return done(null, user);
39 | });
40 | }
41 |
42 | });
43 | } else if(!req.user.email){
44 | // The user is logged in but has no local account.
45 | // Presumably they're trying to connect a local account
46 | // Check if the email used to connect a local account is being used by another user
47 | User.findOne({ 'email' : email }, function(err, user){
48 | if(err){ return done(err); }
49 |
50 | if(user){
51 | return done(null, false, { 'loginMessage' : 'That email is already taken.' });
52 | // Using 'loginMessage instead of signupMessage because it's used by /connect/local'
53 | } else {
54 | var user = req.user;
55 | user.email = email;
56 | user.password = user.generateHash(password);
57 | user.save(function (err){
58 | if(err){ return done(err); }
59 | return done(null,user);
60 | });
61 | }
62 | });
63 | } else {
64 | // User is logged in and already has a local account. Ignore signup.
65 | return done(null, req.user);
66 | }
67 |
68 | });
69 | }
70 |
71 | };
72 |
73 | }
--------------------------------------------------------------------------------
/app/models/user.js:
--------------------------------------------------------------------------------
1 | // load the things we need
2 | var mongoose = require('mongoose');
3 | var bcrypt = require('bcrypt-nodejs');
4 |
5 | // define the schema for our user model
6 | var userSchema = mongoose.Schema({
7 |
8 | email: String,
9 | password: String,
10 | name: String,
11 |
12 | facebook: {
13 | id: String,
14 | token: String,
15 | email: String,
16 | name: String
17 | },
18 | twitter: {
19 | id: String,
20 | token: String,
21 | displayName: String,
22 | username: String
23 | },
24 | google: {
25 | id: String,
26 | token: String,
27 | email: String,
28 | name: String
29 | }
30 |
31 | });
32 |
33 | // generating a hash
34 | userSchema.methods.generateHash = function(password) {
35 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
36 | };
37 |
38 | // checking if password is valid
39 | userSchema.methods.validPassword = function(password) {
40 | return bcrypt.compareSync(password, this.password);
41 | };
42 |
43 | userSchema.methods.displayName = function(){
44 | return this.name || this.facebook.name || this.twitter.displayName;
45 | };
46 |
47 | userSchema.methods.username = function(){
48 | return this.twitter.username;
49 | };
50 |
51 |
52 | // create the model for users and expose it to our app
53 | module.exports = mongoose.model('User', userSchema);
54 |
--------------------------------------------------------------------------------
/app/routes.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 |
4 | module.exports = function(app, controllers){
5 |
6 | // ======================================================================
7 | // Method to ensure user is logged in
8 | function isAuthenticated(req, res, next){
9 | if(req.isAuthenticated()){
10 | return next();
11 | }
12 | return res.status(422).json({"message" : "Not authorized"});
13 | }
14 |
15 | // ======================================================================
16 | //
17 | // Routes
18 | //
19 |
20 | // Home
21 | app.get('/', controllers.index.index);
22 |
23 | // Login
24 | app.post('/sessions', controllers.strategies.local.create);
25 | app.get('/logout', controllers.strategies.local.destroy);
26 | app.delete('/sessions', controllers.strategies.local.destroy);
27 |
28 | app.post('/connect/local', controllers.strategies.local.create);
29 | app.get('/unlink/local', isAuthenticated, controllers.strategies.local.unlink);
30 | app.delete('/unlink/local', isAuthenticated, controllers.strategies.local.unlink);
31 |
32 | // Sign up
33 | app.post('/users', controllers.users.create);
34 |
35 | // Facebook
36 | app.get('/auth/facebook', controllers.strategies.facebook.start);
37 | app.get('/auth/facebook/callback', controllers.strategies.facebook.callback);
38 | app.get('/connect/facebook', controllers.strategies.facebook.start);
39 | app.get('/connect/facebook/callback', controllers.strategies.facebook.callback);
40 | app.get('/unlink/facebook', isAuthenticated, controllers.strategies.facebook.unlink);
41 |
42 | // Twitter
43 | app.get('/auth/twitter', controllers.strategies.twitter.start);
44 | app.get('/auth/twitter/callback', controllers.strategies.twitter.callback);
45 | app.get('/connect/twitter', controllers.strategies.twitter.start);
46 | app.get('/connect/twitter/callback', controllers.strategies.twitter.callback);
47 | app.get('/unlink/twitter', isAuthenticated, controllers.strategies.twitter.unlink);
48 |
49 | // Google
50 | app.get('/auth/google', controllers.strategies.google.start);
51 | app.get('/auth/google/callback', controllers.strategies.google.callback);
52 | app.get('/connect/google', controllers.strategies.google.start);
53 | app.get('/connect/google/callback', controllers.strategies.google.callback);
54 | app.get('/unlink/google', isAuthenticated, controllers.strategies.google.unlink);
55 |
56 | };
--------------------------------------------------------------------------------
/app/utils/strategy_common.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | finishAuth: function(kind, passport, req, res, next){
4 | passport.authenticate(kind, function(err, user, info) {
5 |
6 | if(err){ return next(err); }
7 |
8 | if(!user){ // HTTP status 422: user create failed
9 | return res.status(422).json(info);
10 | }
11 |
12 | req.logIn(user, function(err) {
13 | if (err) { return next(err); }
14 | // Redirect to home page.
15 | res.redirect('/');
16 | });
17 |
18 | })(req, res, next);
19 | }
20 |
21 | };
--------------------------------------------------------------------------------
/app/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
21 | <%- content %>
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/client/assets/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/client/assets/humans.txt:
--------------------------------------------------------------------------------
1 | # humanstxt.org/
2 | # The humans responsible & technology colophon
3 |
4 | # TEAM
5 |
6 | -- --
7 |
8 | # THANKS
9 |
10 |
11 |
12 | # TECHNOLOGY COLOPHON
13 |
14 | HTML5, CSS3, JavaScript
15 | React, jQuery, Bootstrap
16 |
--------------------------------------------------------------------------------
/client/assets/robots.txt:
--------------------------------------------------------------------------------
1 | # www.robotstxt.org/
2 |
3 | # Allow crawling of all content
4 | User-agent: *
5 | Disallow:
6 |
--------------------------------------------------------------------------------
/client/html/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Page Not Found
6 |
7 |
8 |
9 | @@include('./partials/_header.html')
10 | Page Not Found
11 | Sorry, but the page you were trying to view does not exist.
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | @@include('./partials/_header.html')
13 |
14 | @@include('./partials/_google_analytics.html')
15 |
16 |
17 |
--------------------------------------------------------------------------------
/client/html/partials/_google_analytics.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/client/html/partials/_header.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/js/actions/api.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import Request from "superagent";
4 | import User from "../stores/user";
5 | import Constants from "../constants";
6 | import Dispatcher from "../dispatcher";
7 | import SettingsStore from '../stores/settings';
8 |
9 | const TIMEOUT = 10000;
10 |
11 | var _pendingRequests = {};
12 |
13 | function abortPendingRequests(key) {
14 | if(_pendingRequests[key]) {
15 | _pendingRequests[key]._callback = function() {};
16 | _pendingRequests[key].abort();
17 | _pendingRequests[key] = null;
18 | }
19 | }
20 |
21 | // Get the access token from the user
22 | function token() {
23 | return User.token();
24 | }
25 |
26 | function makeUrl(part){
27 | if(part.indexOf("http") >= 0){
28 | return part;
29 | } else {
30 | return SettingsStore.current().apiUrl + part;
31 | }
32 | }
33 |
34 | // GET request with a token param
35 | function get(url) {
36 | return Request
37 | .get(url)
38 | .timeout(TIMEOUT)
39 | .set('Accept', 'application/json')
40 | .query({
41 | authtoken: token()
42 | });
43 | }
44 |
45 | // POST request with a token param
46 | function post(url, body) {
47 | return Request
48 | .post(url)
49 | .send(body)
50 | .set('Accept', 'application/json')
51 | .timeout(TIMEOUT)
52 | .query({
53 | authtoken: token()
54 | });
55 | }
56 |
57 | // PUT request with a token param
58 | function put(url, body) {
59 | return Request
60 | .put(url)
61 | .send(body)
62 | .set('Accept', 'application/json')
63 | .timeout(TIMEOUT)
64 | .query({
65 | authtoken: token()
66 | });
67 | }
68 |
69 | // DELETER request with a token param
70 | function del(url) {
71 | return Request
72 | .del(url)
73 | .set('Accept', 'application/json')
74 | .timeout(TIMEOUT)
75 | .query({
76 | authtoken: token()
77 | });
78 | }
79 |
80 | function dispatch(key, response) {
81 | Dispatcher.dispatch({
82 | action: key,
83 | data: response
84 | });
85 | }
86 |
87 | // Dispatch a response based on the server response
88 | function dispatchResponse(key) {
89 | return function(err, response) {
90 | if(err && err.timeout === TIMEOUT) {
91 | dispatch(Constants.TIMEOUT, response);
92 | } else if(response.status === 400) {
93 | dispatch(Constants.NOT_AUTHORIZED, response);
94 | } else if(!response.ok) {
95 | dispatch(Constants.ERROR, response);
96 | } else {
97 | dispatch(key, response);
98 | }
99 | };
100 | }
101 |
102 | function doRequest(key, url, callback){
103 | abortPendingRequests(key);
104 | var request = _pendingRequests[key] = callback(makeUrl(url));
105 | request.end(dispatchResponse(key));
106 | return request;
107 | }
108 |
109 | export default {
110 |
111 | get(key, url){
112 | return doRequest(key, url, function(fullUrl){
113 | return get(fullUrl);
114 | });
115 | },
116 |
117 | post(key, url, body){
118 | return doRequest(key, url, function(fullUrl){
119 | return post(fullUrl, body);
120 | });
121 | },
122 |
123 | put(key, url, body){
124 | return doRequest(key, url, function(fullUrl){
125 | return put(fullUrl, body);
126 | });
127 | },
128 |
129 | del(key, url){
130 | return doRequest(key, url, function(fullUrl){
131 | return del(fullUrl);
132 | });
133 | }
134 |
135 | };
136 |
--------------------------------------------------------------------------------
/client/js/actions/settings.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import Constants from "../constants";
4 | import Dispatcher from "../dispatcher";
5 |
6 | export default {
7 |
8 | load(defaultSettings){
9 | Dispatcher.dispatch({ action: Constants.SETTINGS_LOAD, data: defaultSettings });
10 | }
11 |
12 | };
--------------------------------------------------------------------------------
/client/js/actions/user.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import Constants from "../constants";
4 | import Api from "./api";
5 | import Dispatcher from "../dispatcher";
6 |
7 | export default {
8 |
9 | login(payload){
10 | Dispatcher.dispatch({ action: Constants.LOGIN_PENDING });
11 | Api.post(Constants.LOGIN, "sessions/", payload);
12 | },
13 |
14 | register(payload) {
15 | Dispatcher.dispatch({ action: Constants.REGISTER_PENDING });
16 | Api.post(Constants.REGISTER, "users/", payload);
17 | }
18 |
19 | };
--------------------------------------------------------------------------------
/client/js/app.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import Router from 'react-router';
5 |
6 | import routes from './routes';
7 | import SettingsAction from './actions/settings';
8 |
9 | //Needed for onTouchTap
10 | //Can go away when react 1.0 release
11 | //Check this repo:
12 | //https://github.com/zilverline/react-tap-event-plugin
13 | import injectTapEventPlugin from "react-tap-event-plugin";
14 | injectTapEventPlugin();
15 |
16 |
17 | // Set a device type based on window width, so that we can write media queries in javascript
18 | // by calling if (this.props.deviceType === "mobile")
19 | var deviceType;
20 |
21 | if (window.matchMedia("(max-width: 639px)").matches){
22 | deviceType = "mobile";
23 | } else if (window.matchMedia("(max-width: 768px)").matches){
24 | deviceType = "tablet";
25 | } else {
26 | deviceType = "desktop";
27 | }
28 |
29 | // Initialize store singletons
30 | SettingsAction.load(window.DEFAULT_SETTINGS);
31 |
32 | Router.run(routes, (Handler) => {
33 | return React.render(, document.body);
34 | });
35 |
36 | // Router.run(routes, (Handler) => {
37 | // return React.render(, document.body);
38 | // });
39 |
40 | // Use the HTML5 history API for cleaner URLs:
41 | // Router.run(routes, Router.HistoryLocation, (Handler) => {
42 | // return React.render(, document.body);
43 | // });
44 |
--------------------------------------------------------------------------------
/client/js/components/common/message.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | export default React.createClass({
6 |
7 | render() {
8 | return (
9 |
10 | {this.props.children}
11 |
12 | );
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/client/js/components/common/messages.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import Messages from "../../stores/messages";
5 | import Message from "./message";
6 | import StoreKeeper from "../mixins/store_keeper";
7 | import { Toolbar } from "material-ui";
8 |
9 | export default React.createClass({
10 |
11 |
12 | getState(){
13 | return {
14 | messages: Messages.current(),
15 | hasMessages: Messages.hasMessages()
16 | };
17 | },
18 |
19 | getInitialState(){
20 | return this.getState();
21 | },
22 |
23 | // Method to update state based upon store changes
24 | storeChanged(){
25 | this.setState(this.getState());
26 | },
27 |
28 | // Listen for changes in the stores
29 | componentDidMount(){
30 | Messages.addChangeListener(this.storeChanged);
31 | },
32 |
33 | // Remove change listers from stores
34 | componentWillUnmount(){
35 | Messages.removeChangeListener(this.storeChanged);
36 | },
37 |
38 | render() {
39 |
40 | if(!this.state.hasMessages){
41 | return null;
42 | }
43 |
44 | var messages = this.state.messages.map(function(message){
45 | return {message};
46 | });
47 |
48 | return (
49 |
50 |
53 |
54 | );
55 | }
56 | });
57 |
--------------------------------------------------------------------------------
/client/js/components/index.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from "react";
4 | import Messages from "./common/messages";
5 | import LeftNav from "./layout/left_nav";
6 | import {RouteHandler} from "react-router";
7 | import { AppCanvas, AppBar, IconButton } from "material-ui";
8 |
9 | export default React.createClass({
10 |
11 | render(){
12 |
13 | var title = "React Kindling";
14 |
15 | var githubButton = (
16 |
21 | );
22 |
23 | return (
24 |
25 |
26 |
31 | {githubButton}
32 |
33 |
34 |
35 |
36 |
42 |
43 |
44 |
45 | Built by Atomic Jolt.
46 |
47 | {githubButton}
48 |
49 |
50 |
51 |
52 | );
53 | },
54 |
55 | _onMenuIconButtonTouchTap: function() {
56 | this.refs.leftNav.toggle();
57 | }
58 |
59 | });
60 |
--------------------------------------------------------------------------------
/client/js/components/layout/left_nav.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from "react";
4 | import User from "../../stores/user";
5 | import StoreKeeper from "../mixins/store_keeper";
6 | import Router from "react-router";
7 | import { LeftNav } from "material-ui";
8 |
9 | export default React.createClass({
10 |
11 | mixins: [StoreKeeper],
12 |
13 | contextTypes: {
14 | router: React.PropTypes.func
15 | },
16 |
17 | statics: {
18 | stores: [User], // Subscribe to changes in the messages store
19 | getState: () => { // Method to retrieve state from stores
20 |
21 | var loggedIn = User.loggedIn();
22 |
23 | var menuItems = [
24 | { route: 'home', text: 'Home' }
25 | ];
26 |
27 | if(loggedIn){
28 | menuItems.push({ route: 'logout', text: 'Logout' });
29 | menuItems.push({ route: 'dashboard', text: 'Dashboard' });
30 | menuItems.push({ route: 'connections', text: 'Connections' });
31 | } else {
32 | menuItems.push({ route: 'login', text: 'Sign In' });
33 | menuItems.push({ route: 'register', text: 'Sign Up' });
34 | }
35 |
36 | return {
37 | loggedIn: loggedIn,
38 | menuItems: menuItems
39 | };
40 |
41 | }
42 | },
43 |
44 | getInitialState: function() {
45 | return {
46 | selectedIndex: null
47 | };
48 | },
49 |
50 | render: function() {
51 |
52 |
53 | var header = Home
;
54 |
55 | return (
56 |
64 | );
65 | },
66 |
67 | toggle: function() {
68 | this.refs.leftNav.toggle();
69 | },
70 |
71 | _getSelectedIndex: function() {
72 | var currentItem;
73 |
74 | var router = this.context.router;
75 |
76 | for (var i = this.state.menuItems.length - 1; i >= 0; i--) {
77 | currentItem = this.state.menuItems[i];
78 | if (currentItem.route && router.isActive(currentItem.route)) return i;
79 | }
80 | },
81 |
82 | _onLeftNavChange: function(e, key, payload) {
83 | var router = this.context.router;
84 | router.transitionTo(payload.route);
85 | },
86 |
87 | _onHeaderClick: function() {
88 | var router = this.context.router;
89 | router.transitionTo('root');
90 | this.refs.leftNav.close();
91 | }
92 |
93 | });
94 |
--------------------------------------------------------------------------------
/client/js/components/main/about.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | export default React.createClass({
6 | render(){
7 | return About
;
8 | }
9 | });
--------------------------------------------------------------------------------
/client/js/components/main/dashboard.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from "react";
4 | import { Link } from "react-router";
5 |
6 | export default React.createClass({
7 | render(){
8 | return (
9 |
10 |
I am the Dashboard
11 | Logout
12 |
13 |
);
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/client/js/components/main/home.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | export default React.createClass({
6 | render(){
7 | return Home
;
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/client/js/components/mixins/store_keeper.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import _ from "lodash";
4 |
5 | export default {
6 |
7 | getInitialState(){
8 | return this.constructor.getState();
9 | },
10 |
11 | // Method to update state based upon store changes
12 | storeChanged(){
13 | this.setState(this.constructor.getState());
14 | },
15 |
16 | // Listen for changes in the stores
17 | componentDidMount(){
18 | _.each(this.constructor.stores, function(store){
19 | store.addChangeListener(this.storeChanged);
20 | }.bind(this));
21 | },
22 |
23 | // Remove change listers from stores
24 | componentWillUnmount(){
25 | _.each(this.constructor.stores, function(store){
26 | store.removeChangeListener(this.storeChanged);
27 | }.bind(this));
28 | }
29 |
30 | };
--------------------------------------------------------------------------------
/client/js/components/not_found.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | export default React.createClass({
6 | render(){
7 | return Not Found
;
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/client/js/components/sessions/login.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from "react";
4 | import { Link } from "react-router";
5 | import Validator from "validator";
6 | import UserActions from "../../actions/user";
7 | import _ from "lodash";
8 | import assign from "object-assign";
9 | import { Paper, TextField, FlatButton, RaisedButton, FontIcon } from "material-ui";
10 |
11 | export default React.createClass({
12 |
13 | getInitialState(){
14 | return {
15 | validations: {}
16 | };
17 | },
18 |
19 | handleLogin(e){
20 | e.preventDefault();
21 | if(this.validateAll()){
22 | UserActions.login({
23 | email: this.refs.email.getValue(),
24 | password: this.refs.password.getValue()
25 | });
26 | }
27 | },
28 |
29 | validateAll(){
30 | return _.every([
31 | this.validateEmail()
32 | ], (v)=> { return v; });
33 | },
34 |
35 | validate(isValid, invalidState, emptyState){
36 | if(!isValid){
37 | this.setState(assign(this.state.validations, invalidState));
38 | } else {
39 | this.setState(assign(this.state.validations, emptyState));
40 | }
41 | return isValid;
42 | },
43 |
44 | validateEmail(e){
45 | return this.validate(
46 | Validator.isEmail(this.refs.email.getValue()),
47 | { email: "Invalid email" },
48 | { email: "" }
49 | );
50 | },
51 |
52 | render: function(){
53 | return (
54 |
55 |
64 |
65 |
66 |
67 |
68 |
69 | Login with Facebook
70 |
71 |
72 |
73 |
74 |
75 |
76 | Login with Twitter
77 |
78 |
79 |
80 |
81 |
82 |
83 | Login with Google+
84 |
85 |
86 |
87 |
);
88 | }
89 | });
90 |
--------------------------------------------------------------------------------
/client/js/components/sessions/logout.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | export default React.createClass({
6 | render: function () {
7 | return Logout
;
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/client/js/components/users/connect.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import { Link } from 'react-router';
5 |
6 | export default React.createClass({
7 | render() {
8 | return ();
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/client/js/components/users/connections.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 |
5 | export default React.createClass({
6 | render() {
7 | return Connections
;
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/client/js/components/users/register.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import { Link } from 'react-router';
5 | import Validator from "validator";
6 | import UserActions from "../../actions/user";
7 | import _ from "lodash";
8 | import assign from "object-assign";
9 | import { Paper, TextField, FlatButton, RaisedButton, FontIcon } from "material-ui";
10 |
11 | export default React.createClass({
12 |
13 | getInitialState(){
14 | return {
15 | validations: {}
16 | };
17 | },
18 |
19 | validateEmail(e){
20 | return this.validate(
21 | Validator.isEmail(this.refs.email.getValue()),
22 | { email: "Invalid email" },
23 | { email: "" }
24 | );
25 | },
26 |
27 | validatePassword(e){
28 | return this.validate(
29 | Validator.isLength(this.refs.password.getValue(), 5),
30 | { password: "Password must be at least 5 characters" },
31 | { password: "" }
32 | );
33 | },
34 |
35 | validateConfirmPassword(){
36 | return this.validate(
37 | (this.refs.password.getValue() == this.refs.confirmPassword.getValue()),
38 | { confirmPassword: "Passwords do not match" },
39 | { confirmPassword: "" }
40 | );
41 | },
42 |
43 | validate(isValid, invalidState, emptyState){
44 | if(!isValid){
45 | this.setState(assign(this.state.validations, invalidState));
46 | } else {
47 | this.setState(assign(this.state.validations, emptyState));
48 | }
49 | return isValid;
50 | },
51 |
52 | validateAll(){
53 | return _.every([
54 | this.validateEmail(),
55 | this.validatePassword(),
56 | this.validateConfirmPassword()
57 | ], (v)=> { return v; });
58 | },
59 |
60 | handleRegister(e){
61 | e.preventDefault();
62 | if(this.validateAll()){
63 | UserActions.register({
64 | email: this.refs.email.getValue(),
65 | password: this.refs.password.getValue()
66 | });
67 | }
68 | },
69 |
70 | render(){
71 | return (
72 |
73 | Signup
74 |
80 |
81 | Already have an account? Login
82 |
83 |
84 |
);
85 | }
86 |
87 | });
88 |
--------------------------------------------------------------------------------
/client/js/constants.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | export default {
4 |
5 | // User
6 | LOGIN: "login",
7 | LOGIN_PENDING: "login_pending",
8 | REGISTER: "register",
9 | REGISTER_PENDING: "register_pending",
10 |
11 | // Errors
12 | TIMEOUT: "timeout",
13 | ERROR: "error",
14 | NOT_AUTHORIZED: "not_authorized",
15 |
16 | // settings
17 | SETTINGS_LOAD: "settings_load",
18 | USER_LOAD: "user_load"
19 |
20 | };
21 |
--------------------------------------------------------------------------------
/client/js/dispatcher.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import { Dispatcher } from 'flux';
4 |
5 | export default new Dispatcher();
6 |
--------------------------------------------------------------------------------
/client/js/routes.jsx:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import Router from 'react-router';
5 |
6 | import Index from './components/index';
7 | import Home from './components/main/home';
8 | import Login from './components/sessions/login';
9 | import Logout from './components/sessions/logout';
10 | import Register from './components/users/register';
11 | import Dashboard from './components/main/dashboard';
12 | import NotFound from './components/not_found';
13 | import Connections from './components/users/connections';
14 | import About from './components/main/about';
15 |
16 | var Route = Router.Route;
17 | var NotFoundRoute = Router.NotFoundRoute;
18 | var DefaultRoute = Router.DefaultRoute;
19 | var Redirect = Router.Redirect;
20 |
21 | var routes = (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 |
34 | module.exports = routes;
35 |
--------------------------------------------------------------------------------
/client/js/stores/messages.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import Dispatcher from "../dispatcher";
4 | import Constants from "../constants";
5 | import StoreCommon from "./store_common";
6 | import assign from "object-assign";
7 | import _ from "lodash";
8 |
9 | const MessageTimeout = 5000;
10 |
11 | var _messages = [];
12 | var messageCount = 0;
13 |
14 | // Extend Message Store with EventEmitter to add eventing capabilities
15 | var MessagesStore = assign({}, StoreCommon, {
16 |
17 | // Return current messages
18 | current(){
19 | return _messages;
20 | },
21 |
22 | hasMessages(){
23 | return _.keys(_messages).length > 0;
24 | }
25 |
26 | });
27 |
28 | // Register callback with Dispatcher
29 | Dispatcher.register(function(payload) {
30 |
31 | switch(payload.action){
32 |
33 | // Respond to TIMEOUT action
34 | case Constants.TIMEOUT:
35 | addMessage("Request timed out. Reponse was: " + payload.data);
36 | break;
37 |
38 | // Respond to NOT_AUTHORIZED action
39 | case Constants.NOT_AUTHORIZED:
40 | addServerMessage(payload.data);
41 | break;
42 |
43 | // Respond to ERROR action
44 | case Constants.ERROR:
45 | addServerMessage(payload.data);
46 | break;
47 |
48 | default:
49 | return true;
50 | }
51 |
52 | // If action was responded to, emit change event
53 | MessagesStore.emitChange();
54 |
55 | return true;
56 |
57 | });
58 |
59 |
60 | function addServerMessage(message){
61 | var messageId = addMessage(JSON.parse(message.text).message);
62 | setTimeout(function(){
63 | removeMessage(messageId);
64 | }, MessageTimeout);
65 | }
66 |
67 | function removeMessage(messageId){
68 | _messages = _.omit(_messages, messageId);
69 | MessagesStore.emitChange();
70 | }
71 |
72 | function addMessage(message){
73 | messageCount++;
74 | _messages[messageCount] = message;
75 | return messageCount;
76 | }
77 |
78 | export default MessagesStore;
79 |
80 |
--------------------------------------------------------------------------------
/client/js/stores/settings.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import Dispatcher from "../dispatcher";
4 | import Constants from "../constants";
5 | import StoreCommon from "./store_common";
6 | import assign from "object-assign";
7 | import QueryString from '../utils/query_string';
8 |
9 | var _settings = {};
10 |
11 | function loadSettings(defaultSettings){
12 |
13 | defaultSettings = defaultSettings || {};
14 |
15 | var bestValue = function(settings_prop, params_prop, default_prop){
16 | return defaultSettings[settings_prop] || QueryString.params()[params_prop] || default_prop;
17 | };
18 |
19 | _settings = {
20 | apiUrl : bestValue('apiUrl', 'api_url', '/'),
21 | loggedIn : defaultSettings.loggedIn
22 | };
23 |
24 | }
25 |
26 |
27 | // Extend Message Store with EventEmitter to add eventing capabilities
28 | var SettingsStore = assign({}, StoreCommon, {
29 |
30 | // Return current messages
31 | current(){
32 | return _settings;
33 | }
34 |
35 | });
36 |
37 | // Register callback with Dispatcher
38 | Dispatcher.register(function(payload) {
39 |
40 | switch(payload.action){
41 |
42 | // Respond to TIMEOUT action
43 | case Constants.SETTINGS_LOAD:
44 | loadSettings(payload.data);
45 | break;
46 |
47 | default:
48 | return true;
49 | }
50 |
51 | // If action was responded to, emit change event
52 | SettingsStore.emitChange();
53 |
54 | return true;
55 |
56 | });
57 |
58 | export default SettingsStore;
59 |
60 |
--------------------------------------------------------------------------------
/client/js/stores/store_common.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import assign from "object-assign";
4 | import EventEmitter from "events";
5 |
6 | const CHANGE_EVENT = 'change';
7 |
8 | export default assign({}, EventEmitter.prototype, {
9 |
10 | // Emit Change event
11 | emitChange(){
12 | this.emit(CHANGE_EVENT);
13 | },
14 |
15 | // Add change listener
16 | addChangeListener(callback){
17 | this.on(CHANGE_EVENT, callback);
18 | },
19 |
20 | // Remove change listener
21 | removeChangeListener(callback){
22 | this.removeListener(CHANGE_EVENT, callback);
23 | }
24 |
25 | });
--------------------------------------------------------------------------------
/client/js/stores/user.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import Dispatcher from "../dispatcher";
4 | import Constants from "../constants";
5 | import StoreCommon from "./store_common";
6 | import assign from "object-assign";
7 | import SettingsStore from '../stores/settings';
8 |
9 | var _user = {};
10 |
11 | // log the user in
12 | function login(email, displayName){
13 | _user.email = email;
14 | _user.loggedIn = true;
15 | _user.displayName = displayName;
16 | }
17 |
18 | // Register
19 | function register(user){
20 | _user.email = user.email;
21 | _user.loggedIn = true;
22 | _user.displayName = user.displayName;
23 | }
24 |
25 | function loadUserFromSettings(payload) {
26 | _user.email = payload.data.email;
27 | _user.loggedIn = payload.data.loggedIn;
28 | _user.displayName = payload.data.displayName;
29 | }
30 |
31 | // Extend User Store with EventEmitter to add eventing capabilities
32 | var UserStore = assign({}, StoreCommon, {
33 |
34 | // Return current user
35 | current(){
36 | return _user;
37 | },
38 |
39 | loggedIn(){
40 | return _user.loggedIn;
41 | }
42 |
43 | });
44 |
45 | // Register callback with Dispatcher
46 | Dispatcher.register(function(payload) {
47 | var action = payload.action;
48 |
49 | switch(action){
50 |
51 | // Respond to LOGIN action
52 | case Constants.LOGIN:
53 | login(payload.email, payload.displayName);
54 | break;
55 |
56 | // Respond to REGISTER action
57 | case Constants.REGISTER:
58 | register(payload.user);
59 | break;
60 |
61 | case Constants.SETTINGS_LOAD:
62 | loadUserFromSettings(payload);
63 | break;
64 |
65 | default:
66 | return true;
67 | }
68 |
69 | // If action was responded to, emit change event
70 | UserStore.emitChange();
71 |
72 | return true;
73 |
74 | });
75 |
76 | export default UserStore;
77 |
78 |
--------------------------------------------------------------------------------
/client/js/utils/query_string.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | export default {
4 |
5 | params(){
6 | var queryDict = {};
7 | var vars = window.location.search.substring(1).split('&');
8 | for (var i = 0; i < vars.length; i++){
9 | var pair = vars[i].split('=');
10 | queryDict[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
11 | }
12 | return queryDict;
13 | }
14 |
15 | };
16 |
--------------------------------------------------------------------------------
/client/styles/.csscomb.json:
--------------------------------------------------------------------------------
1 | {
2 | "always-semicolon": true,
3 | "block-indent": 2,
4 | "colon-space": [0, 1],
5 | "color-case": "lower",
6 | "color-shorthand": true,
7 | "combinator-space": true,
8 | "element-case": "lower",
9 | "eof-newline": true,
10 | "leading-zero": false,
11 | "remove-empty-rulesets": true,
12 | "rule-indent": 2,
13 | "stick-brace": " ",
14 | "strip-spaces": true,
15 | "unitless-zero": true,
16 | "vendor-prefix-align": true,
17 | "sort-order": [
18 | [
19 | "position",
20 | "top",
21 | "right",
22 | "bottom",
23 | "left",
24 | "z-index",
25 | "display",
26 | "float",
27 | "width",
28 | "min-width",
29 | "max-width",
30 | "height",
31 | "min-height",
32 | "max-height",
33 | "-webkit-box-sizing",
34 | "-moz-box-sizing",
35 | "box-sizing",
36 | "-webkit-appearance",
37 | "padding",
38 | "padding-top",
39 | "padding-right",
40 | "padding-bottom",
41 | "padding-left",
42 | "margin",
43 | "margin-top",
44 | "margin-right",
45 | "margin-bottom",
46 | "margin-left",
47 | "overflow",
48 | "overflow-x",
49 | "overflow-y",
50 | "-webkit-overflow-scrolling",
51 | "-ms-overflow-x",
52 | "-ms-overflow-y",
53 | "-ms-overflow-style",
54 | "clip",
55 | "clear",
56 | "font",
57 | "font-family",
58 | "font-size",
59 | "font-style",
60 | "font-weight",
61 | "font-variant",
62 | "font-size-adjust",
63 | "font-stretch",
64 | "font-effect",
65 | "font-emphasize",
66 | "font-emphasize-position",
67 | "font-emphasize-style",
68 | "font-smooth",
69 | "-webkit-hyphens",
70 | "-moz-hyphens",
71 | "hyphens",
72 | "line-height",
73 | "color",
74 | "text-align",
75 | "-webkit-text-align-last",
76 | "-moz-text-align-last",
77 | "-ms-text-align-last",
78 | "text-align-last",
79 | "text-emphasis",
80 | "text-emphasis-color",
81 | "text-emphasis-style",
82 | "text-emphasis-position",
83 | "text-decoration",
84 | "text-indent",
85 | "text-justify",
86 | "text-outline",
87 | "-ms-text-overflow",
88 | "text-overflow",
89 | "text-overflow-ellipsis",
90 | "text-overflow-mode",
91 | "text-shadow",
92 | "text-transform",
93 | "text-wrap",
94 | "-webkit-text-size-adjust",
95 | "-ms-text-size-adjust",
96 | "letter-spacing",
97 | "-ms-word-break",
98 | "word-break",
99 | "word-spacing",
100 | "-ms-word-wrap",
101 | "word-wrap",
102 | "-moz-tab-size",
103 | "-o-tab-size",
104 | "tab-size",
105 | "white-space",
106 | "vertical-align",
107 | "list-style",
108 | "list-style-position",
109 | "list-style-type",
110 | "list-style-image",
111 | "pointer-events",
112 | "cursor",
113 | "visibility",
114 | "zoom",
115 | "flex-direction",
116 | "flex-order",
117 | "flex-pack",
118 | "flex-align",
119 | "table-layout",
120 | "empty-cells",
121 | "caption-side",
122 | "border-spacing",
123 | "border-collapse",
124 | "content",
125 | "quotes",
126 | "counter-reset",
127 | "counter-increment",
128 | "resize",
129 | "-webkit-user-select",
130 | "-moz-user-select",
131 | "-ms-user-select",
132 | "-o-user-select",
133 | "user-select",
134 | "nav-index",
135 | "nav-up",
136 | "nav-right",
137 | "nav-down",
138 | "nav-left",
139 | "background",
140 | "background-color",
141 | "background-image",
142 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
143 | "filter:progid:DXImageTransform.Microsoft.gradient",
144 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
145 | "filter",
146 | "background-repeat",
147 | "background-attachment",
148 | "background-position",
149 | "background-position-x",
150 | "background-position-y",
151 | "-webkit-background-clip",
152 | "-moz-background-clip",
153 | "background-clip",
154 | "background-origin",
155 | "-webkit-background-size",
156 | "-moz-background-size",
157 | "-o-background-size",
158 | "background-size",
159 | "border",
160 | "border-color",
161 | "border-style",
162 | "border-width",
163 | "border-top",
164 | "border-top-color",
165 | "border-top-style",
166 | "border-top-width",
167 | "border-right",
168 | "border-right-color",
169 | "border-right-style",
170 | "border-right-width",
171 | "border-bottom",
172 | "border-bottom-color",
173 | "border-bottom-style",
174 | "border-bottom-width",
175 | "border-left",
176 | "border-left-color",
177 | "border-left-style",
178 | "border-left-width",
179 | "border-radius",
180 | "border-top-left-radius",
181 | "border-top-right-radius",
182 | "border-bottom-right-radius",
183 | "border-bottom-left-radius",
184 | "-webkit-border-image",
185 | "-moz-border-image",
186 | "-o-border-image",
187 | "border-image",
188 | "-webkit-border-image-source",
189 | "-moz-border-image-source",
190 | "-o-border-image-source",
191 | "border-image-source",
192 | "-webkit-border-image-slice",
193 | "-moz-border-image-slice",
194 | "-o-border-image-slice",
195 | "border-image-slice",
196 | "-webkit-border-image-width",
197 | "-moz-border-image-width",
198 | "-o-border-image-width",
199 | "border-image-width",
200 | "-webkit-border-image-outset",
201 | "-moz-border-image-outset",
202 | "-o-border-image-outset",
203 | "border-image-outset",
204 | "-webkit-border-image-repeat",
205 | "-moz-border-image-repeat",
206 | "-o-border-image-repeat",
207 | "border-image-repeat",
208 | "outline",
209 | "outline-width",
210 | "outline-style",
211 | "outline-color",
212 | "outline-offset",
213 | "-webkit-box-shadow",
214 | "-moz-box-shadow",
215 | "box-shadow",
216 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
217 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
218 | "opacity",
219 | "-ms-interpolation-mode",
220 | "-webkit-transition",
221 | "-moz-transition",
222 | "-ms-transition",
223 | "-o-transition",
224 | "transition",
225 | "-webkit-transition-delay",
226 | "-moz-transition-delay",
227 | "-ms-transition-delay",
228 | "-o-transition-delay",
229 | "transition-delay",
230 | "-webkit-transition-timing-function",
231 | "-moz-transition-timing-function",
232 | "-ms-transition-timing-function",
233 | "-o-transition-timing-function",
234 | "transition-timing-function",
235 | "-webkit-transition-duration",
236 | "-moz-transition-duration",
237 | "-ms-transition-duration",
238 | "-o-transition-duration",
239 | "transition-duration",
240 | "-webkit-transition-property",
241 | "-moz-transition-property",
242 | "-ms-transition-property",
243 | "-o-transition-property",
244 | "transition-property",
245 | "-webkit-transform",
246 | "-moz-transform",
247 | "-ms-transform",
248 | "-o-transform",
249 | "transform",
250 | "-webkit-transform-origin",
251 | "-moz-transform-origin",
252 | "-ms-transform-origin",
253 | "-o-transform-origin",
254 | "transform-origin",
255 | "-webkit-animation",
256 | "-moz-animation",
257 | "-ms-animation",
258 | "-o-animation",
259 | "animation",
260 | "-webkit-animation-name",
261 | "-moz-animation-name",
262 | "-ms-animation-name",
263 | "-o-animation-name",
264 | "animation-name",
265 | "-webkit-animation-duration",
266 | "-moz-animation-duration",
267 | "-ms-animation-duration",
268 | "-o-animation-duration",
269 | "animation-duration",
270 | "-webkit-animation-play-state",
271 | "-moz-animation-play-state",
272 | "-ms-animation-play-state",
273 | "-o-animation-play-state",
274 | "animation-play-state",
275 | "-webkit-animation-timing-function",
276 | "-moz-animation-timing-function",
277 | "-ms-animation-timing-function",
278 | "-o-animation-timing-function",
279 | "animation-timing-function",
280 | "-webkit-animation-delay",
281 | "-moz-animation-delay",
282 | "-ms-animation-delay",
283 | "-o-animation-delay",
284 | "animation-delay",
285 | "-webkit-animation-iteration-count",
286 | "-moz-animation-iteration-count",
287 | "-ms-animation-iteration-count",
288 | "-o-animation-iteration-count",
289 | "animation-iteration-count",
290 | "-webkit-animation-direction",
291 | "-moz-animation-direction",
292 | "-ms-animation-direction",
293 | "-o-animation-direction",
294 | "animation-direction"
295 | ]
296 | ]
297 | }
298 |
--------------------------------------------------------------------------------
/client/styles/.csslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "adjoining-classes": false,
3 | "box-sizing": false,
4 | "box-model": false,
5 | "compatible-vendor-prefixes": false,
6 | "floats": false,
7 | "font-sizes": false,
8 | "gradients": false,
9 | "important": false,
10 | "known-properties": false,
11 | "outline-none": false,
12 | "qualified-headings": false,
13 | "regex-selectors": false,
14 | "shorthand": false,
15 | "text-indent": false,
16 | "unique-headings": false,
17 | "universal-selector": false,
18 | "unqualified-attributes": false
19 | }
20 |
--------------------------------------------------------------------------------
/client/styles/_common.scss:
--------------------------------------------------------------------------------
1 | .clear{
2 | clear: both;
3 | }
--------------------------------------------------------------------------------
/client/styles/_custom.less:
--------------------------------------------------------------------------------
1 | //Override Variables in scaffolding.less
2 | @accent-1-color: @orange-A200;
--------------------------------------------------------------------------------
/client/styles/_defines.scss:
--------------------------------------------------------------------------------
1 | $icon-font-path: '../fonts/bootstrap';
2 | $fa-font-path: '../fonts';
3 |
4 | $white: #fff;
5 | $black: #000;
6 |
7 | // Flat UI
8 | $clouds_light: #ecf0f5;
9 | $clouds: #ECF0F1;
10 | $silver: #BDC3C7;
11 | $transparent_clouds: rgba(236, 240, 241, 0.2);
12 |
13 | $asbestos: #7F8C8D;
14 | $concrete: #95A5A6;
15 |
16 | $midnight_blue: green;
17 | $wet_asphalt: #34495E;
18 |
19 | $peter_river: #3498DB;
20 | $belize_hole: #2980B9;
21 |
22 | $sun_flower: #F1C40F;
23 | $transparent_sun_flower: rgba(241, 196, 15, 0.5);
24 | $orange: #F39C12;
25 |
26 | $carrot: #E67E22;
27 | $pumpkin: #D35400;
28 |
29 | $alizarin: #e74c3c;
30 | $pomegranate: #c0392b;
31 |
32 | $turquoise: #1ABC9C;
33 | $green_sea: #16A085;
34 |
35 | $emerald: #2ecc71;
36 | $nephritis: #27ae60;
37 |
38 | $amethyst: #9b59b6;
39 | $wisteria: #8e44ad;
40 |
41 | $text_color: $midnight_blue;
42 | $background_color: $wisteria
43 |
44 | $navbar_inverse_bg: $midnight_blue;
45 | $navbar_inverse_link_active_bg: $midnight_blue;
46 |
47 | $width: 960px;
48 | $label_width: 122px;
49 | $border_color: $silver;
50 | $border_radius: 5px;
51 | $message_width: 86%;
52 | $font_family: "Lucida Grande",Tahoma,Arial,Verdana,sans-serif;
53 | $font_size_error: 18px;
54 |
55 |
56 | $btn_color: $white;
57 | $input_border: $silver;
58 | $border_color: $silver;
59 |
60 | $btn_background_color: $midnight_blue;
61 | $btn_background_color_highlight: $wet_asphalt;
62 |
--------------------------------------------------------------------------------
/client/styles/font-icons/Read Me.txt:
--------------------------------------------------------------------------------
1 | Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures.
2 |
3 | You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects.
4 |
5 | You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu > Manage Projects) to retrieve your icon selection.
6 |
--------------------------------------------------------------------------------
/client/styles/font-icons/demo-files/demo.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | font-family: sans-serif;
5 | font-size: 1em;
6 | line-height: 1.5;
7 | color: #555;
8 | background: #fff;
9 | }
10 | h1 {
11 | font-size: 1.5em;
12 | font-weight: normal;
13 | }
14 | small {
15 | font-size: .66666667em;
16 | }
17 | a {
18 | color: #e74c3c;
19 | text-decoration: none;
20 | }
21 | a:hover, a:focus {
22 | box-shadow: 0 1px #e74c3c;
23 | }
24 | .bshadow0, input {
25 | box-shadow: inset 0 -2px #e7e7e7;
26 | }
27 | input:hover {
28 | box-shadow: inset 0 -2px #ccc;
29 | }
30 | input, fieldset {
31 | font-size: 1em;
32 | margin: 0;
33 | padding: 0;
34 | border: 0;
35 | }
36 | input {
37 | color: inherit;
38 | line-height: 1.5;
39 | height: 1.5em;
40 | padding: .25em 0;
41 | }
42 | input:focus {
43 | outline: none;
44 | box-shadow: inset 0 -2px #449fdb;
45 | }
46 | .glyph {
47 | font-size: 16px;
48 | width: 15em;
49 | padding-bottom: 1em;
50 | margin-right: 4em;
51 | margin-bottom: 1em;
52 | float: left;
53 | overflow: hidden;
54 | }
55 | .liga {
56 | width: 80%;
57 | width: calc(100% - 2.5em);
58 | }
59 | .talign-right {
60 | text-align: right;
61 | }
62 | .talign-center {
63 | text-align: center;
64 | }
65 | .bgc1 {
66 | background: #f1f1f1;
67 | }
68 | .fgc1 {
69 | color: #999;
70 | }
71 | .fgc0 {
72 | color: #000;
73 | }
74 | p {
75 | margin-top: 1em;
76 | margin-bottom: 1em;
77 | }
78 | .mvm {
79 | margin-top: .75em;
80 | margin-bottom: .75em;
81 | }
82 | .mtn {
83 | margin-top: 0;
84 | }
85 | .mtl, .mal {
86 | margin-top: 1.5em;
87 | }
88 | .mbl, .mal {
89 | margin-bottom: 1.5em;
90 | }
91 | .mal, .mhl {
92 | margin-left: 1.5em;
93 | margin-right: 1.5em;
94 | }
95 | .mhmm {
96 | margin-left: 1em;
97 | margin-right: 1em;
98 | }
99 | .mls {
100 | margin-left: .25em;
101 | }
102 | .ptl {
103 | padding-top: 1.5em;
104 | }
105 | .pbs, .pvs {
106 | padding-bottom: .25em;
107 | }
108 | .pvs, .pts {
109 | padding-top: .25em;
110 | }
111 | .unit {
112 | float: left;
113 | }
114 | .unitRight {
115 | float: right;
116 | }
117 | .size1of2 {
118 | width: 50%;
119 | }
120 | .size1of1 {
121 | width: 100%;
122 | }
123 | .clearfix:before, .clearfix:after {
124 | content: " ";
125 | display: table;
126 | }
127 | .clearfix:after {
128 | clear: both;
129 | }
130 | .hidden-true {
131 | display: none;
132 | }
133 | .textbox0 {
134 | width: 3em;
135 | background: #f1f1f1;
136 | padding: .25em .5em;
137 | line-height: 1.5;
138 | height: 1.5em;
139 | }
140 | #testDrive {
141 | display: block;
142 | padding-top: 24px;
143 | line-height: 1.5;
144 | }
145 | .fs0 {
146 | font-size: 16px;
147 | }
148 | .fs1 {
149 | font-size: 32px;
150 | }
151 |
--------------------------------------------------------------------------------
/client/styles/font-icons/demo-files/demo.js:
--------------------------------------------------------------------------------
1 | if (!('boxShadow' in document.body.style)) {
2 | document.body.setAttribute('class', 'noBoxShadow');
3 | }
4 |
5 | document.body.addEventListener("click", function(e) {
6 | var target = e.target;
7 | if (target.tagName === "INPUT" &&
8 | target.getAttribute('class').indexOf('liga') === -1) {
9 | target.select();
10 | }
11 | });
12 |
13 | (function() {
14 | var fontSize = document.getElementById('fontSize'),
15 | testDrive = document.getElementById('testDrive'),
16 | testText = document.getElementById('testText');
17 | function updateTest() {
18 | testDrive.innerHTML = testText.value || String.fromCharCode(160);
19 | if (window.icomoonLiga) {
20 | window.icomoonLiga(testDrive);
21 | }
22 | }
23 | function updateSize() {
24 | testDrive.style.fontSize = fontSize.value + 'px';
25 | }
26 | fontSize.addEventListener('change', updateSize, false);
27 | testText.addEventListener('input', updateTest, false);
28 | testText.addEventListener('change', updateTest, false);
29 | updateSize();
30 | }());
31 |
--------------------------------------------------------------------------------
/client/styles/font-icons/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IcoMoon Demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Font Name: icomoon (Glyphs: 4)
13 |
14 |
15 |
Grid Size: 16
16 |
32 |
48 |
64 |
80 |
81 |
82 |
83 |
84 |
Font Test Drive
85 |
90 |
92 |
93 |
94 |
95 |
96 |
97 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/client/styles/font-icons/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbasdf/react-kindling/cfc53eb47bf969eba5782b4d03d146532d4af754/client/styles/font-icons/fonts/icomoon.eot
--------------------------------------------------------------------------------
/client/styles/font-icons/fonts/icomoon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/client/styles/font-icons/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbasdf/react-kindling/cfc53eb47bf969eba5782b4d03d146532d4af754/client/styles/font-icons/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/client/styles/font-icons/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbasdf/react-kindling/cfc53eb47bf969eba5782b4d03d146532d4af754/client/styles/font-icons/fonts/icomoon.woff
--------------------------------------------------------------------------------
/client/styles/font-icons/selection.json:
--------------------------------------------------------------------------------
1 | {
2 | "IcoMoonType": "selection",
3 | "icons": [
4 | {
5 | "icon": {
6 | "paths": [
7 | "M789.534 64c0 0-200.956 0-267.94 0-120.122 0-233.17 91.006-233.17 196.422 0 107.726 81.882 194.666 204.088 194.666 8.498 0 16.756-0.17 24.84-0.752-7.928 15.186-13.6 32.288-13.6 50.042 0 29.938 16.104 54.21 36.468 74.024-15.386 0-30.242 0.448-46.452 0.448-148.782-0.002-263.3 94.76-263.3 193.022 0 96.778 125.542 157.312 274.334 157.312 169.624 0 263.306-96.244 263.306-193.028 0-77.602-22.896-124.072-93.686-174.134-24.216-17.142-70.532-58.836-70.532-83.344 0-28.72 8.196-42.868 51.428-76.646 44.312-34.624 75.674-83.302 75.674-139.916 0-67.406-30.022-133.098-86.374-154.772h84.956l59.96-43.344zM695.948 719.458c2.126 8.972 3.284 18.208 3.284 27.628 0 78.2-50.39 139.31-194.974 139.31-102.842 0-177.116-65.104-177.116-143.3 0-76.642 92.126-140.444 194.964-139.332 24 0.254 46.368 4.116 66.668 10.69 55.828 38.828 95.876 60.76 107.174 105.004zM531.286 427.776c-69.038-2.064-134.636-77.226-146.552-167.86-11.916-90.666 34.37-160.042 103.388-157.99 69.008 2.074 134.636 74.814 146.558 165.458 11.906 90.66-34.39 162.458-103.394 160.392z"
8 | ],
9 | "attrs": [],
10 | "tags": [
11 | "google",
12 | "brand"
13 | ],
14 | "defaultCode": 58514,
15 | "grid": 16
16 | },
17 | "attrs": [],
18 | "properties": {
19 | "id": 1291,
20 | "order": 2,
21 | "prevSize": 32,
22 | "code": 60039,
23 | "ligatures": "google, brand",
24 | "name": "google"
25 | },
26 | "setIdx": 0,
27 | "iconIdx": 391
28 | },
29 | {
30 | "icon": {
31 | "paths": [
32 | "M672 192c-88.366 0-160 71.634-160 160v96h-128v128h128v448h128v-448h144l32-128h-176v-96c0-17.672 14.326-32 32-32h160v-128h-160z"
33 | ],
34 | "attrs": [],
35 | "tags": [
36 | "facebook",
37 | "brand",
38 | "social"
39 | ],
40 | "defaultCode": 58520,
41 | "grid": 16
42 | },
43 | "attrs": [],
44 | "properties": {
45 | "id": 1297,
46 | "order": 3,
47 | "prevSize": 32,
48 | "code": 60044,
49 | "ligatures": "facebook, brand6",
50 | "name": "facebook"
51 | },
52 | "setIdx": 0,
53 | "iconIdx": 396
54 | },
55 | {
56 | "icon": {
57 | "paths": [
58 | "M1024 194.418c-37.676 16.708-78.164 28.002-120.66 33.080 43.372-26 76.686-67.17 92.372-116.23-40.596 24.078-85.556 41.56-133.41 50.98-38.32-40.83-92.922-66.34-153.346-66.34-116.022 0-210.088 94.058-210.088 210.078 0 16.466 1.858 32.5 5.44 47.878-174.6-8.764-329.402-92.4-433.018-219.506-18.084 31.028-28.446 67.116-28.446 105.618 0 72.888 37.088 137.192 93.46 174.866-34.438-1.092-66.832-10.542-95.154-26.278-0.020 0.876-0.020 1.756-0.020 2.642 0 101.788 72.418 186.696 168.522 206-17.626 4.8-36.188 7.372-55.348 7.372-13.538 0-26.698-1.32-39.528-3.772 26.736 83.46 104.32 144.206 196.252 145.896-71.9 56.35-162.486 89.934-260.916 89.934-16.958 0-33.68-0.994-50.116-2.94 92.972 59.61 203.402 94.394 322.042 94.394 386.422 0 597.736-320.124 597.736-597.744 0-9.108-0.206-18.168-0.61-27.18 41.056-29.62 76.672-66.62 104.836-108.748z"
59 | ],
60 | "attrs": [],
61 | "tags": [
62 | "twitter",
63 | "brand",
64 | "tweet",
65 | "social"
66 | ],
67 | "defaultCode": 58525,
68 | "grid": 16
69 | },
70 | "attrs": [],
71 | "properties": {
72 | "id": 1302,
73 | "order": 6,
74 | "prevSize": 32,
75 | "code": 60049,
76 | "ligatures": "twitter, brand11",
77 | "name": "twitter"
78 | },
79 | "setIdx": 0,
80 | "iconIdx": 401
81 | },
82 | {
83 | "icon": {
84 | "paths": [
85 | "M512.008 12.642c-282.738 0-512.008 229.218-512.008 511.998 0 226.214 146.704 418.132 350.136 485.836 25.586 4.738 34.992-11.11 34.992-24.632 0-12.204-0.48-52.542-0.696-95.324-142.448 30.976-172.504-60.41-172.504-60.41-23.282-59.176-56.848-74.916-56.848-74.916-46.452-31.778 3.51-31.124 3.51-31.124 51.4 3.61 78.476 52.766 78.476 52.766 45.672 78.27 119.776 55.64 149.004 42.558 4.588-33.086 17.852-55.68 32.506-68.464-113.73-12.942-233.276-56.85-233.276-253.032 0-55.898 20.004-101.574 52.76-137.428-5.316-12.9-22.854-64.972 4.952-135.5 0 0 43.006-13.752 140.84 52.49 40.836-11.348 84.636-17.036 128.154-17.234 43.502 0.198 87.336 5.886 128.256 17.234 97.734-66.244 140.656-52.49 140.656-52.49 27.872 70.528 10.35 122.6 5.036 135.5 32.82 35.856 52.694 81.532 52.694 137.428 0 196.654-119.778 239.95-233.79 252.624 18.364 15.89 34.724 47.046 34.724 94.812 0 68.508-0.596 123.644-0.596 140.508 0 13.628 9.222 29.594 35.172 24.566 203.322-67.776 349.842-259.626 349.842-485.768 0-282.78-229.234-511.998-511.992-511.998z"
86 | ],
87 | "attrs": [],
88 | "tags": [
89 | "github",
90 | "brand",
91 | "octacat",
92 | "social"
93 | ],
94 | "defaultCode": 61195,
95 | "grid": 16
96 | },
97 | "attrs": [],
98 | "properties": {
99 | "order": 5,
100 | "id": 1412,
101 | "prevSize": 32,
102 | "code": 60081,
103 | "ligatures": "github, brand40",
104 | "name": "github"
105 | },
106 | "setIdx": 0,
107 | "iconIdx": 433
108 | }
109 | ],
110 | "height": 1024,
111 | "metadata": {
112 | "name": "icomoon"
113 | },
114 | "preferences": {
115 | "showGlyphs": true,
116 | "showQuickUse": true,
117 | "showQuickUse2": true,
118 | "showSVGs": true,
119 | "fontPref": {
120 | "prefix": "icon-",
121 | "metadata": {
122 | "fontFamily": "icomoon"
123 | },
124 | "metrics": {
125 | "emSize": 1024,
126 | "baseline": 6.25,
127 | "whitespace": 50
128 | }
129 | },
130 | "imagePref": {
131 | "prefix": "icon-",
132 | "png": true,
133 | "useClassSelector": true,
134 | "color": 4473924,
135 | "bgColor": 16777215
136 | },
137 | "historySize": 100,
138 | "showCodes": true
139 | }
140 | }
--------------------------------------------------------------------------------
/client/styles/font-icons/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'icomoon';
3 | src:url('fonts/icomoon.eot?-px660i');
4 | src:url('fonts/icomoon.eot?#iefix-px660i') format('embedded-opentype'),
5 | url('fonts/icomoon.woff?-px660i') format('woff'),
6 | url('fonts/icomoon.ttf?-px660i') format('truetype'),
7 | url('fonts/icomoon.svg?-px660i#icomoon') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | }
11 |
12 | [class^="icon-"], [class*=" icon-"] {
13 | font-family: 'icomoon';
14 | speak: none;
15 | font-style: normal;
16 | font-weight: normal;
17 | font-variant: normal;
18 | text-transform: none;
19 | line-height: 1;
20 |
21 | /* Better Font Rendering =========== */
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: grayscale;
24 | }
25 |
26 | .icon-google:before {
27 | content: "\ea87";
28 | }
29 |
30 | .icon-facebook:before {
31 | content: "\ea8c";
32 | }
33 |
34 | .icon-twitter:before {
35 | content: "\ea91";
36 | }
37 |
38 | .icon-github:before {
39 | content: "\eab1";
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/client/styles/layout/_footer.less:
--------------------------------------------------------------------------------
1 | .footer {
2 |
3 | background-color: @grey-900;
4 | text-align: center;
5 |
6 | a {
7 | .mui-text-dark-white;
8 | }
9 |
10 | p {
11 | margin: 0 auto;
12 | padding: 0;
13 | .mui-text-light-white;
14 | max-width: 335px;
15 | }
16 |
17 | .mui-icon-button {
18 | color: @dark-white;
19 | }
20 | }
--------------------------------------------------------------------------------
/client/styles/layout/_full_width.less:
--------------------------------------------------------------------------------
1 | .full-width-section {
2 | padding: @desktop-gutter;
3 | .clearfix();
4 |
5 | .full-width-section-content {
6 | max-width: 1200px;
7 | margin: 0 auto;
8 | }
9 |
10 | @media @device-small {
11 | padding-top: (@desktop-gutter * 2);
12 | padding-bottom: (@desktop-gutter * 2);
13 | }
14 |
15 | @media @device-large {
16 | padding-top: (@desktop-gutter * 3);
17 | padding-bottom: (@desktop-gutter * 3);
18 | }
19 | }
--------------------------------------------------------------------------------
/client/styles/styles.js:
--------------------------------------------------------------------------------
1 | // This file exists to compile all css - less and scss - into one file
2 | require('../styles/styles.less');
3 | require('../styles/styles.scss');
--------------------------------------------------------------------------------
/client/styles/styles.less:
--------------------------------------------------------------------------------
1 | @import "../../node_modules/material-ui/src/less/scaffolding.less";
2 | @import "./_custom.less"; //Define a custom less file to override any variables defined in scaffolding.less
3 | @import "../../node_modules/material-ui/src/less/components.less";
4 |
5 | @import "./layout/_footer.less";
6 | @import "./layout/_full_width.less";
7 |
8 | /* App Specific Fonts */
9 | @import "../styles/font-icons/style.css";
10 |
11 | body{
12 | min-height: 2000px;
13 | padding-top: 0px;
14 | }
15 |
16 | a {
17 | color: @accent-1-color;
18 | text-decoration: none;
19 |
20 | &:hover {
21 | text-decoration: underline;
22 | }
23 | }
24 |
25 | /* App Specific Styles */
26 | .mui-app-canvas {
27 |
28 | height: 100%;
29 |
30 | .mui-app-bar {
31 | .github-icon-button {
32 | float: right;
33 | margin-right: -16px;
34 | }
35 | }
36 | }
37 |
38 | .login-container{
39 | .mui-paper{
40 | width: 400px;
41 | margin: 0 auto;
42 | }
43 | }
44 |
45 | .mui-paper{
46 | form{
47 | margin: 20px;
48 | }
49 | }
50 |
51 | .login-screen{
52 | margin: auto;
53 | }
54 |
55 | .login-paper{
56 | max-width: 450px;
57 | margin: 80px auto 0 auto;
58 | > div {
59 | padding: 23px;
60 | }
61 | }
62 |
63 | .register-paper{
64 | max-width: 450px;
65 | margin: 5% auto;
66 | > div {
67 | padding: 23px;
68 | }
69 | }
70 |
71 | .auth-button{
72 | max-width: 450px;
73 | height: 70px;
74 | margin: 20px auto;
75 | display:block;
76 | }
77 |
78 | .sign-up-button{
79 | margin-top: 20px;
80 | }
81 |
82 | /*Brandon's Shtuff*/
83 |
84 | /*fix height of inner div on buttons*/
85 | .login-social-button .mui-raised-button .mui-raised-button-container{
86 | height: 100%;
87 | display: flex;
88 | align-items: center;
89 | }
90 |
91 | .login-social-button .mui-raised-button .mui-raised-button-label{
92 | text-transform: none;
93 | align-self: center;
94 | }
95 |
96 | .login-social-button .mui-raised-button-container > div{
97 | display: flex;
98 | align-items: center;
99 | padding-left: 23px;
100 | }
101 | .login-social-button .example-button-icon::before{
102 | color: #fff;
103 | font-size: .9em;
104 | }
105 | .icon-facebook{
106 | margin-top: -3px;
107 | }
108 | .mui-text-field{
109 | width: 100%;
110 | }
111 | .create-account-button .mui-flat-button-label{
112 | color: rgba(0,0,0,.54);
113 | }
114 | .create-account-button{
115 | margin-top: 50px;
116 | display: inline-block;
117 | color: #666;
118 | }
119 | .login-button{
120 | float: right;
121 | margin-top: 50px;
122 | }
123 | .facebook-login .mui-raised-button.mui-is-secondary .mui-raised-button-container{
124 | background-color: #507BBE;
125 | }
126 | .twitter-login .mui-raised-button.mui-is-secondary .mui-raised-button-container{
127 | background-color: #57C1EE;
128 | }
129 | .google-login .mui-raised-button.mui-is-secondary .mui-raised-button-container{
130 | background-color: #E44B25;
131 | }
132 | .mui-app-bar{
133 | background-color: #009698;
134 | }
135 | .mui-raised-button.mui-is-primary .mui-raised-button-container{
136 | background-color: #FF5252;
137 | }
138 | .mui-raised-button.mui-is-primary:hover .mui-raised-button-container{
139 | background-color: #EF3A3A;
140 | }
141 | .mui-flat-button.mui-is-primary:hover, .mui-flat-button.mui-is-primary.mui-is-keyboard-focused{
142 | background-color: #FCD0D0;
143 | }
144 | .mui-text-field .mui-text-field-focus-underline{
145 | border-color: #009698;
146 | }
147 | .mui-text-field.mui-has-floating-labels.mui-is-focused .mui-text-field-floating-label{
148 | color: #009698;
149 | }
150 | h4{
151 | padding: 0;
152 | }
--------------------------------------------------------------------------------
/client/styles/styles.scss:
--------------------------------------------------------------------------------
1 | @import '_defines';
2 | //@import '../../node_modules/font-awesome/scss/font-awesome';
3 | //@import '../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap';
4 | @import '_common';
--------------------------------------------------------------------------------
/config/passport.js:
--------------------------------------------------------------------------------
1 | var LocalStrategy = require('passport-local').Strategy;
2 | var FacebookStrategy = require('passport-facebook').Strategy;
3 | var TwitterStrategy = require('passport-twitter').Strategy;
4 | var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
5 |
6 | var User = require('../app/models/user');
7 | var secrets = require('./secrets'); // use this one for testing
8 |
9 | module.exports = function(passport, controllers) {
10 |
11 | // =========================================================================
12 | // Passport session setup - serialize and unserialize users out of session
13 |
14 | // Serialize the user for the session
15 | passport.serializeUser(function(user, done) {
16 | done(null, user.id);
17 | });
18 |
19 | // Deserialize the user
20 | passport.deserializeUser(function(id, done) {
21 | User.findById(id, function(err, user) {
22 | done(err, user);
23 | });
24 | });
25 |
26 | // =========================================================================
27 | // Local login
28 | passport.use('local-login', new LocalStrategy({
29 | usernameField : 'email', // use email instead of username
30 | passwordField : 'password',
31 | passReqToCallback : true
32 | }, controllers.strategies.local.strategyCallback));
33 |
34 | // =========================================================================
35 | // Local signup
36 | passport.use('local-signup', new LocalStrategy({
37 | usernameField : 'email', // use email instead of username
38 | passwordField : 'password',
39 | passReqToCallback : true
40 | }, controllers.users.strategyCallback));
41 |
42 | // =========================================================================
43 | // Facebook
44 | passport.use(new FacebookStrategy({
45 | clientID : secrets.facebookAuth.clientID,
46 | clientSecret : secrets.facebookAuth.clientSecret,
47 | callbackURL : secrets.facebookAuth.callbackURL,
48 | passReqToCallback : true
49 | }, controllers.strategies.facebook.strategyCallback));
50 |
51 | // =========================================================================
52 | // Twitter
53 | passport.use(new TwitterStrategy({
54 | consumerKey : secrets.twitterAuth.consumerKey,
55 | consumerSecret : secrets.twitterAuth.consumerSecret,
56 | callbackURL : secrets.twitterAuth.callbackURL,
57 | passReqToCallback : true
58 | }, controllers.strategies.twitter.strategyCallback));
59 |
60 | // =========================================================================
61 | // Google
62 | passport.use(new GoogleStrategy({
63 | clientID : secrets.googleAuth.clientID,
64 | clientSecret : secrets.googleAuth.clientSecret,
65 | callbackURL : secrets.googleAuth.callbackURL,
66 | passReqToCallback : true
67 | }, controllers.strategies.google.strategyCallback));
68 |
69 | };
70 |
--------------------------------------------------------------------------------
/config/secrets.example.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | 'mongoUrl' : 'mongodb://localhost/reactkindling', // looks like mongodb://:@mongo.onmodulus.net:27017/Mikha4ot
4 |
5 | 'sessionSecret' : 'Generate a secure session secret and put it here.',
6 |
7 | 'devApplicationUrl': 'https://myserversubdomain.ngrok.com',
8 | 'devAssetsUrl': 'https://myassetssubdomain.ngrok.com',
9 |
10 | 'facebookAuth' : { // Get this from https://developers.facebook.com
11 | 'clientID' : 'your-secret-clientID-here', // your App ID
12 | 'clientSecret' : 'your-client-secret-here', // your App Secret
13 | 'callbackURL' : 'http://localhost:8080/auth/facebook/callback' // In production localhost will need to be your live url
14 | },
15 |
16 | 'twitterAuth' : { // Get this from https://apps.twitter.com/
17 | 'consumerKey' : 'your-consumer-key-here',
18 | 'consumerSecret' : 'your-client-secret-here',
19 | 'callbackURL' : 'http://localhost:8080/auth/twitter/callback'
20 | },
21 |
22 | 'googleAuth' : {
23 | 'clientID' : 'your-secret-clientID-here',
24 | 'clientSecret' : 'your-client-secret-here',
25 | 'callbackURL' : 'http://localhost:8080/auth/google/callback'
26 | }
27 |
28 | };
--------------------------------------------------------------------------------
/config/settings.js:
--------------------------------------------------------------------------------
1 | var info = require('../package.json');
2 | var path = require('path');
3 | var secrets = require('./secrets.js');
4 |
5 | var clientAppPath = path.join(__dirname, '../client/');
6 | var devRelativeOutput = '/build/dev/';
7 | var prodRelativeOutput = '/build/release/';
8 | var devOutput = path.join(__dirname, '..' + devRelativeOutput);
9 | var prodOutput = path.join(__dirname, '..' + prodRelativeOutput);
10 |
11 | var serverPort = 8888;
12 | var hotPort = 8080;
13 |
14 | module.exports = {
15 | title: info.title,
16 | author: info.author,
17 | version: info.versions,
18 | build: Date.now(),
19 |
20 | devRelativeOutput: devRelativeOutput,
21 | prodRelativeOutput: prodRelativeOutput,
22 |
23 | devOutput: devOutput,
24 | prodOutput: prodOutput,
25 |
26 | // This setting affects the 'build' in gulpfile.js
27 | // If projectType is 'client' then pages from client/html will be used
28 | // If projectType is 'client-server' page from client/html will be ignored in the build
29 | // and node will serve pages from app/views instead
30 | projectType: 'client-server',
31 |
32 | // Used by pageSpeed. This should be the url of your production server
33 | applicationUrl: 'http://www.example.com',
34 |
35 | // Dev urls
36 | devApplicationUrl: secrets.devApplicationUrl || 'http://localhost:' + serverPort,
37 | devAssetsUrl: secrets.devAssetsUrl || 'http://localhost:' + hotPort,
38 |
39 | ports: {
40 | server: serverPort,
41 | reloadProxy: 5000,
42 | hotPort: hotPort,
43 | livereload: 35729
44 | },
45 | appFiles: [
46 | './server.js',
47 | './app/**/*'
48 | ],
49 | assets: {
50 | paths: {
51 | all: clientAppPath + 'assets/**/*',
52 | output: {
53 | dev: devOutput,
54 | prod: prodOutput
55 | }
56 | }
57 | },
58 | fonts: {
59 | paths: {
60 | all: [
61 | clientAppPath + 'fonts/**'
62 | // './node_modules/bootstrap-sass/assets/fonts/**',
63 | // './node_modules/font-awesome/fonts/**'
64 | ],
65 | output: {
66 | dev: devOutput + 'fonts',
67 | prod: prodOutput + 'fonts'
68 | }
69 | }
70 | },
71 | scripts: {
72 | paths: {
73 | all: clientAppPath + 'js/**/*.js',
74 | entries: {
75 | app: clientAppPath + 'js/app.jsx',
76 | styles: clientAppPath + 'styles/styles.js'
77 | },
78 | output: {
79 | dev: devOutput + 'js',
80 | prod: prodOutput + 'js'
81 | },
82 | relativeOutput: {
83 | dev: devRelativeOutput + 'js',
84 | prod: prodRelativeOutput + 'js'
85 | }
86 | }
87 | },
88 | styles: {
89 | autoPrefix: ['last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'],
90 | paths: {
91 | all: clientAppPath + 'styles/**/*.scss',
92 |
93 | output: {
94 | dev: devOutput + 'css/',
95 | prod: prodOutput + 'css/'
96 | }
97 | },
98 | sourcemaps: false
99 | },
100 | html: {
101 | paths: {
102 | all: clientAppPath + 'html/**/*.html',
103 | ignore: clientAppPath + '!html/**/_*.html',
104 | output: {
105 | dev: devOutput,
106 | prod: prodOutput
107 | }
108 | }
109 | },
110 | images: {
111 | paths: {
112 | all: [clientAppPath + 'images/*', clientAppPath + 'images/**/*.jpg', clientAppPath + 'images/**/*.jpeg', clientAppPath + 'images/**/*.gif'],
113 | output: {
114 | dev: devOutput + 'images/',
115 | prod: prodOutput + 'images/'
116 | }
117 | },
118 | compression: 3
119 | }
120 | };
121 |
--------------------------------------------------------------------------------
/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 | var ExtractTextPlugin = require("extract-text-webpack-plugin");
4 | var settings = require('./settings.js');
5 |
6 | module.exports = function(release){
7 |
8 | var autoprefix = '{browsers:["Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", "Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}';
9 | var jsLoaders = ['babel-loader?experimental&optional=runtime'];
10 | var cssLoaders = ['css-loader', 'autoprefixer-loader?' + autoprefix];
11 |
12 | var scssLoaders = cssLoaders.slice(0);
13 | scssLoaders.push('sass-loader?outputStyle=expanded&includePaths[]=' + (path.resolve(__dirname, './node_modules/bootstrap-sass')));
14 |
15 | var lessLoaders = cssLoaders.slice(0);
16 | lessLoaders.push('less-loader');
17 |
18 | var entries;
19 |
20 | if(release){
21 | entries = settings.scripts.paths.entries;
22 | } else {
23 | jsLoaders.unshift('react-hot');
24 |
25 | // Configure entries with hotloader
26 | var originalEntries = settings.scripts.paths.entries;
27 | entries = {};
28 | for(var name in originalEntries){
29 | entries[name] = ['webpack-dev-server/client?http://localhost:' + settings.ports.hotPort, 'webpack/hot/only-dev-server', originalEntries[name]];
30 | }
31 | }
32 |
33 | var cssEntries = settings.styles.paths.entries;
34 | for(var name in cssEntries){
35 | entries[name] = cssEntries[name];
36 | }
37 |
38 | return {
39 | entry: entries,
40 | output: {
41 | path: release ? settings.prodOutput : settings.devOutput,
42 | filename: '[name].js',
43 | publicPath: release ? settings.scripts.paths.relativeOutput.prod : 'http://localhost:' + settings.ports.hotPort + settings.devRelativeOutput,
44 | sourceMapFilename: "debugging/[file].map",
45 | pathinfo: !release // http://webpack.github.io/docs/configuration.html#output-pathinfo
46 | },
47 | resolve: {
48 | extensions: ['', '.js', '.json', '.jsx'],
49 | modulesDirectories: ["node_modules", "vendor"]
50 | },
51 | cache: true,
52 | devtool: release ? false : "eval", // http://webpack.github.io/docs/configuration.html#devtool
53 | stats: {
54 | colors: true
55 | },
56 | plugins: release ? [
57 | new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}),
58 | new webpack.optimize.DedupePlugin(),
59 | new webpack.optimize.UglifyJsPlugin(),
60 | new webpack.optimize.OccurenceOrderPlugin(),
61 | new webpack.optimize.AggressiveMergingPlugin(),
62 | new ExtractTextPlugin("[name].css")
63 | ] : [
64 | new ExtractTextPlugin("[name].css"),
65 | new webpack.HotModuleReplacementPlugin(),
66 | new webpack.NoErrorsPlugin()
67 | //new webpack.optimize.CommonsChunkPlugin('init.js') // Use to extract common code from multiple entry points into a single init.js
68 | ],
69 | module: {
70 | loaders: [
71 | { test: /\.js$/, loaders: jsLoaders, exclude: /node_modules/ },
72 | { test: /\.jsx?$/, loaders: jsLoaders, exclude: /node_modules/ },
73 | { test: /\.scss$/, loader: ExtractTextPlugin.extract('style-loader', scssLoaders.join('!')) },
74 | { test: /\.css$/ , loader: ExtractTextPlugin.extract('style-loader', cssLoaders.join('!')) },
75 | { test: /\.less$/ , loader: ExtractTextPlugin.extract('style-loader', lessLoaders.join('!')) },
76 | //{ test: /\.html$/, loader: 'webpack-compile-templates' }, // Add if you need to compile underscore.js - https://www.npmjs.com/package/webpack-compile-templates
77 | //{ test: /.*\.(gif|png|jpg|jpeg|svg)$/, loaders: ['file?hash=sha512&digest=hex&size=16&name=[hash].[ext]', 'image-webpack-loader?optimizationLevel=7&interlaced=false']},
78 | //{ test: /.*\.(eot|woff2|woff|ttf)/, loader: 'file?hash=sha512&digest=hex&size=16&name=cd [hash].[ext]'}
79 | { test: /\.(png|woff|woff2|eot|ttf|svg)($|\?)/, loader: 'url-loader' }
80 | ]
81 |
82 | }
83 | };
84 | };
85 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var gutil = require('gulp-util');
3 | var sass = require('gulp-sass');
4 | var uglify = require('gulp-uglify');
5 | var autoprefixer = require('gulp-autoprefixer');
6 | var minifycss = require('gulp-minify-css');
7 | var jshint = require('gulp-jshint');
8 | var imagemin = require('gulp-imagemin');
9 | var htmlmin = require('gulp-htmlmin');
10 | var embedlr = require("gulp-embedlr");
11 | var fileinclude = require('gulp-file-include');
12 | var gulpif = require('gulp-if');
13 | var replace = require('gulp-replace');
14 | var rename = require('gulp-rename');
15 | var shell = require('gulp-shell');
16 | var nodemon = require('gulp-nodemon');
17 |
18 | var webpackDevServer = require('webpack-dev-server');
19 | var runSequence = require('run-sequence');
20 | var path = require('path');
21 | var argv = require('minimist')(process.argv.slice(2));
22 | var pagespeed = require('psi');
23 | var webpack = require('webpack');
24 | var del = require('del');
25 |
26 | var release = argv.release || false;
27 | var settings = require('./config/settings.js');
28 | var webpackConfig = require('./config/webpack.config.js')(release);
29 |
30 | // Tasks
31 | gulp.task('default', ['serve:node', 'serve:webpack']);
32 |
33 | gulp.task('build', ['clean'], function(callback){
34 | var tasks = ['images', 'assets', 'fonts'];
35 | if(settings.projectType == 'client'){
36 | tasks.push('html');
37 | }
38 | if(!release){
39 | tasks.unshift('hint');
40 | } else {
41 | // In dev the webpack dev server handles compiling assets, but when we release we need to run webpack
42 | tasks.unshift('webpack');
43 | }
44 | runSequence(tasks, callback);
45 | });
46 |
47 | gulp.task('clean', function(callback){
48 | if(release){
49 | del([settings.prodOutput], callback);
50 | } else {
51 | del([settings.devOutput], callback);
52 | }
53 | });
54 |
55 | // js packaging with webpack
56 | gulp.task('webpack', function(callback){
57 | webpack(webpackConfig, function (err, stats){
58 | if(err){
59 | throw new gutil.PluginError('webpack', err);
60 | }
61 | return callback();
62 | });
63 | });
64 |
65 | gulp.task('hint', function(){
66 | return gulp.src(settings.scripts.paths.all)
67 | .pipe(jshint('.jshintrc'))
68 | .pipe(jshint.reporter('default'));
69 | });
70 |
71 | gulp.task('images', function(){
72 | var images = gulp.src(settings.images.paths.all);
73 | if(release){
74 | return images
75 | .pipe(imagemin({
76 | optimizationLevel: settings.images.compression,
77 | progressive: true,
78 | interlaced: true
79 | }))
80 | .pipe(gulp.dest(settings.images.paths.output.prod));
81 | } else {
82 | return images
83 | .pipe(gulp.dest(settings.images.paths.output.dev));
84 | }
85 | });
86 |
87 | gulp.task('assets', function(){
88 | var assets = gulp.src(settings.assets.paths.all);
89 | if(release){
90 | return assets
91 | .pipe(gulp.dest(settings.assets.paths.output.prod));
92 | } else {
93 | return assets
94 | .pipe(gulp.dest(settings.assets.paths.output.dev));
95 | }
96 | });
97 |
98 | gulp.task('fonts', function(){
99 | var fonts = gulp.src(settings.fonts.paths.all);
100 | if(release){
101 | return fonts
102 | .pipe(gulp.dest(settings.fonts.paths.output.prod));
103 | } else {
104 | return fonts
105 | .pipe(gulp.dest(settings.fonts.paths.output.dev));
106 | }
107 | });
108 |
109 | // Build html. This will also use fileinclude for basic partial includes.
110 | gulp.task('html', function(){
111 | var html = gulp.src([settings.html.paths.all, settings.html.paths.ignore])
112 | .pipe(fileinclude())
113 | .pipe(rename({ extname: "" }))
114 | .pipe(rename({ extname: ".html" }));
115 |
116 | if(!release){
117 | return html
118 | .pipe(embedlr())
119 | .pipe(gulp.dest(settings.html.paths.output.dev));
120 | } else {
121 | return html
122 | .pipe(htmlmin({ removeComments: true, collapseWhitespace: true, minifyJS: true }))
123 | .pipe(gulp.dest(settings.html.paths.output.prod));
124 | }
125 | });
126 |
127 | // Launch the node server
128 | gulp.task('serve:node', ['build'], function(){
129 | var assign = require('react/lib/Object.assign');
130 | nodemon({
131 | script: path.join(__dirname, './server.js'),
132 | ext: 'js html',
133 | // execMap: {
134 | // js: "node --debug"
135 | // },
136 | env: assign({ NODE_ENV: 'development' }, process.env)
137 | });
138 | });
139 |
140 | // Run webpack hot reload server
141 | gulp.task('serve:webpack', function(){
142 |
143 | gutil.log('Starting Webpack hot load server');
144 |
145 | new webpackDevServer(webpack(webpackConfig), {
146 | //contentBase: 'http://localhost:' + settings.ports.hotPort,
147 | publicPath: webpackConfig.output.publicPath,
148 | hot: true,
149 | noInfo: true,
150 | headers: { "Access-Control-Allow-Origin": "*" }
151 | }).listen(settings.ports.hotPort, 'localhost', function(err, result){
152 | if(err){
153 | throw new gutil.PluginError('webpack-dev-server', err);
154 | }
155 | gutil.log('Webpack hot load server listening on port ' + settings.ports.hotPort);
156 | });
157 |
158 | });
159 |
160 | gulp.task('release', function(callback){
161 | release = true;
162 | var tasks = ['build', 'deploy:amazon'];
163 | runSequence(tasks, callback);
164 | });
165 |
166 | // Deploy to Amazon S3
167 | gulp.task('deploy:amazon', shell.task([
168 | 's3_website push'
169 | ]));
170 |
171 | // Deploy to GitHub Pages
172 | gulp.task('deploy:github', function(){
173 |
174 | var ghPages = require('gulp-gh-pages');
175 |
176 | // Remove temp folder
177 | if(argv.clean){
178 | var os = require('os');
179 | var repoPath = path.join(os.tmpdir(), 'tmpRepo');
180 | gutil.log('Delete ' + gutil.colors.magenta(repoPath));
181 | del.sync(repoPath, {force: true});
182 | }
183 |
184 | return gulp.src(settings.prodOutput + '/**/*')
185 | .pipe(gulpif('**/robots.txt', !argv.production ? replace('Disallow:', 'Disallow: /') : gutil.noop()))
186 | .pipe(ghPages({
187 | remoteUrl: 'https://github.com/{name}/{name}.github.io.git',
188 | branch: 'master'
189 | }));
190 | });
191 |
192 | // Run PageSpeed Insights
193 | gulp.task('pagespeed', function(callback) {
194 | pagespeed.output(settings.applicationUrl, {
195 | strategy: 'mobile'
196 | // By default we use the PageSpeed Insights free (no API key) tier.
197 | // Use a Google Developer API key if you have one: http://goo.gl/RkN0vE
198 | // key: 'YOUR_API_KEY'
199 | }, callback);
200 | });
201 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma config info: http://karma-runner.github.io/0.12/config/configuration-file.html
2 | module.exports = function(config) {
3 |
4 | function isCoverage(argument) {
5 | return argument === '--coverage';
6 | }
7 |
8 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
9 | var reporters = ['spec'];
10 |
11 | if(process.argv.some(isCoverage)){
12 | reporters.push('coverage');
13 | }
14 |
15 | var testConfig = {
16 |
17 | // If browser does not capture in given timeout [ms], kill it
18 | captureTimeout: 60000,
19 |
20 | // How long will Karma wait for a message from a browser before disconnecting from it (in ms)
21 | browserNoActivityTimeout: 60000,
22 |
23 | // enable / disable colors in the output (reporters and logs)
24 | colors: true,
25 |
26 | // web server port
27 | port: 9876,
28 |
29 | files: [
30 | './specs/spec_helper.js',
31 | //'./specs/**/*.spec.js' // Use webpack to build each test individually. If changed here, match the change in preprocessors
32 | './specs/tests.webpack.js' // More performant but tests cannot be run individually
33 | ],
34 |
35 | // Transpile tests with the karma-webpack plugin
36 | preprocessors: {
37 | //'./specs/**/*.spec.js': ['webpack', 'sourcemap'] // Use webpack to build each test individually. If changed here, match the change in files
38 | './specs/tests.webpack.js': ['webpack', 'sourcemap'] // More performant but tests cannot be run individually
39 | },
40 |
41 | // Run the tests using any of the following browsers
42 | // - Chrome npm install --save-dev karma-chrome-launcher
43 | // - ChromeCanary
44 | // - Firefox npm install --save-dev karma-firefox-launcher
45 | // - Opera npm install --save-dev karma-opera-launcher
46 | // - Safari npm install --save-dev karma-safari-launcher (only Mac)
47 | // - PhantomJS npm install --save-dev karma-phantomjs-launcher
48 | // - IE npm install karma-ie-launcher (only Windows)
49 | browsers: ['Chrome'],
50 |
51 | // Exit the test runner as well when the test suite returns.
52 | singleRun: false,
53 |
54 | // enable / disable watching file and executing tests whenever any file changes
55 | autoWatch: true,
56 |
57 | // Use jasmine as the test framework
58 | frameworks: ['jasmine'],
59 |
60 | reporters: reporters,
61 |
62 | // karma-webpack configuration. Load and transpile js and jsx files.
63 | // Use istanbul-transformer post loader to generate code coverage report.
64 | webpack: {
65 | devtool: 'eval',
66 | module: {
67 | loaders: [
68 | { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" },
69 | { test: /\.jsx?$/, exclude: /node_modules/, loader: "babel-loader" }
70 | ]
71 | },
72 | resolve: {
73 | extensions: ['', '.js', '.jsx'],
74 | modulesDirectories: ["node_modules", "vendor"]
75 | }
76 | },
77 |
78 | // Reduce the noise to the console
79 | webpackMiddleware: {
80 | noInfo: true,
81 | stats: {
82 | colors: true
83 | }
84 | }
85 |
86 | };
87 |
88 |
89 | // Generate code coverage report if --coverage is specified
90 | if(process.argv.some(isCoverage)) {
91 | // Generate a code coverage report using `lcov` format. Result will be output to coverage/lcov.info
92 | // run using `npm coveralls`
93 | testConfig['webpack']['module']['postLoaders'] = [{
94 | test: /\.jsx?$/,
95 | exclude: /(test|node_modules)\//,
96 | loader: 'istanbul-instrumenter'
97 | }];
98 |
99 | testConfig['coverageReporter'] = {
100 | dir: 'coverage/',
101 | reporters: [
102 | { type: 'lcovonly', subdir: '.', file: 'lcov.info' },
103 | { type: 'html', subdir: 'html' }
104 | ]
105 | };
106 | }
107 |
108 | config.set(testConfig);
109 | };
110 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_kindling",
3 | "private": false,
4 | "version": "0.0.1",
5 | "author": "Justin Ball ",
6 | "description": "Template for a React.js application",
7 | "repository": "https://github.com/jbasdf/react_kindling",
8 | "scripts": {
9 | "start": "gulp",
10 | "test": "karma start",
11 | "coveralls": "cat coverage/lcov.info | coveralls"
12 | },
13 | "dependencies": {
14 | "babel": "^4.3.0",
15 | "babel-runtime": "^4.3.0",
16 | "bcrypt-nodejs": "^0.0.3",
17 | "body-parser": "^1.6.4",
18 | "compression": "^1.3.0",
19 | "connect-flash": "~0.1.1",
20 | "cookie-parser": "~1.0.0",
21 | "cors": "^2.5.2",
22 | "ejs": "~0.8.5",
23 | "express": "^4.3.2",
24 | "express-session": "~1.11.1",
25 | "flux": "^2.0.1",
26 | "font-awesome": "^4.2.0",
27 | "jquery": "^2.1.3",
28 | "lodash": "^3.2.0",
29 | "material-ui": "^0.7.4",
30 | "mongoose": "~3.8.1",
31 | "morgan": "~1.0.0",
32 | "object-assign": "^2.0.0",
33 | "passport": "~0.1.17",
34 | "passport-facebook": "~1.0.2",
35 | "passport-google-oauth": "~0.1.5",
36 | "passport-local": "~0.1.6",
37 | "passport-twitter": "~1.0.2",
38 | "react": "^0.13.2",
39 | "react-router": "^0.13.2",
40 | "serve-favicon": "^2.2.0",
41 | "superagent": "^0.21.0",
42 | "ua-parser-js": "^0.7.3",
43 | "validator": "^3.32.0"
44 | },
45 | "devDependencies": {
46 | "autoprefixer-loader": "^1.0.0",
47 | "babel-core": "^4.3.0",
48 | "babel-loader": "^4.0.0",
49 | "browser-sync": "^2.0.1",
50 | "bufferstreams": "^1.0.1",
51 | "coveralls": "^2.11.2",
52 | "css-loader": "^0.9.0",
53 | "del": "^1.1.1",
54 | "ecstatic": "^0.5.8",
55 | "event-stream": "^3.2.1",
56 | "extend-shallow": "^0.2.0",
57 | "extract-text-webpack-plugin": "^0.3.8",
58 | "gulp": "^3.8.10",
59 | "gulp-autoprefixer": "^2.0.0",
60 | "gulp-changed": "^1.1.0",
61 | "gulp-embedlr": "^0.5.2",
62 | "gulp-file-include": "^0.8.0",
63 | "gulp-gh-pages": "^0.4.0",
64 | "gulp-htmlmin": "^1.1.0",
65 | "gulp-if": "^1.2.5",
66 | "gulp-imagemin": "^2.2.0",
67 | "gulp-install": "^0.2.0",
68 | "gulp-jshint": "^1.9.0",
69 | "gulp-load-plugins": "^0.8.0",
70 | "gulp-minify-css": "^0.3.11",
71 | "gulp-nodemon": "^2.0.2",
72 | "gulp-plumber": "^0.6.6",
73 | "gulp-react": "^2.0.0",
74 | "gulp-rename": "^1.2.0",
75 | "gulp-replace": "^0.5.2",
76 | "gulp-sass": "~1.3.3",
77 | "gulp-shell": "^0.3.0",
78 | "gulp-uglify": "^1.0.2",
79 | "gulp-util": "^3.0.1",
80 | "html-minifier": "^0.7.0",
81 | "imports-loader": "~0.6.3",
82 | "istanbul": "^0.3.8",
83 | "istanbul-instrumenter-loader": "^0.1.2",
84 | "jasmine-core": "^2.2.0",
85 | "jshint": "^2.5.11",
86 | "jshint-loader": "^0.8.0",
87 | "jshint-stylish": "^1.0.0",
88 | "karma": "^0.12.31",
89 | "karma-chrome-launcher": "^0.1.7",
90 | "karma-cli": "0.0.4",
91 | "karma-coverage": "^0.2.7",
92 | "karma-firefox-launcher": "^0.1.4",
93 | "karma-jasmine": "^0.3.5",
94 | "karma-phantomjs-launcher": "^0.1.4",
95 | "karma-sourcemap-loader": "^0.3.4",
96 | "karma-spec-reporter": "0.0.16",
97 | "karma-webpack": "^1.5.0",
98 | "less": "^2.4.0",
99 | "less-loader": "^2.0.0",
100 | "minimist": "^1.1.0",
101 | "nodemon": "^1.3.7",
102 | "npm-shrinkwrap": "^5.1.0",
103 | "phantomjs": "^1.9.16",
104 | "protractor": "^1.5.0",
105 | "psi": "^1.0.5",
106 | "react-hot-loader": "^1.2.3",
107 | "react-tools": "^0.12.2",
108 | "rimraf": "^2.2.8",
109 | "run-sequence": "^1.0.2",
110 | "sass-loader": "^1.0.1",
111 | "style-loader": "^0.8.2",
112 | "url-loader": "^0.5.5",
113 | "webpack": "^1.8.5",
114 | "webpack-compile-templates": "^0.1.4",
115 | "webpack-dev-server": "^1.8.0"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/s3_website.example.yml:
--------------------------------------------------------------------------------
1 | s3_id: S3_ID
2 | s3_secret: S3_SECRET
3 | s3_bucket: S3_BUCKET
4 |
5 |
6 | # Below are examples of all the available configurations.
7 | # See README for more detailed info on each of them.
8 |
9 | site: public
10 |
11 | # max_age:
12 | # "assets/*": 6000
13 | # "*": 300
14 |
15 | # gzip:
16 | # - .html
17 | # - .css
18 | # - .md
19 | # gzip_zopfli: true
20 |
21 | # See http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region for valid endpoints
22 | # s3_endpoint: ap-northeast-1
23 |
24 | # ignore_on_server: that_folder_of_stuff_i_dont_keep_locally
25 |
26 | # exclude_from_upload:
27 | # - those_folders_of_stuff
28 | # - i_wouldnt_want_to_upload
29 |
30 | # s3_reduced_redundancy: true
31 |
32 | # cloudfront_distribution_id: your-dist-id
33 |
34 | # cloudfront_distribution_config:
35 | # default_cache_behavior:
36 | # min_TTL: <%= 60 * 60 * 24 %>
37 | # aliases:
38 | # quantity: 1
39 | # items:
40 | # CNAME: your.website.com
41 |
42 | # cloudfront_invalidate_root: true
43 |
44 | # concurrency_level: 5
45 |
46 | # redirects:
47 | # index.php: /
48 | # about.php: about.html
49 | # music-files/promo.mp4: http://www.youtube.com/watch?v=dQw4w9WgXcQ
50 |
51 | # routing_rules:
52 | # - condition:
53 | # key_prefix_equals: blog/some_path
54 | # redirect:
55 | # host_name: blog.example.com
56 | # replace_key_prefix_with: some_new_path/
57 | # http_redirect_code: 301
58 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('babel/register'); // Install `babel` hook
2 | var express = require('express');
3 | var app = express();
4 | var path = require('path');
5 | var mongoose = require('mongoose');
6 | var flash = require('connect-flash');
7 | var morgan = require('morgan');
8 | var cookieParser = require('cookie-parser');
9 | var bodyParser = require('body-parser');
10 | var session = require('express-session');
11 | var fs = require('fs');
12 | var passport = require('passport');
13 | var favicon = require('serve-favicon');
14 |
15 | var settings = require('./config/settings.js');
16 | var secrets = require('./config/secrets.js');
17 | var passportConfig = require('./config/passport');
18 | var port = process.env.PORT || settings.ports.server;
19 |
20 |
21 | // ======================================================================
22 | // Setup the database
23 |
24 | mongoose.connect(secrets.mongoUrl);
25 |
26 | // ======================================================================
27 | // set up logging
28 |
29 | app.use(morgan('dev')); // log every request to the console
30 |
31 | // ======================================================================
32 | // Server configuration. If using nginx you might want to var nginx handle these instead
33 |
34 | // Enable cross-origin resource sharing (CORS)
35 | var cors = require('cors');
36 | app.use(cors());
37 |
38 | // Enable compression
39 | var compression = require('compression');
40 | app.use(compression());
41 |
42 | // ======================================================================
43 | // Serve static assets in the public directory. In production serve assets in build as well
44 | //app.use(favicon(path.join(__dirname, '/app/assets/favicon.ico')));
45 |
46 | app.use(express.static(path.join(__dirname, 'app/assets')));
47 |
48 | if (process.env.NODE_ENV === "production"){
49 | // In production serve a static directory for the webpack-compiled js and css.
50 | // In development the webpack dev server handles js and css
51 | app.use('/build', express.static(path.join(__dirname, '/build')));
52 | }
53 |
54 | // ======================================================================
55 | // Parse Cookies, forms and urls
56 |
57 | app.use(cookieParser()); // read cookies (needed for auth)
58 | app.use(bodyParser.json()); // get information from html forms
59 | app.use(bodyParser.urlencoded({ extended: true }));
60 |
61 | // ======================================================================
62 | // Use ejs for server side templates
63 |
64 | app.set('view engine', 'ejs');
65 |
66 | // ======================================================================
67 | // Configure session
68 | app.use(session({
69 | secret: secrets.sessionSecret,
70 | resave: false,
71 | saveUninitialized: true
72 | }));
73 |
74 | // ======================================================================
75 | // Configure views and routes
76 | app.set('views', __dirname + '/app/views');
77 |
78 | // ======================================================================
79 | // Load Controllers
80 | function loadControllers(dir){
81 | var controllers = {};
82 | fs.readdirSync(dir).forEach(function(file){
83 | if(file.substr(-3) == '.js'){
84 | var name = file.replace('.js', '');
85 | console.log('Loading controller ' + name);
86 | controllers[name] = require(dir + file)(app, passport);
87 | }
88 | });
89 | return controllers;
90 | }
91 |
92 | var controllers = loadControllers(path.join(__dirname, './app/controllers/'));
93 | controllers.strategies = loadControllers(path.join(__dirname, './app/controllers/strategies/'));
94 |
95 | // ======================================================================
96 | // Setup passport
97 | passportConfig(passport, controllers);
98 |
99 | app.use(passport.initialize());
100 | app.use(passport.session());
101 | app.use(flash()); // use connect-flash for flash messages stored in session
102 |
103 |
104 | // ======================================================================
105 | // Setup routes
106 | require('./app/routes.js')(app, controllers);
107 |
108 |
109 | // ======================================================================
110 | // Launch
111 | app.listen(port);
112 |
113 | if (process.env.NODE_ENV !== "production") {
114 | console.log('Listening on port ' + port);
115 | }
116 |
--------------------------------------------------------------------------------
/specs/components/index.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TestUtils from 'react/lib/ReactTestUtils';
3 | import Index from '../../client/js/components/index';
4 | import StubRouterContext from '../support/stub_router_context';
5 |
6 | describe('index', function() {
7 | it('renders the main chrome', function() {
8 |
9 | // Render component wrapped in router context
10 | // var Subject = StubRouterContext(Index, {});
11 |
12 | // var result = TestUtils.renderIntoDocument();
13 |
14 | // expect(result).toBeDefined();
15 |
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/specs/components/main/about.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TestUtils from 'react/lib/ReactTestUtils';
3 | import About from '../../../client/js/components/main/about';
4 | import StubRouterContext from '../../support/stub_router_context';
5 |
6 | describe('about', function() {
7 | it('renders the main chrome', function() {
8 |
9 | // Render component wrapped in router context
10 | // var Subject = StubRouterContext(Index, {});
11 |
12 | // var result = TestUtils.renderIntoDocument();
13 |
14 | // expect(result).toBeDefined();
15 |
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/specs/components/not_found.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TestUtils from 'react/lib/ReactTestUtils';
3 | import NotFound from '../../client/js/components/not_found';
4 |
5 | describe('not_found', function() {
6 | it('renders a not found message', function() {
7 |
8 | var result = TestUtils.renderIntoDocument();
9 | expect(result.getDOMNode().textContent).toEqual('Not Found');
10 |
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/specs/components/sessions/login.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TestUtils from 'react/lib/ReactTestUtils';
3 | import Login from '../../../client/js/components/sessions/login';
4 | import StubRouterContext from '../../support/stub_router_context';
5 | import Utils from '../../support/utils';
6 |
7 | describe('login', function() {
8 | var login;
9 | var Subject;
10 | var textFields;
11 |
12 | beforeEach(function() {
13 | // Render component wrapped in router context
14 | Subject = StubRouterContext(Login, {});
15 | login = TestUtils.renderIntoDocument();
16 | textFields = TestUtils.scryRenderedDOMComponentsWithClass(login, 'mui-text-field');
17 | });
18 |
19 | it('renders the login component', function() {
20 | expect(login).toBeDefined();
21 |
22 | var email = Utils.findTextField(textFields, 'email');
23 | expect(email).toBeDefined();
24 |
25 | var password = Utils.findTextField(textFields, 'password');
26 | expect(password).toBeDefined();
27 | });
28 |
29 | it('outputs a validation error if no email is provided', function(){
30 | // var button = TestUtils.findRenderedDOMComponentWithClass(login, 'login-button');
31 | // TestUtils.Simulate.click(button);
32 |
33 | var form = TestUtils.findRenderedDOMComponentWithTag(login, 'form');
34 | TestUtils.Simulate.submit(form);
35 |
36 | var email = Utils.findTextField(textFields, 'email');
37 | expect(email.getDOMNode().className).toContain('mui-has-error');
38 | expect(login.getDOMNode().textContent).toContain('Invalid email');
39 | });
40 |
41 | });
42 |
--------------------------------------------------------------------------------
/specs/components/users/register.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from 'react';
4 | import TestUtils from 'react/lib/ReactTestUtils';
5 | import Register from '../../../client/js/components/users/register';
6 | import StubRouterContext from '../../support/stub_router_context';
7 | import Utils from '../../support/utils';
8 | import UserActions from '../../../client/js/actions/user';
9 |
10 | describe('register', function() {
11 | var register;
12 | var Subject;
13 | var textFields;
14 |
15 | beforeEach(function() {
16 | // Render component wrapped in router context
17 | Subject = StubRouterContext(Register, {});
18 | register = TestUtils.renderIntoDocument();
19 | textFields = TestUtils.scryRenderedDOMComponentsWithClass(register, 'mui-text-field');
20 | });
21 |
22 | it('renders the register component', function() {
23 | expect(register).toBeDefined();
24 |
25 | var textFields = TestUtils.scryRenderedDOMComponentsWithClass(register, 'mui-text-field');
26 |
27 | var email = Utils.findTextField(textFields, 'email');
28 | expect(email).toBeDefined();
29 |
30 | var password = Utils.findTextField(textFields, 'password');
31 | expect(password).toBeDefined();
32 |
33 | var confirmPassword = Utils.findTextField(textFields, 'confirm password');
34 | expect(confirmPassword).toBeDefined();
35 | });
36 |
37 | it('submits the login via the button', function(){
38 | // In the application clicking the button submits the form even though it's not a submit button
39 | // Need to figure out why and then add an appropriate test for submitting via the button
40 | //var button = TestUtils.findRenderedDOMComponentWithClass(register, 'sign-up-button');
41 | //This only triggers the onTouchTap event for the button, not the form submit.
42 | //TestUtils.Simulate.click(button.getDOMNode());
43 | });
44 |
45 | it('outputs a validation error if no email is provided', function(){
46 | var form = TestUtils.findRenderedDOMComponentWithTag(register, 'form');
47 | TestUtils.Simulate.submit(form);
48 |
49 | var email = Utils.findTextField(textFields, 'email');
50 | expect(email.getDOMNode().className).toContain('mui-has-error');
51 | expect(register.getDOMNode().textContent).toContain('Invalid email');
52 | });
53 |
54 | it('clears the email error after the user enters a valid email', function(){
55 |
56 | // Submit the form to put it into an invalid state
57 | var form = TestUtils.findRenderedDOMComponentWithTag(register, 'form');
58 | TestUtils.Simulate.submit(form);
59 |
60 | // Find the email material ui component and it's input field
61 | var email = Utils.findTextField(textFields, 'email');
62 | var emailInput = TestUtils.findRenderedDOMComponentWithTag(email, 'input');
63 |
64 | // Set a valid email and blur the field
65 | emailInput.getDOMNode().value = "johndoe@example.com";
66 | TestUtils.Simulate.blur(emailInput);
67 |
68 | // Test to make sure the email is now valid
69 | expect(email.getDOMNode().className).not.toContain('mui-has-error');
70 | expect(register.getDOMNode().textContent).not.toContain('Invalid email');
71 | });
72 |
73 | it('ensures the password is at least 5 chars', function(){
74 | var password = Utils.findTextField(textFields, 'password');
75 | var passwordInput = TestUtils.findRenderedDOMComponentWithTag(password, 'input');
76 |
77 | passwordInput.getDOMNode().value = "test";
78 | TestUtils.Simulate.blur(passwordInput.getDOMNode());
79 |
80 | expect(register.getDOMNode().textContent).toContain("at least 5 characters");
81 |
82 | });
83 |
84 | it('clears the password error after the user enters a valid password', function(){
85 | var password = Utils.findTextField(textFields, 'password');
86 | var passwordInput = TestUtils.findRenderedDOMComponentWithClass(password, 'mui-text-field-input');
87 |
88 | TestUtils.Simulate.blur(passwordInput.getDOMNode());
89 |
90 | expect(password.getDOMNode().className).toContain('mui-has-error');
91 | expect(password.getDOMNode().textContent).toContain('Password must be at least 5 characters');
92 |
93 | passwordInput.getDOMNode().value = "aoeuaoeu";
94 |
95 | TestUtils.Simulate.blur(passwordInput.getDOMNode());
96 |
97 | expect(password.getDOMNode().className).not.toContain('mui-has-error');
98 | expect(password.getDOMNode().textContent).not.toContain('Password must be at least 5 characters');
99 |
100 | });
101 |
102 | it('ensures the password confirmation matches', function(){
103 | var form = TestUtils.findRenderedDOMComponentWithTag(register, 'form');
104 | var password = Utils.findTextField(textFields, 'password');
105 | var passwordInput = TestUtils.findRenderedDOMComponentWithClass(password, 'mui-text-field-input');
106 | var confirmPassword = Utils.findTextField(textFields, 'confirm password');
107 | var confirmPasswordInput = TestUtils.findRenderedDOMComponentWithClass(confirmPassword, 'mui-text-field-input');
108 | var email = Utils.findTextField(textFields, 'email');
109 | var emailInput = TestUtils.findRenderedDOMComponentWithTag(email, 'input');
110 | var expectedRegisterObject ={
111 | email: "johndoe@example.com",
112 | password: "aoeuaoeu",
113 | badpassword: "asdfasdf"
114 | };
115 |
116 | emailInput.getDOMNode().value = expectedRegisterObject.email;
117 | passwordInput.getDOMNode().value = expectedRegisterObject.password;
118 | confirmPasswordInput.getDOMNode().value = expectedRegisterObject.badpassword;
119 | TestUtils.Simulate.blur(confirmPasswordInput);
120 |
121 | // Test to make sure the password is not valid
122 | expect(confirmPassword.getDOMNode().className).toContain('mui-has-error');
123 | expect(register.getDOMNode().textContent).toContain('Passwords do not match');
124 |
125 | confirmPasswordInput.getDOMNode().value = expectedRegisterObject.password;
126 |
127 | TestUtils.Simulate.blur(confirmPasswordInput.getDOMNode());
128 |
129 | // Test to make sure the password is now valid
130 | expect(confirmPassword.getDOMNode().className).not.toContain('mui-has-error');
131 | expect(register.getDOMNode().textContent).not.toContain('Passwords do not match');
132 | });
133 |
134 | it('Doesn\'t allow form submission if there are validation errors', function(){
135 | //arrange
136 | var form = TestUtils.findRenderedDOMComponentWithTag(register, 'form');
137 | var password = Utils.findTextField(textFields, 'password');
138 | var passwordInput = TestUtils.findRenderedDOMComponentWithClass(password, 'mui-text-field-input');
139 | var confirmPassword = Utils.findTextField(textFields, 'confirm password');
140 | var confirmPasswordInput = TestUtils.findRenderedDOMComponentWithClass(confirmPassword, 'mui-text-field-input');
141 | var email = Utils.findTextField(textFields, 'email');
142 | var emailInput = TestUtils.findRenderedDOMComponentWithTag(email, 'input');
143 | var badRegisterObject ={
144 | email: "johndoe",
145 | password: "asdf"
146 | };
147 |
148 | emailInput.getDOMNode().value = badRegisterObject.email;
149 | passwordInput.getDOMNode().value = badRegisterObject.password;
150 | confirmPasswordInput.getDOMNode().value = badRegisterObject.password;
151 | spyOn(UserActions, 'register');
152 |
153 | //act
154 | TestUtils.Simulate.submit(form);
155 |
156 | //assert
157 | expect(email.getDOMNode().className).toContain('mui-has-error');
158 | expect(register.getDOMNode().textContent).toContain('Invalid email');
159 | expect(register.getDOMNode().textContent).toContain("at least 5 characters");
160 | expect(UserActions.register).not.toHaveBeenCalledWith(badRegisterObject);
161 | });
162 |
163 | it('submits the form if all fields are valid', function(){
164 | //arrange
165 | var form = TestUtils.findRenderedDOMComponentWithTag(register, 'form');
166 | var password = Utils.findTextField(textFields, 'password');
167 | var passwordInput = TestUtils.findRenderedDOMComponentWithClass(password, 'mui-text-field-input');
168 | var confirmPassword = Utils.findTextField(textFields, 'confirm password');
169 | var confirmPasswordInput = TestUtils.findRenderedDOMComponentWithClass(confirmPassword, 'mui-text-field-input');
170 | var email = Utils.findTextField(textFields, 'email');
171 | var emailInput = TestUtils.findRenderedDOMComponentWithTag(email, 'input');
172 | var expectedRegisterObject ={
173 | email: "johndoe@example.com",
174 | password: "aoeuaoeu"
175 | };
176 |
177 | emailInput.getDOMNode().value = expectedRegisterObject.email;
178 | passwordInput.getDOMNode().value = expectedRegisterObject.password;
179 | confirmPasswordInput.getDOMNode().value = expectedRegisterObject.password;
180 | spyOn(UserActions, 'register');
181 |
182 | //act
183 | TestUtils.Simulate.submit(form);
184 |
185 | //assert
186 | expect(UserActions.register).toHaveBeenCalledWith(expectedRegisterObject);
187 | });
188 |
189 | });
190 |
--------------------------------------------------------------------------------
/specs/routes.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Router from 'react-router';
3 |
4 | import routes from '../client/js/routes';
5 |
6 | var Route = Router.Route;
7 |
8 | describe('default route', function () {
9 | it('renders home', function (done) {
10 | Router.run(routes, '/', function (Handler, state){
11 | var html = React.renderToString();
12 | expect(html).toMatch(/Home/);
13 | done();
14 | });
15 | });
16 | });
17 |
18 | describe('about route', function () {
19 | it('renders about', function (done) {
20 | Router.run(routes, '/about', function (Handler, state){
21 | var html = React.renderToString();
22 | expect(html).toMatch(/About/);
23 | done();
24 | });
25 | });
26 | });
27 |
28 | describe('login route', function(){
29 | it('renders login', function (done) {
30 | Router.run(routes, '/login', function (Handler, state) {
31 | var html = React.renderToString();
32 | expect(html).toMatch(/Login/);
33 | done();
34 | });
35 | });
36 | });
37 |
38 | describe('register route', function(){
39 | it('renders register', function (done) {
40 | Router.run(routes, '/register', function (Handler, state) {
41 | var html = React.renderToString();
42 | expect(html).toMatch(/Signup/);
43 | done();
44 | });
45 | });
46 | });
47 |
48 | describe('logout route', function(){
49 | it('renders logout', function (done) {
50 | Router.run(routes, '/logout', function (Handler, state) {
51 | var html = React.renderToString();
52 | expect(html).toMatch(/Logout/);
53 | done();
54 | });
55 | });
56 | });
57 |
58 | describe('dashboard route', function(){
59 | it('renders dashboard', function (done) {
60 | Router.run(routes, '/dashboard', function (Handler, state) {
61 | var html = React.renderToString();
62 | expect(html).toMatch(/Dashboard/);
63 | done();
64 | });
65 | });
66 | });
67 |
68 | describe('connections route', function(){
69 | it('renders connections', function (done) {
70 | Router.run(routes, '/connections', function (Handler, state) {
71 | var html = React.renderToString();
72 | expect(html).toMatch(/Connections/);
73 | done();
74 | });
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/specs/spec_helper.js:
--------------------------------------------------------------------------------
1 | // Test helper. Load libraries and polyfills
2 |
3 | // Function.prototype.bind polyfill used by PhantomJS
4 | if (typeof Function.prototype.bind != 'function') {
5 | Function.prototype.bind = function bind(obj) {
6 | var args = Array.prototype.slice.call(arguments, 1),
7 | self = this,
8 | nop = function() {
9 | },
10 | bound = function() {
11 | return self.apply(
12 | this instanceof nop ? this : (obj || {}), args.concat(
13 | Array.prototype.slice.call(arguments)
14 | )
15 | );
16 | };
17 | nop.prototype = this.prototype || {};
18 | bound.prototype = new nop();
19 | return bound;
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/specs/stores/messages.spec.js:
--------------------------------------------------------------------------------
1 | //jest.dontMock('Messages');
--------------------------------------------------------------------------------
/specs/stores/user.spec.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbasdf/react-kindling/cfc53eb47bf969eba5782b4d03d146532d4af754/specs/stores/user.spec.js
--------------------------------------------------------------------------------
/specs/support/stub_router_context.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | import React from "react";
4 | import assign from "object-assign";
5 |
6 | var { func } = React.PropTypes;
7 |
8 | export default (Component, props, stubs) => {
9 | function RouterStub() { }
10 |
11 | assign(RouterStub, {
12 | makePath () {},
13 | makeHref () {},
14 | transitionTo () {},
15 | replaceWith () {},
16 | goBack () {},
17 | getCurrentPath () {},
18 | getCurrentRoutes () {},
19 | getCurrentPathname () {},
20 | getCurrentParams () {},
21 | getCurrentQuery () {},
22 | isActive () {},
23 | getRouteAtDepth() {},
24 | setRouteComponentAtDepth() {}
25 | }, stubs);
26 |
27 | return React.createClass({
28 | childContextTypes: {
29 | router: React.PropTypes.func,
30 | routeDepth: React.PropTypes.number
31 | },
32 |
33 | getChildContext () {
34 | return {
35 | router: RouterStub,
36 | routeDepth: 0
37 | };
38 | },
39 |
40 | render () {
41 | return ;
42 | }
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/specs/support/utils.js:
--------------------------------------------------------------------------------
1 | import TestUtils from 'react/lib/ReactTestUtils';
2 | import _ from "lodash";
3 |
4 | export default {
5 |
6 | findTextField(textFields, labelText){
7 | return _.find(textFields, function(field){
8 | var label = TestUtils.findRenderedDOMComponentWithTag(field, 'label');
9 | return label.getDOMNode().textContent.toLowerCase() == labelText;
10 | });
11 | }
12 |
13 | };
14 |
--------------------------------------------------------------------------------
/specs/tests.webpack.js:
--------------------------------------------------------------------------------
1 | // require all modules ending in ".spec.js" from the
2 | // current directory and all subdirectories
3 | var context = require.context(".", true, /\.spec\.js$/);
4 | context.keys().forEach(context);
--------------------------------------------------------------------------------
/webpack_server.js:
--------------------------------------------------------------------------------
1 | var webpackDevServer = require('webpack-dev-server');
2 | var webpack = require('webpack');
3 | var webpackConfig = require('./config/webpack.config.js')(false);
4 | var settings = require('./config/settings.js');
5 |
6 | console.log('Starting Webpack hot load server');
7 |
8 | new webpackDevServer(webpack(webpackConfig), {
9 | //contentBase: 'http://localhost:' + settings.ports.hotPort,
10 | publicPath: webpackConfig.output.publicPath,
11 | hot: true,
12 | noInfo: true,
13 | headers: { "Access-Control-Allow-Origin": "*" }
14 | }).listen(settings.ports.hotPort, 'localhost', function(err, result){
15 | if(err){
16 | throw new gutil.PluginError('webpack-dev-server', err);
17 | }
18 | console.log('Webpack hot load server listening on port ' + settings.ports.hotPort);
19 | });
--------------------------------------------------------------------------------