├── .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 |
      51 | {messages} 52 |
    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 |
    37 | 38 |
    39 | 40 |
    41 |
    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 |
    56 |

    Login

    57 | 58 | 59 | 60 | 61 | 62 | 63 | 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 (
    9 |
    10 |
    11 | 12 | 13 |
    14 |
    15 | 16 | 17 |
    18 | 19 |
    20 |
    ); 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 |
    75 | 76 | 77 | 78 | 79 | 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 |
    17 |
    18 | 19 | 20 | 21 | icon-google 22 |
    23 |
    24 | 25 | 26 |
    27 |
    28 | liga: 29 | 30 |
    31 |
    32 |
    33 |
    34 | 35 | 36 | 37 | icon-facebook 38 |
    39 |
    40 | 41 | 42 |
    43 |
    44 | liga: 45 | 46 |
    47 |
    48 |
    49 |
    50 | 51 | 52 | 53 | icon-twitter 54 |
    55 |
    56 | 57 | 58 |
    59 |
    60 | liga: 61 | 62 |
    63 |
    64 |
    65 |
    66 | 67 | 68 | 69 | icon-github 70 |
    71 |
    72 | 73 | 74 |
    75 |
    76 | liga: 77 | 78 |
    79 |
    80 |
    81 | 82 | 83 |
    84 |

    Font Test Drive

    85 | 90 | 92 | 93 |
      94 |
    95 |
    96 | 97 |
    98 |

    Generated by IcoMoon

    99 |
    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 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | }); --------------------------------------------------------------------------------