├── Backend ├── .babelrc ├── .gitignore ├── api │ ├── auth │ │ ├── index.js │ │ └── index.test.js │ ├── contact │ │ ├── index.js │ │ └── index.test.js │ └── index.js ├── app.js ├── express.js ├── index.js ├── package.json └── test │ └── setup.js ├── FrontEnd ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .lgtm ├── .travis.yml ├── app │ ├── .htaccess │ ├── .nginx.conf │ ├── app.js │ ├── components │ │ ├── Footer │ │ │ └── index.js │ │ └── Header │ │ │ └── index.js │ ├── containers │ │ ├── App │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── sagas.js │ │ │ └── selectors.js │ │ ├── Contact │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── reducer.js │ │ │ ├── sagas.js │ │ │ └── selectors.js │ │ ├── HomePage │ │ │ ├── index.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ ├── LanguageProvider │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── reducer.js │ │ │ └── selectors.js │ │ ├── Login │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── reducer.js │ │ │ ├── sagas.js │ │ │ ├── selectors.js │ │ │ └── tests │ │ │ │ ├── actions.test.js │ │ │ │ ├── index.test.js │ │ │ │ └── reducer.test.js │ │ ├── NotFoundPage │ │ │ ├── index.js │ │ │ └── messages.js │ │ └── Users │ │ │ ├── actions.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── reducer.js │ │ │ ├── sagas.js │ │ │ └── selectors.js │ ├── favicon.ico │ ├── global-styles.js │ ├── i18n.js │ ├── img │ │ └── intro-bg.jpg │ ├── index.html │ ├── manifest.json │ ├── reducers.js │ ├── routes.js │ ├── store.js │ ├── tests │ │ └── store.test.js │ ├── translations │ │ └── en.json │ └── utils │ │ ├── AuthWrapper.js │ │ ├── asyncInjectors.js │ │ ├── request.js │ │ └── tests │ │ └── asyncInjectors.test.js ├── appveyor.yml ├── internals │ ├── config.js │ ├── generators │ │ ├── component │ │ │ ├── es6.js.hbs │ │ │ ├── es6.pure.js.hbs │ │ │ ├── index.js │ │ │ ├── messages.js.hbs │ │ │ ├── stateless.js.hbs │ │ │ └── test.js.hbs │ │ ├── container │ │ │ ├── actions.js.hbs │ │ │ ├── actions.test.js.hbs │ │ │ ├── constants.js.hbs │ │ │ ├── index.js │ │ │ ├── index.js.hbs │ │ │ ├── messages.js.hbs │ │ │ ├── reducer.js.hbs │ │ │ ├── reducer.test.js.hbs │ │ │ ├── sagas.js.hbs │ │ │ ├── sagas.test.js.hbs │ │ │ ├── selectors.js.hbs │ │ │ ├── selectors.test.js.hbs │ │ │ └── test.js.hbs │ │ ├── index.js │ │ ├── language │ │ │ ├── add-locale-data.hbs │ │ │ ├── app-locale.hbs │ │ │ ├── format-translation-messages.hbs │ │ │ ├── index.js │ │ │ ├── intl-locale-data.hbs │ │ │ ├── polyfill-intl-locale.hbs │ │ │ ├── translation-messages.hbs │ │ │ └── translations-json.hbs │ │ ├── route │ │ │ ├── index.js │ │ │ ├── route.hbs │ │ │ └── routeWithReducer.hbs │ │ └── utils │ │ │ └── componentExists.js │ ├── scripts │ │ ├── analyze.js │ │ ├── clean.js │ │ ├── dependencies.js │ │ ├── extract-intl.js │ │ ├── helpers │ │ │ ├── checkmark.js │ │ │ └── progress.js │ │ ├── npmcheckversion.js │ │ ├── pagespeed.js │ │ └── setup.js │ ├── testing │ │ ├── karma.conf.js │ │ └── test-bundler.js │ └── webpack │ │ ├── webpack.base.babel.js │ │ ├── webpack.dev.babel.js │ │ ├── webpack.dll.babel.js │ │ ├── webpack.prod.babel.js │ │ └── webpack.test.babel.js ├── package.json ├── server │ ├── index.js │ ├── logger.js │ └── middlewares │ │ └── frontendMiddleware.js └── yarn.lock ├── README.md └── banner.png /Backend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-1" 5 | ], 6 | "plugins": [ 7 | "transform-runtime" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /Backend/api/auth/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | var jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens 3 | const router = new Router() 4 | 5 | const secret = 'superSecret' 6 | 7 | export const require_auth = (req, res, next) =>{ 8 | // check header or url parameters or post parameters for token 9 | var token = req.body.token || req.params.token || req.headers['x-access-token']; 10 | // decode token 11 | if (token) { 12 | // verifies secret and checks exp 13 | jwt.verify(token, secret, function(err, decoded) { 14 | if (err) { 15 | return res.json({ success: false, message: 'Failed to authenticate token.' }); 16 | } else { 17 | // if everything is good, save to request for use in other routes 18 | req.decoded = decoded; 19 | next(); 20 | } 21 | }); 22 | } else { 23 | // if there is no token 24 | // return an error 25 | return res.status(403).send({ 26 | success: false, 27 | message: 'No token provided.' 28 | }); 29 | } 30 | } 31 | 32 | router.post('/',function(req, res) { 33 | var user = { 34 | email : 'john@doe.com', 35 | password: 'secret', 36 | name: 'John Doe', 37 | picture: 'https://api.adorable.io/avatars/285/abott@adorable.io.png' 38 | } 39 | if (user.email != req.body.email) { 40 | res.json({ success: false, message: 'Authentication failed. User not found.' }); 41 | } else if (user) { 42 | // check if password matches 43 | if (user.password != req.body.password) { 44 | res.json({ success: false, message: 'Authentication failed. Wrong password.' }); 45 | } else { 46 | // if user is found and password is right 47 | // create a token 48 | var token = jwt.sign(user, secret, { 49 | expiresIn: 86400 // expires in 24 hours 50 | }); 51 | 52 | res.json({ 53 | success: true, 54 | data: { email: user.email, name: user.name, picture: user.picture}, 55 | token: token 56 | }); 57 | } 58 | } 59 | }); 60 | router.get('/', require_auth, function(req, res) { 61 | 62 | res.json({ 63 | success: true, 64 | data: req.decoded 65 | }) 66 | }); 67 | 68 | 69 | export default router 70 | -------------------------------------------------------------------------------- /Backend/api/auth/index.test.js: -------------------------------------------------------------------------------- 1 | import { stub } from 'sinon' 2 | import request from 'supertest-as-promised' 3 | var jwt = require('jsonwebtoken'); // used to create, sign, and verify tokens 4 | 5 | import express from '../../express' 6 | import routes from '.' 7 | const secret = 'superSecret' 8 | 9 | const app = () => express(routes) 10 | 11 | var token=null; 12 | 13 | test('POST /auth 200 ', async () => { 14 | const { status, body } = await request(app()) 15 | .post('/') 16 | .send({ email: 'john@doe.com', password: 'secret' }) 17 | expect(status).toBe(200) 18 | expect(typeof body).toBe('object') 19 | expect(typeof body.token).toBe('string') 20 | token = body.token; 21 | expect(typeof body.data).toBe('object') 22 | expect(await jwt.verify(body.token,secret)).toBeTruthy() 23 | }) 24 | 25 | test('POST /auth - invalid email', async () => { 26 | const { status, body } = await request(app()) 27 | .post('/') 28 | .send({ email: 'john@doe.comm', password: 'secret' }) 29 | expect(status).toBe(200) 30 | expect(typeof body).toBe('object') 31 | expect(body.success).toBe(false) 32 | }) 33 | 34 | test('GET /auth - invalid token', async () => { 35 | const { status, body } = await request(app()) 36 | .get('/') 37 | .query({ token: '' }) 38 | expect(status).toBe(403) 39 | }) 40 | 41 | test('GET /auth - valid token', async () => { 42 | const { status, body } = await request(app()) 43 | .get('/') 44 | .send({ token: token }) 45 | expect(status).toBe(200) 46 | }) 47 | -------------------------------------------------------------------------------- /Backend/api/contact/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | const router = new Router() 4 | 5 | router.post('/',function(req, res) { 6 | res.json({ 7 | success: true, 8 | data: req.body 9 | }) 10 | }); 11 | 12 | 13 | export default router 14 | -------------------------------------------------------------------------------- /Backend/api/contact/index.test.js: -------------------------------------------------------------------------------- 1 | import { stub } from 'sinon' 2 | import request from 'supertest-as-promised' 3 | import express from '../../express' 4 | import routes from '.' 5 | const app = () => express(routes) 6 | 7 | test('POST /contact 200 ', async () => { 8 | const { status, body } = await request(app()) 9 | .post('/') 10 | .send({ email: 'john@doe.com', name: 'name', message:'message' }) 11 | expect(status).toBe(200) 12 | expect(typeof body).toBe('object') 13 | expect(typeof body.data).toBe('object') 14 | }) 15 | -------------------------------------------------------------------------------- /Backend/api/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | import auth from './auth' 4 | import contact from './contact' 5 | 6 | 7 | const router = new Router() 8 | 9 | /** 10 | * @apiDefine master Master access only 11 | * You must pass `access_token` parameter or a Bearer Token authorization header 12 | * to access this endpoint. 13 | */ 14 | /** 15 | * @apiDefine admin Admin access only 16 | * You must pass `access_token` parameter or a Bearer Token authorization header 17 | * to access this endpoint. 18 | */ 19 | /** 20 | * @apiDefine user User access only 21 | * You must pass `access_token` parameter or a Bearer Token authorization header 22 | * to access this endpoint. 23 | */ 24 | /** 25 | * @apiDefine listParams 26 | * @apiParam {String} [q] Query to search. 27 | * @apiParam {Number{1..30}} [page=1] Page number. 28 | * @apiParam {Number{1..100}} [limit=30] Amount of returned items. 29 | * @apiParam {String[]} [sort=-createdAt] Order of returned items. 30 | * @apiParam {String[]} [fields] Fields to be returned. 31 | */ 32 | router.use('/auth', auth) 33 | router.use('/contact', contact) 34 | 35 | 36 | export default router 37 | -------------------------------------------------------------------------------- /Backend/app.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | var ip = '127.0.0.1'; 3 | var port = 9000; 4 | import express from './express'; 5 | import api from './api'; 6 | 7 | var app = express(api); 8 | const server = http.createServer(app); 9 | 10 | setImmediate(() => { 11 | server.listen(port, ip, () => { 12 | console.log('Express server listening on http://%s:%d', ip, port) 13 | }) 14 | }); 15 | 16 | export default app 17 | -------------------------------------------------------------------------------- /Backend/express.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import cors from 'cors' 3 | import compression from 'compression' 4 | import morgan from 'morgan' 5 | import bodyParser from 'body-parser' 6 | 7 | export default (routes) => { 8 | const app = express() 9 | app.use(cors()) 10 | app.use(compression()) 11 | app.use(morgan('dev')) 12 | app.use(bodyParser.urlencoded({ extended: false })) 13 | app.use(bodyParser.json()) 14 | app.use(routes) 15 | 16 | return app 17 | } 18 | -------------------------------------------------------------------------------- /Backend/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register') 2 | 3 | exports = module.exports = require('./app') 4 | -------------------------------------------------------------------------------- /Backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "main": "index.js", 4 | "scripts": { 5 | "test": "jest", 6 | "coverage": "npm test -- --coverage && opn coverage/lcov-report/index.html", 7 | "start": "nodemon -i '*.test.js' .", 8 | "prod": "cross-env NODE_ENV=production nodemon index" 9 | }, 10 | "jest": { 11 | "testEnvironment": "node", 12 | "setupTestFrameworkScriptFile": "./test/setup.js" 13 | }, 14 | "dependencies": { 15 | "babel-core": "^6.21.0", 16 | "babel-plugin-transform-runtime": "^6.15.0", 17 | "babel-preset-es2015": "^6.18.0", 18 | "babel-preset-stage-1": "^6.16.0", 19 | "babel-runtime": "^6.20.0", 20 | "body-parser": "^1.15.2", 21 | "compression": "^1.6.2", 22 | "cors": "^2.8.1", 23 | "cross-env": "^3.1.4", 24 | "express": "^4.13.4", 25 | "http": "0.0.0", 26 | "jsonwebtoken": "^5.7.0", 27 | "mongoose": "^4.4.5", 28 | "morgan": "^1.7.0" 29 | }, 30 | "devDependencies": { 31 | "babel-eslint": "^7.1.1", 32 | "babel-jest": "^18.0.0", 33 | "jest-cli": "^18.1.0", 34 | "nodemon": "^1.11.0", 35 | "sinon": "^1.17.7", 36 | "supertest": "^2.0.1", 37 | "supertest-as-promised": "^4.0.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Backend/test/setup.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | 3 | EventEmitter.defaultMaxListeners = Infinity 4 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000 5 | 6 | global.Array = Array 7 | global.Date = Date 8 | global.Function = Function 9 | global.Math = Math 10 | global.Number = Number 11 | global.Object = Object 12 | global.RegExp = RegExp 13 | global.String = String 14 | global.Uint8Array = Uint8Array 15 | global.WeakMap = WeakMap 16 | global.Set = Set 17 | global.Error = Error 18 | global.TypeError = TypeError 19 | global.parseInt = parseInt 20 | global.parseFloat = parseFloat 21 | -------------------------------------------------------------------------------- /FrontEnd/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /FrontEnd/.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | .gitconfig text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | 73 | # Heroku 74 | Procfile text 75 | .slugignore text 76 | 77 | # Documentation 78 | *.md text 79 | LICENSE text 80 | AUTHORS text 81 | 82 | 83 | # 84 | ## These files are binary and should be left untouched 85 | # 86 | 87 | # (binary is a macro for -text -diff) 88 | *.png binary 89 | *.jpg binary 90 | *.jpeg binary 91 | *.gif binary 92 | *.ico binary 93 | *.mov binary 94 | *.mp4 binary 95 | *.mp3 binary 96 | *.flv binary 97 | *.fla binary 98 | *.swf binary 99 | *.gz binary 100 | *.zip binary 101 | *.7z binary 102 | *.ttf binary 103 | *.eot binary 104 | *.woff binary 105 | *.pyc binary 106 | *.pdf binary 107 | -------------------------------------------------------------------------------- /FrontEnd/.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /FrontEnd/.lgtm: -------------------------------------------------------------------------------- 1 | approvals = 2 2 | pattern = "(?i):shipit:|LGTM" 3 | self_approval_off = true 4 | -------------------------------------------------------------------------------- /FrontEnd/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 6 5 | - 5 6 | - 4 7 | 8 | script: npm run build 9 | 10 | install: 11 | - npm i -g npm@latest 12 | - npm install 13 | 14 | before_install: 15 | - export CHROME_BIN=chromium-browser 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | 19 | notifications: 20 | email: 21 | on_failure: change 22 | 23 | after_success: 'npm run coveralls' 24 | 25 | cache: 26 | directories: 27 | - node_modules 28 | -------------------------------------------------------------------------------- /FrontEnd/app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | -------------------------------------------------------------------------------- /FrontEnd/app/.nginx.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have line 'include /etc/nginx/conf.d/*.conf;' 4 | # in your main nginx configuration file 5 | ## 6 | 7 | ## 8 | # Redirect to the same URL with https:// 9 | ## 10 | 11 | server { 12 | 13 | listen 80; 14 | 15 | # Type your domain name below 16 | server_name example.com; 17 | 18 | return 301 https://$server_name$request_uri; 19 | 20 | } 21 | 22 | ## 23 | # HTTPS configurations 24 | ## 25 | 26 | server { 27 | 28 | listen 443; 29 | 30 | # Type your domain name below 31 | server_name example.com; 32 | 33 | ssl on; 34 | ssl_certificate /path/to/certificate.crt; 35 | ssl_certificate_key /path/to/server.key; 36 | 37 | # Use only TSL protocols for more secure 38 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 39 | 40 | # Always serve index.html for any request 41 | location / { 42 | # Set path 43 | root /var/www/; 44 | try_files $uri /index.html; 45 | } 46 | 47 | ## 48 | # If you want to use Node/Rails/etc. API server 49 | # on the same port (443) config Nginx as a reverse proxy. 50 | # For security reasons use a firewall like ufw in Ubuntu 51 | # and deny port 3000/tcp. 52 | ## 53 | 54 | # location /api/ { 55 | # 56 | # proxy_pass http://localhost:3000; 57 | # proxy_http_version 1.1; 58 | # proxy_set_header X-Forwarded-Proto https; 59 | # proxy_set_header Upgrade $http_upgrade; 60 | # proxy_set_header Connection 'upgrade'; 61 | # proxy_set_header Host $host; 62 | # proxy_cache_bypass $http_upgrade; 63 | # 64 | # } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /FrontEnd/app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * This is the entry file for the application, only setup and boilerplate 5 | * code. 6 | */ 7 | import 'babel-polyfill'; 8 | 9 | /* eslint-disable import/no-unresolved, import/extensions */ 10 | // Load the manifest.json file and the .htaccess file 11 | import '!file?name=[name].[ext]!./manifest.json'; 12 | import 'file?name=[name].[ext]!./.htaccess'; 13 | /* eslint-enable import/no-unresolved, import/extensions */ 14 | 15 | // Import all the third party stuff 16 | import React from 'react'; 17 | import ReactDOM from 'react-dom'; 18 | import { Provider } from 'react-redux'; 19 | import { applyRouterMiddleware, Router, browserHistory } from 'react-router'; 20 | import { syncHistoryWithStore } from 'react-router-redux'; 21 | import { useScroll } from 'react-router-scroll'; 22 | import LanguageProvider from 'containers/LanguageProvider'; 23 | import configureStore from './store'; 24 | 25 | // Import i18n messages 26 | import { translationMessages } from './i18n'; 27 | 28 | // Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder 29 | // import 'sanitize.css/sanitize.css'; 30 | import './global-styles'; 31 | // Create redux store with history 32 | // this uses the singleton browserHistory provided by react-router 33 | // Optionally, this could be changed to leverage a created history 34 | // e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` 35 | const initialState = {}; 36 | const store = configureStore(initialState, browserHistory); 37 | 38 | // Sync history and store, as the react-router-redux reducer 39 | // is under the non-default key ("routing"), selectLocationState 40 | // must be provided for resolving how to retrieve the "route" in the state 41 | import { selectLocationState } from 'containers/App/selectors'; 42 | const history = syncHistoryWithStore(browserHistory, store, { 43 | selectLocationState: selectLocationState(), 44 | }); 45 | 46 | // Set up the router, wrapping all Routes in the App component 47 | import App from 'containers/App'; 48 | import createRoutes from './routes'; 49 | const rootRoute = { 50 | component: App, 51 | childRoutes: createRoutes(store), 52 | }; 53 | 54 | 55 | const render = (translatedMessages) => { 56 | ReactDOM.render( 57 | 58 | 59 | 68 | 69 | , 70 | document.getElementById('app') 71 | ); 72 | }; 73 | 74 | 75 | // Hot reloadable translation json files 76 | if (module.hot) { 77 | // modules.hot.accept does not accept dynamic dependencies, 78 | // have to be constants at compile-time 79 | module.hot.accept('./i18n', () => { 80 | render(translationMessages); 81 | }); 82 | } 83 | 84 | // Chunked polyfill for browsers without Intl support 85 | if (!window.Intl) { 86 | (new Promise((resolve) => { 87 | resolve(System.import('intl')); 88 | })) 89 | .then(() => Promise.all([ 90 | System.import('intl/locale-data/jsonp/de.js'), 91 | ])) 92 | .then(() => render(translationMessages)) 93 | .catch((err) => { 94 | throw err; 95 | }); 96 | } else { 97 | render(translationMessages); 98 | } 99 | 100 | // Install ServiceWorker and AppCache in the end since 101 | // it's not most important operation and if main code fails, 102 | // we do not want it installed 103 | import { install } from 'offline-plugin/runtime'; 104 | install(); 105 | -------------------------------------------------------------------------------- /FrontEnd/app/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Footer 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | 10 | class Footer extends React.Component { // eslint-disable-line react/prefer-stateless-function 11 | render() { 12 | return ( 13 | 16 | ); 17 | } 18 | } 19 | 20 | export default Footer; 21 | -------------------------------------------------------------------------------- /FrontEnd/app/components/Header/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Header 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { push } from 'react-router-redux'; 10 | import { createStructuredSelector } from 'reselect'; 11 | import * as loginSelectors from '../../containers/Login/selectors'; 12 | import * as loginActions from '../../containers/Login/actions'; 13 | 14 | class Header extends React.Component { // eslint-disable-line react/prefer-stateless-function 15 | openRoute = (route) => { this.props.changeRoute(route); }; 16 | openHome = () => { 17 | this.openRoute('/'); 18 | } 19 | openUsers = () => { 20 | this.openRoute('/users'); 21 | } 22 | openContact = () => { 23 | this.openRoute('/contact'); 24 | } 25 | openLogin = () => { 26 | this.openRoute('/login'); 27 | } 28 | render() { 29 | let LoginLogout=null 30 | if(this.props.isAuthenticated){ 31 | LoginLogout = Logout, {this.props.selectName} 32 | }else{ 33 | LoginLogout = Login 34 | } 35 | return ( 36 | 68 | ); 69 | } 70 | } 71 | const mapStateToProps = createStructuredSelector({ 72 | selectName: loginSelectors.selectName(), 73 | isAuthenticated:loginSelectors.selectisAuthenticated() 74 | }); 75 | function mapDispatchToProps(dispatch) { 76 | return { 77 | changeRoute: (url) => dispatch(push(url)), 78 | logout: ()=> dispatch( loginActions.logout() ), 79 | dispatch, 80 | }; 81 | } 82 | 83 | export default connect(mapStateToProps, mapDispatchToProps)(Header); 84 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 3 | * Each action has a corresponding type, which the reducer knows and picks up on. 4 | * To avoid weird typos between the reducer and the actions, we save them as 5 | * constants here. We prefix them with 'yourproject/YourComponent' so we avoid 6 | * reducers accidentally picking up actions they shouldn't. 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT'; 10 | */ 11 | 12 | export const DEFAULT_LOCALE = 'en'; 13 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | * NOTE: while this component should technically be a stateless functional 9 | * component (SFC), hot reloading does not currently support SFCs. If hot 10 | * reloading is not a necessity for you then you can refactor it and remove 11 | * the linting exception. 12 | */ 13 | 14 | import React from 'react'; 15 | import Header from '../../components/Header'; 16 | import Footer from '../../components/Footer'; 17 | 18 | export default class App extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 19 | 20 | static propTypes = { 21 | children: React.PropTypes.node, 22 | }; 23 | 24 | render() { 25 | return ( 26 |
27 |
28 |
29 | {React.Children.toArray(this.props.children)} 30 |
32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/App/sagas.js: -------------------------------------------------------------------------------- 1 | 2 | import { takeLatest } from 'redux-saga'; 3 | import { take, call, put, select, fork, cancel } from 'redux-saga/effects'; 4 | import { LOCATION_CHANGE } from 'react-router-redux'; 5 | import * as loginActions from 'containers/Login/actions'; 6 | import { push } from 'react-router-redux' 7 | import request from 'utils/request'; 8 | 9 | export function* get_user_info(action) { 10 | const requestURL = '/api/auth'; 11 | const options = { 12 | method: 'get', 13 | headers: { 14 | 'Accept': 'application/json', 15 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 16 | 'x-access-token': action.token 17 | } 18 | } 19 | try { 20 | const { response, error } = yield call(request,requestURL, options); 21 | if (response){ 22 | if (response.success){ 23 | response.token = action.token; 24 | yield put(loginActions.loginUserSuccess(response)); 25 | yield put(push(action.redirect)); 26 | }else{ 27 | yield put(loginActions.loginUserFailure(response.message)); 28 | } 29 | 30 | }else{ 31 | yield put(loginActions.loginUserFailure(error)); 32 | } 33 | } catch (err) { 34 | yield put(loginActions.loginUserFailure(err)); 35 | } 36 | } 37 | /** 38 | * Root saga manages watcher lifecycle 39 | */ 40 | export function* rootSaga() { 41 | const watcher0 = yield takeLatest('GET_USER_INFO_REQUEST', get_user_info); 42 | 43 | } 44 | 45 | export default rootSaga; 46 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | // selectLocationState expects a plain JS object for the routing state 2 | const selectLocationState = () => { 3 | let prevRoutingState; 4 | let prevRoutingStateJS; 5 | 6 | return (state) => { 7 | const routingState = state.get('route'); // or state.route 8 | 9 | if (!routingState.equals(prevRoutingState)) { 10 | prevRoutingState = routingState; 11 | prevRoutingStateJS = routingState.toJS(); 12 | } 13 | 14 | return prevRoutingStateJS; 15 | }; 16 | }; 17 | 18 | export { 19 | selectLocationState, 20 | }; 21 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Contact/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Contact actions 4 | * 5 | */ 6 | 7 | import * as C from './constants'; 8 | 9 | export function sendContactInfoRequest(data) { 10 | return { 11 | type: C.SEND_CONTACT_INFO_REQUEST, 12 | data 13 | }; 14 | } 15 | export function sendContactInfoSuccess(data) { 16 | return { 17 | type: C.SEND_CONTACT_INFO_SUCCESS, 18 | data 19 | }; 20 | } 21 | export function sendContactInfoFailure(error) { 22 | return { 23 | type: C.SEND_CONTACT_INFO_FAILURE, 24 | error 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Contact/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Contact constants 4 | * 5 | */ 6 | 7 | 8 | export const SEND_CONTACT_INFO_REQUEST = 'SEND_CONTACT_INFO_REQUEST'; 9 | export const SEND_CONTACT_INFO_SUCCESS = 'SEND_CONTACT_INFO_SUCCESS'; 10 | export const SEND_CONTACT_INFO_FAILURE = 'SEND_CONTACT_INFO_FAILURE'; 11 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Contact/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Contact 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import selectContact from './selectors'; 10 | import { createStructuredSelector } from 'reselect'; 11 | import { bindActionCreators } from 'redux'; 12 | import * as actionCreators from './actions'; 13 | import * as loginActions from '../Login/actions'; 14 | import * as loginSelectors from '../Login/selectors'; 15 | 16 | 17 | export class Contact extends React.Component { // eslint-disable-line react/prefer-stateless-function 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | name:'', 22 | email:'', 23 | subject:'', 24 | message:'' 25 | }; 26 | } 27 | componentDidMount(){ 28 | if(sessionStorage.getItem('token') != null ){ 29 | if(!this.props.isAuthenticated){ 30 | var token = sessionStorage.getItem('token'); 31 | this.props.dispatch(loginActions.get_user_info(token,'/contact')); 32 | } 33 | } 34 | } 35 | handleNameChange = (event) => { this.setState({name: event.target.value}); } 36 | handleEmailChange = (event) => { this.setState({email: event.target.value}); } 37 | handleSubjectChange = (event) => { this.setState({subject: event.target.value}); } 38 | handleMessageChange = (event) => { this.setState({message: event.target.value}); } 39 | onSubmit = (e) => { 40 | e.preventDefault(); 41 | this.props.actions.sendContactInfoRequest(this.state ); 42 | }; 43 | render() { 44 | let statusText = null; 45 | if(this.props.selectContact.sendingError != false){ 46 | statusText=
{ this.props.selectContact.sendingError }
; 47 | }else{ 48 | if(this.props.selectContact.sentInfo){ 49 | statusText=
Form Sent!
; 50 | } 51 | } 52 | return ( 53 |
54 |
55 |
56 |
57 |

58 | Contact us Feel free to contact us

59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 72 | 73 |
74 |
75 | 77 |
78 | 79 | 80 |
81 |
82 |
83 | 85 | 86 |
87 |
88 |
89 |
90 | 92 | 94 |
95 |
96 |
97 | { statusText } 98 | 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |  Our office 108 |
109 | Twitter, Inc.
110 | 795 Folsom Ave, Suite 600
111 | San Francisco, CA 94107
112 | 113 | P: 114 | (123) 456-7890 115 |
116 |
117 | Full Name
118 | first.last@example.com 119 |
120 |
121 |
122 |
123 |
124 |
125 | ); 126 | } 127 | } 128 | 129 | // const mapStateToProps = selectContact(); 130 | 131 | function mapDispatchToProps(dispatch) { 132 | return { 133 | actions : bindActionCreators(actionCreators, dispatch), 134 | dispatch, 135 | }; 136 | } 137 | const mapStateToProps = createStructuredSelector({ 138 | selectContact: selectContact(), 139 | isAuthenticated:loginSelectors.selectisAuthenticated() 140 | }); 141 | export default connect(mapStateToProps, mapDispatchToProps)(Contact); 142 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Contact/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Contact reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import * as C from './constants'; 9 | 10 | const initialState = fromJS({ 11 | sendingInfo: false, 12 | sentInfo:false, 13 | sendingError:false 14 | }); 15 | 16 | function contactReducer(state = initialState, action) { 17 | switch (action.type) { 18 | case C.SEND_CONTACT_INFO_REQUEST: 19 | return state 20 | .set('sendingInfo', true) 21 | .set('errorText', false); 22 | case C.SEND_CONTACT_INFO_SUCCESS: 23 | return state 24 | .set('sendingInfo', false) 25 | .set('sentInfo', true) 26 | .set('errorText', false); 27 | case C.SEND_CONTACT_INFO_FAILURE: 28 | return state 29 | .set('sendingInfo', false) 30 | .set('sentInfo', false) 31 | .set('errorText', action.error); 32 | default: 33 | return state; 34 | } 35 | } 36 | 37 | export default contactReducer; 38 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Contact/sagas.js: -------------------------------------------------------------------------------- 1 | 2 | import { takeLatest } from 'redux-saga'; 3 | import { take, call, put, select, fork, cancel } from 'redux-saga/effects'; 4 | import { LOCATION_CHANGE } from 'react-router-redux'; 5 | import * as contactActions from 'containers/Contact/actions'; 6 | import { push } from 'react-router-redux' 7 | import request from 'utils/request'; 8 | 9 | export function* sendContact(action) { 10 | let {name, email, subject, message} = action.data; 11 | var body='email='+email+'&name='+name+'&subject='+subject+'&message='+message; 12 | const requestURL = '/api/contact'; 13 | const options = { 14 | method: 'post', 15 | headers: { 16 | 'Accept': 'application/json', 17 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' 18 | }, 19 | body: body 20 | } 21 | try { 22 | const { response, error } = yield call(request,requestURL, options); 23 | if (response){ 24 | if (response.success){ 25 | yield put(contactActions.sendContactInfoSuccess(response)); 26 | }else{ 27 | yield put(contactActions.sendContactInfoFailure(response.message)); 28 | } 29 | }else{ 30 | yield put(contactActions.sendContactInfoFailure(error)); 31 | } 32 | } catch (err) { 33 | yield put(contactActions.sendContactInfoFailure(err)); 34 | } 35 | } 36 | 37 | 38 | /** 39 | * Root saga manages watcher lifecycle 40 | */ 41 | export function* root() { 42 | const watcher1 = yield takeLatest('SEND_CONTACT_INFO_REQUEST', sendContact); 43 | yield take(LOCATION_CHANGE); 44 | yield cancel(watcher1); 45 | } 46 | 47 | // Bootstrap sagas 48 | export default [ 49 | root, 50 | ]; 51 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Contact/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the contact state domain 5 | */ 6 | const selectContactDomain = () => (state) => state.get('contact'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by Contact 15 | */ 16 | 17 | const selectContact = () => createSelector( 18 | selectContactDomain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default selectContact; 23 | export { 24 | selectContactDomain, 25 | }; 26 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/HomePage/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage 3 | * 4 | * This is the first thing users see of our App, at the '/' route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a necessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { connect } from 'react-redux'; 14 | import { createStructuredSelector } from 'reselect'; 15 | import * as selectors from '../Login/selectors'; 16 | import * as loginActions from '../Login/actions'; 17 | 18 | export class HomePage extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 19 | componentDidMount(){ 20 | if(sessionStorage.getItem('token') != null ){ 21 | if(!this.props.isAuthenticated){ 22 | var token = sessionStorage.getItem('token'); 23 | this.props.dispatch(loginActions.get_user_info(token,'/')); 24 | } 25 | } 26 | } 27 | render() { 28 | let welcomeMessage = null; 29 | if(this.props.isAuthenticated){ 30 | welcomeMessage =

Welcome, { this.props.selectName }

31 | } 32 | return ( 33 |
34 |
35 |
36 | { welcomeMessage } 37 |

Grayscale is a free Bootstrap 3 theme created by Start Bootstrap. It can be yours right now, simply download the template on the preview page. The theme is open source, and you can use it for any purpose, personal or commercial.

38 |

This theme features stock photos by Gratisography along with a custom Google Maps skin courtesy of Snazzy Maps.

39 |

Grayscale includes full HTML, CSS, and custom JavaScript files along with LESS files for easy customization.

40 |
41 |
42 |
43 | ); 44 | } 45 | } 46 | const mapDispatchToProps = (dispatch) => ({ 47 | dispatch 48 | }); 49 | const mapStateToProps = createStructuredSelector({ 50 | selectName: selectors.selectName(), 51 | isAuthenticated:selectors.selectisAuthenticated() 52 | }); 53 | 54 | export default connect(mapStateToProps, mapDispatchToProps)(HomePage); 55 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/HomePage/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import { HomePage } from '../index'; 2 | import expect from 'expect'; 3 | import { shallow } from 'enzyme'; 4 | import React from 'react'; 5 | 6 | describe('', () => { 7 | it('', () => { 8 | const props = { 9 | isAuthenticated:true, 10 | selectName:'name' 11 | } 12 | 13 | const renderedComponent = shallow( 14 | 15 | ); 16 | expect( 17 | renderedComponent.find("h2").node 18 | ).toExist(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { 8 | CHANGE_LOCALE, 9 | } from './constants'; 10 | 11 | export function changeLocale(languageLocale) { 12 | return { 13 | type: CHANGE_LOCALE, 14 | locale: languageLocale, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | export const DEFAULT_LOCALE = 'en'; 9 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import React from 'react'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { IntlProvider } from 'react-intl'; 13 | import { selectLocale } from './selectors'; 14 | 15 | export class LanguageProvider extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 16 | render() { 17 | return ( 18 | 19 | {React.Children.only(this.props.children)} 20 | 21 | ); 22 | } 23 | } 24 | 25 | LanguageProvider.propTypes = { 26 | locale: React.PropTypes.string, 27 | messages: React.PropTypes.object, 28 | children: React.PropTypes.element.isRequired, 29 | }; 30 | 31 | 32 | const mapStateToProps = createSelector( 33 | selectLocale(), 34 | (locale) => ({ locale }) 35 | ); 36 | 37 | function mapDispatchToProps(dispatch) { 38 | return { 39 | dispatch, 40 | }; 41 | } 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider); 44 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | CHANGE_LOCALE, 10 | } from './constants'; 11 | import { 12 | DEFAULT_LOCALE, 13 | } from '../App/constants'; // eslint-disable-line 14 | 15 | const initialState = fromJS({ 16 | locale: DEFAULT_LOCALE, 17 | }); 18 | 19 | function languageProviderReducer(state = initialState, action) { 20 | switch (action.type) { 21 | case CHANGE_LOCALE: 22 | return state 23 | .set('locale', action.locale); 24 | default: 25 | return state; 26 | } 27 | } 28 | 29 | export default languageProviderReducer; 30 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the languageToggle state domain 5 | */ 6 | const selectLanguage = () => (state) => state.get('language'); 7 | 8 | /** 9 | * Select the language locale 10 | */ 11 | 12 | const selectLocale = () => createSelector( 13 | selectLanguage(), 14 | (languageState) => languageState.get('locale') 15 | ); 16 | 17 | export { 18 | selectLanguage, 19 | selectLocale, 20 | }; 21 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Login actions 4 | * 5 | */ 6 | import {LOGIN_USER_REQUEST, LOGIN_USER_FAILURE, LOGIN_USER_SUCCESS, LOGOUT_USER, 7 | GET_USER_INFO_REQUEST} from './constants'; 8 | import { push } from 'react-router-redux' 9 | import jwtDecode from 'jwt-decode'; 10 | 11 | export function loginUser(email, password, redirect="/") { 12 | let data={}; 13 | data['email']=email; 14 | data['password']=password; 15 | data['redirect']=redirect; 16 | return { 17 | type: LOGIN_USER_REQUEST, 18 | data 19 | } 20 | } 21 | export function loginUserSuccess(response) { 22 | sessionStorage.setItem('token', response.token); 23 | return { 24 | type: LOGIN_USER_SUCCESS, 25 | response 26 | } 27 | } 28 | export function loginUserFailure(error) { 29 | sessionStorage.removeItem('token'); 30 | return { 31 | type: LOGIN_USER_FAILURE, 32 | error 33 | 34 | } 35 | } 36 | export function logout() { 37 | sessionStorage.removeItem('token'); 38 | return { 39 | type: LOGOUT_USER 40 | } 41 | } 42 | 43 | export function get_user_info(token,redirect) { 44 | return { 45 | type: GET_USER_INFO_REQUEST, 46 | token, 47 | redirect 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Login constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/Login/DEFAULT_ACTION'; 8 | export const LOGIN_USER_REQUEST = 'LOGIN_USER_REQUEST'; 9 | export const LOGIN_USER_FAILURE = 'LOGIN_USER_FAILURE'; 10 | export const LOGIN_USER_SUCCESS = 'LOGIN_USER_SUCCESS'; 11 | export const LOGOUT_USER = 'LOGOUT_USER'; 12 | export const GET_USER_INFO_REQUEST = 'GET_USER_INFO_REQUEST'; 13 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Login 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import { bindActionCreators } from 'redux'; 10 | import { createStructuredSelector } from 'reselect'; 11 | import * as selectors from './selectors'; 12 | import * as actionCreators from './actions'; 13 | 14 | export class Login extends React.Component { // eslint-disable-line react/prefer-stateless-function 15 | constructor(props) { 16 | super(props); 17 | const redirectRoute = this.props.location.query.redirect || '/'; 18 | this.state = { 19 | email:'', 20 | password:'', 21 | redirectTo: redirectRoute, 22 | errorText1:'', 23 | errorText2:'' 24 | }; 25 | } 26 | componentDidMount(){ 27 | if(sessionStorage.getItem('token') != null ){ 28 | var token = sessionStorage.getItem('token'); 29 | this.props.actions.get_user_info(token,this.state.redirectTo); 30 | } 31 | } 32 | handleNameChange(event) { this.setState({email: event.target.value}); } 33 | handlePasswordChange(event) { this.setState({password: event.target.value}); } 34 | onSubmit = (e) => { 35 | e.preventDefault() 36 | if(this.state.email == ''){ 37 | this.setState({errorText1: 'Email can not be empty'}); 38 | } 39 | else if(this.state.password == ''){ 40 | this.setState({errorText1: ''}); 41 | this.setState({errorText2: 'Password can not be empty'}); 42 | }else{ 43 | this.props.actions.loginUser(this.state.email, this.state.password, this.state.redirectTo); 44 | } 45 | }; 46 | 47 | render() { 48 | let errorText = null; 49 | if(this.props.selectError != false){ 50 | errorText=
{ this.props.selectError }
; 51 | } 52 | return ( 53 |
54 |
55 | 56 |

57 |
58 | 59 | 60 | 61 |
62 | 65 |
66 | 67 |
68 | 69 | Forgot the password? 70 | 71 | { errorText } 72 |
73 |
74 | ); 75 | } 76 | } 77 | 78 | const mapDispatchToProps = (dispatch) => ({ 79 | actions : bindActionCreators(actionCreators, dispatch) 80 | }); 81 | const mapStateToProps = createStructuredSelector({ 82 | isAuthenticating: selectors.selectisAuthenticating(), 83 | selectError: selectors.selectError(), 84 | isAuthenticated:selectors.selectisAuthenticated() 85 | }); 86 | 87 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 88 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/reducer.js: -------------------------------------------------------------------------------- 1 | import {LOGIN_USER_REQUEST, LOGIN_USER_SUCCESS, LOGIN_USER_FAILURE, LOGOUT_USER, 2 | GET_USER_INFO_REQUEST} from './constants'; 3 | import { push } from 'react-router-redux'; 4 | import jwtDecode from 'jwt-decode'; 5 | import { fromJS } from 'immutable'; 6 | const initialState = fromJS({ 7 | token: false, 8 | name: false, 9 | picture:false, 10 | email:false, 11 | isAuthenticated: false, 12 | isAuthenticating: false, 13 | errorText: false 14 | }); 15 | function authReducer(state = initialState, action) { 16 | switch (action.type) { 17 | case LOGIN_USER_REQUEST: 18 | return state 19 | .set('isAuthenticating', true) 20 | .set('errorText', false); 21 | case LOGIN_USER_SUCCESS: 22 | return state 23 | .set('isAuthenticating', false) 24 | .set('isAuthenticated', true) 25 | .set('token', action.response.token) 26 | .set('name', action.response.data.name) 27 | .set('picture', action.response.data.picture) 28 | .set('email', action.response.data.email) 29 | .set('errorText', false); 30 | case LOGIN_USER_FAILURE: 31 | return state 32 | .set('isAuthenticating', false) 33 | .set('isAuthenticated', false) 34 | .set('token', false) 35 | .set('name', false) 36 | .set('picture', false) 37 | .set('email', false) 38 | .set('errorText', action.error); 39 | case LOGOUT_USER: 40 | return state 41 | .set('isAuthenticating', false) 42 | .set('isAuthenticated', false) 43 | .set('token', false) 44 | .set('name', false) 45 | .set('picture', false) 46 | .set('email', false) 47 | case GET_USER_INFO_REQUEST: 48 | return state 49 | .set('isAuthenticating', true); 50 | default: 51 | return state; 52 | } 53 | } 54 | 55 | export default authReducer; 56 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/sagas.js: -------------------------------------------------------------------------------- 1 | 2 | import { takeLatest } from 'redux-saga'; 3 | import { take, call, put, select, fork, cancel } from 'redux-saga/effects'; 4 | import { LOCATION_CHANGE } from 'react-router-redux'; 5 | import { LOGIN_USER_REQUEST, LOGIN_USER_FAILURE, LOGIN_USER_SUCCESS, LOGOUT_USER, 6 | GET_USER_INFO_REQUEST } from 'containers/Login/constants'; 7 | import * as loginActions from 'containers/Login/actions'; 8 | import { push } from 'react-router-redux' 9 | import request from 'utils/request'; 10 | 11 | export function* getLogin(action) { 12 | let {email, password,redirect} = action.data; 13 | var auth='email='+email+'&password='+password; 14 | const requestURL = '/api/auth'; 15 | const options = { 16 | method: 'post', 17 | headers: { 18 | 'Accept': 'application/json', 19 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' 20 | }, 21 | body: auth 22 | } 23 | try { 24 | const { response, error } = yield call(request,requestURL, options); 25 | if (response){ 26 | if (response.success){ 27 | yield put(loginActions.loginUserSuccess(response)); 28 | yield put(push(redirect)); 29 | }else{ 30 | yield put(loginActions.loginUserFailure(response.message)); 31 | } 32 | }else{ 33 | console.log(error); 34 | yield put(loginActions.loginUserFailure(error)); 35 | } 36 | } catch (err) { 37 | console.log("error",err); 38 | yield put(loginActions.loginUserFailure(err)); 39 | } 40 | } 41 | 42 | 43 | /** 44 | * Root saga manages watcher lifecycle 45 | */ 46 | export function* root() { 47 | const watcher1 = yield takeLatest(LOGIN_USER_REQUEST, getLogin); 48 | yield take(LOCATION_CHANGE); 49 | yield cancel(watcher1); 50 | } 51 | 52 | // Bootstrap sagas 53 | export default [ 54 | root, 55 | ]; 56 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const selectAuth = () => (state) => state.get('auth'); 4 | 5 | const selectisAuthenticating = () => createSelector( 6 | selectAuth(), 7 | (selectisAuthenticating) => selectisAuthenticating.get('isAuthenticating') 8 | ); 9 | const selectError = () => createSelector( 10 | selectAuth(), 11 | (selectStatus) => selectStatus.get('errorText') 12 | ); 13 | const selectisAuthenticated = () => createSelector( 14 | selectAuth(), 15 | (state) => state.get('isAuthenticated') 16 | ); 17 | 18 | const selectPicture= () => createSelector( 19 | selectAuth(), 20 | (state) => state.get('picture') 21 | ); 22 | const selectName= () => createSelector( 23 | selectAuth(), 24 | (state) => state.get('name') 25 | ); 26 | 27 | export { 28 | selectPicture, 29 | selectisAuthenticating, 30 | selectError, 31 | selectisAuthenticated, 32 | selectName 33 | }; 34 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/tests/actions.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | loginUser,loginUserSuccess,loginUserFailure,logout,get_user_info 4 | } from '../actions'; 5 | 6 | import {LOGIN_USER_REQUEST, LOGIN_USER_FAILURE, LOGIN_USER_SUCCESS, LOGOUT_USER, 7 | GET_USER_INFO_REQUEST} from '../constants'; 8 | 9 | describe('loginUser Action', () => { 10 | it('should create an loginUser action', () => { 11 | const email='email'; 12 | const password='password'; 13 | const redirect='/'; 14 | let data={}; 15 | data['email']=email; 16 | data['password']=password; 17 | data['redirect']=redirect; 18 | const expected = { 19 | type: LOGIN_USER_REQUEST, 20 | data 21 | }; 22 | expect(loginUser(email, password, redirect)).toEqual(expected); 23 | }); 24 | }); 25 | 26 | describe('loginUserSuccess Action', () => { 27 | it('should create an loginUserSuccess action', () => { 28 | const response = 'response'; 29 | const expected = { 30 | type: LOGIN_USER_SUCCESS, 31 | response 32 | }; 33 | expect(loginUserSuccess(response)).toEqual(expected); 34 | }); 35 | }); 36 | 37 | describe('loginUserFailure Action', () => { 38 | it('should create an loginUserFailure action', () => { 39 | const error = 'error'; 40 | const expected = { 41 | type: LOGIN_USER_FAILURE, 42 | error 43 | }; 44 | expect(loginUserFailure(error)).toEqual(expected); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import { Login } from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import {mount} from 'enzyme'; 6 | import React from 'react'; 7 | 8 | describe('', () => { 9 | it('', () => { 10 | const props = { 11 | location:{query:{redirect:'/'}} 12 | } 13 | 14 | const renderedComponent = shallow( 15 | 16 | ); 17 | expect( 18 | renderedComponent.find("button").node 19 | ).toExist(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Login/tests/reducer.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import loginReducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('loginReducer', () => { 6 | it('returns the initial state', () => { 7 | expect(loginReducer(undefined, {})).toEqual(fromJS({ 8 | token: false, 9 | name: false, 10 | picture:false, 11 | email:false, 12 | isAuthenticated: false, 13 | isAuthenticating: false, 14 | errorText: false 15 | })); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a necessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | import { FormattedMessage } from 'react-intl'; 14 | import messages from './messages'; 15 | 16 | export default class NotFound extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 17 | render() { 18 | return ( 19 | 20 |
21 |
22 |
23 |

24 | 404 25 |

26 |

27 | 28 |

29 |
30 |
31 |
32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.NotFoundPage.header', 11 | defaultMessage: 'This is NotFoundPage!', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Users/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Users actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Users/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Users constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/Users/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Users/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Users 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import selectUsers from './selectors'; 10 | import { requireAuthentication } from '../../utils/AuthWrapper'; 11 | 12 | export class Users extends React.Component { // eslint-disable-line react/prefer-stateless-function 13 | render() { 14 | let userlist = this.props.userList.map((user,i) =>{ 15 | return
  • {user}
  • 16 | }); 17 | 18 | return ( 19 |
    20 |
    21 |
    22 |

    User List

    23 |
      24 | { userlist } 25 |
    26 |
    27 |
    28 |
    29 | ); 30 | } 31 | } 32 | 33 | const mapStateToProps = selectUsers(); 34 | 35 | function mapDispatchToProps(dispatch) { 36 | return { 37 | dispatch, 38 | }; 39 | } 40 | 41 | export default connect(mapStateToProps, mapDispatchToProps)(requireAuthentication(Users)); 42 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Users/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Users reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({ 13 | userList: [ 14 | 'John, Doe', 15 | 'Dummy User1', 16 | 'Dummy User2', 17 | 'Dummy User3', 18 | ] 19 | }); 20 | 21 | function usersReducer(state = initialState, action) { 22 | switch (action.type) { 23 | case DEFAULT_ACTION: 24 | return state; 25 | default: 26 | return state; 27 | } 28 | } 29 | 30 | export default usersReducer; 31 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Users/sagas.js: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /FrontEnd/app/containers/Users/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the users state domain 5 | */ 6 | const selectUsersDomain = () => (state) => state.get('users'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by Users 15 | */ 16 | 17 | const selectUsers = () => createSelector( 18 | selectUsersDomain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default selectUsers; 23 | export { 24 | selectUsersDomain, 25 | }; 26 | -------------------------------------------------------------------------------- /FrontEnd/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hurkanyakay/reactjs-nodejs-rest-example/916b86e921753ba32db804006ba576a045bab9db/FrontEnd/app/favicon.ico -------------------------------------------------------------------------------- /FrontEnd/app/global-styles.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components'; 2 | import background from './img/intro-bg.jpg'; 3 | /* eslint no-unused-expressions: 0 */ 4 | injectGlobal` 5 | /*! 6 | * Start Bootstrap - Grayscale v3.3.7+1 (http://startbootstrap.com/template-overviews/grayscale) 7 | * Copyright 2013-2016 Start Bootstrap 8 | * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE) 9 | */ 10 | body { 11 | width: 100%; 12 | height: 100%; 13 | font-family: "Lora", "Helvetica Neue", Helvetica, Arial, sans-serif; 14 | color: white; 15 | background-color: black; 16 | } 17 | html { 18 | width: 100%; 19 | height: 100%; 20 | } 21 | h1, 22 | h2, 23 | h3, 24 | h4, 25 | h5, 26 | h6 { 27 | margin: 0 0 35px; 28 | text-transform: uppercase; 29 | font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif; 30 | font-weight: 700; 31 | letter-spacing: 1px; 32 | } 33 | p { 34 | margin: 0 0 25px; 35 | font-size: 18px; 36 | line-height: 1.5; 37 | } 38 | @media (min-width: 768px) { 39 | p { 40 | margin: 0 0 35px; 41 | font-size: 20px; 42 | line-height: 1.6; 43 | } 44 | } 45 | a { 46 | color: #42DCA3; 47 | -webkit-transition: all 0.2s ease-in-out; 48 | -moz-transition: all 0.2s ease-in-out; 49 | transition: all 0.2s ease-in-out; 50 | } 51 | a:hover, 52 | a:focus { 53 | text-decoration: none; 54 | color: #1d9b6c; 55 | } 56 | .light { 57 | font-weight: 400; 58 | } 59 | .navbar-custom { 60 | margin-bottom: 0; 61 | border-bottom: 1px solid rgba(255, 255, 255, 0.3); 62 | text-transform: uppercase; 63 | font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif; 64 | background-color: black; 65 | } 66 | .navbar-custom .navbar-toggle { 67 | color: white; 68 | background-color: rgba(255, 255, 255, 0.2); 69 | font-size: 12px; 70 | } 71 | .navbar-custom .navbar-toggle:focus, 72 | .navbar-custom .navbar-toggle:active { 73 | outline: none; 74 | } 75 | .navbar-custom .navbar-brand { 76 | font-weight: 700; 77 | } 78 | .navbar-custom .navbar-brand:focus { 79 | outline: none; 80 | } 81 | .navbar-custom a { 82 | color: white; 83 | } 84 | .navbar-custom .nav li a { 85 | -webkit-transition: background 0.3s ease-in-out; 86 | -moz-transition: background 0.3s ease-in-out; 87 | transition: background 0.3s ease-in-out; 88 | } 89 | .navbar-custom .nav li a:hover { 90 | color: rgba(255, 255, 255, 0.8); 91 | outline: none; 92 | background-color: transparent; 93 | } 94 | .navbar-custom .nav li a:focus, 95 | .navbar-custom .nav li a:active { 96 | outline: none; 97 | background-color: transparent; 98 | } 99 | .navbar-custom .nav li.active { 100 | outline: none; 101 | } 102 | .navbar-custom .nav li.active a { 103 | background-color: rgba(255, 255, 255, 0.3); 104 | } 105 | .navbar-custom .nav li.active a:hover { 106 | color: white; 107 | } 108 | @media (min-width: 768px) { 109 | .navbar-custom { 110 | padding: 20px 0; 111 | border-bottom: none; 112 | letter-spacing: 1px; 113 | background: transparent; 114 | -webkit-transition: background 0.5s ease-in-out, padding 0.5s ease-in-out; 115 | -moz-transition: background 0.5s ease-in-out, padding 0.5s ease-in-out; 116 | transition: background 0.5s ease-in-out, padding 0.5s ease-in-out; 117 | } 118 | .navbar-custom.top-nav-collapse { 119 | padding: 0; 120 | background: black; 121 | border-bottom: 1px solid rgba(255, 255, 255, 0.3); 122 | } 123 | } 124 | .intro { 125 | display: table; 126 | width: 100%; 127 | height: auto; 128 | padding: 100px 0; 129 | text-align: center; 130 | color: white; 131 | -webkit-background-size: cover; 132 | -moz-background-size: cover; 133 | background-size: cover; 134 | -o-background-size: cover; 135 | } 136 | .intro .intro-body { 137 | display: table-cell; 138 | vertical-align: middle; 139 | } 140 | .intro .intro-body .brand-heading { 141 | font-size: 40px; 142 | } 143 | .intro .intro-body .intro-text { 144 | font-size: 18px; 145 | } 146 | @media (min-width: 768px) { 147 | .intro { 148 | height: 100%; 149 | padding: 0; 150 | } 151 | .intro .intro-body .brand-heading { 152 | font-size: 100px; 153 | } 154 | .intro .intro-body .intro-text { 155 | font-size: 26px; 156 | } 157 | } 158 | .btn-circle { 159 | width: 70px; 160 | height: 70px; 161 | margin-top: 15px; 162 | padding: 7px 16px; 163 | border: 2px solid white; 164 | border-radius: 100% !important; 165 | font-size: 40px; 166 | color: white; 167 | background: transparent; 168 | -webkit-transition: background 0.3s ease-in-out; 169 | -moz-transition: background 0.3s ease-in-out; 170 | transition: background 0.3s ease-in-out; 171 | } 172 | .btn-circle:hover, 173 | .btn-circle:focus { 174 | outline: none; 175 | color: white; 176 | background: rgba(255, 255, 255, 0.1); 177 | } 178 | .btn-circle i.animated { 179 | -webkit-transition-property: -webkit-transform; 180 | -webkit-transition-duration: 1s; 181 | -moz-transition-property: -moz-transform; 182 | -moz-transition-duration: 1s; 183 | } 184 | .btn-circle:hover i.animated { 185 | -webkit-animation-name: pulse; 186 | -moz-animation-name: pulse; 187 | -webkit-animation-duration: 1.5s; 188 | -moz-animation-duration: 1.5s; 189 | -webkit-animation-iteration-count: infinite; 190 | -moz-animation-iteration-count: infinite; 191 | -webkit-animation-timing-function: linear; 192 | -moz-animation-timing-function: linear; 193 | } 194 | @-webkit-keyframes pulse { 195 | 0% { 196 | -webkit-transform: scale(1); 197 | transform: scale(1); 198 | } 199 | 50% { 200 | -webkit-transform: scale(1.2); 201 | transform: scale(1.2); 202 | } 203 | 100% { 204 | -webkit-transform: scale(1); 205 | transform: scale(1); 206 | } 207 | } 208 | @-moz-keyframes pulse { 209 | 0% { 210 | -moz-transform: scale(1); 211 | transform: scale(1); 212 | } 213 | 50% { 214 | -moz-transform: scale(1.2); 215 | transform: scale(1.2); 216 | } 217 | 100% { 218 | -moz-transform: scale(1); 219 | transform: scale(1); 220 | } 221 | } 222 | .content-section { 223 | padding-top: 70px; 224 | } 225 | .download-section { 226 | width: 100%; 227 | padding: 50px 0; 228 | color: white; 229 | background: url(../img/downloads-bg.jpg) no-repeat center center scroll; 230 | background-color: black; 231 | -webkit-background-size: cover; 232 | -moz-background-size: cover; 233 | background-size: cover; 234 | -o-background-size: cover; 235 | } 236 | #map { 237 | width: 100%; 238 | height: 200px; 239 | margin-top: 100px; 240 | } 241 | @media (min-width: 767px) { 242 | .content-section { 243 | padding-top: 100px; 244 | } 245 | .download-section { 246 | padding: 100px 0; 247 | } 248 | #map { 249 | height: 400px; 250 | margin-top: 250px; 251 | } 252 | } 253 | .btn { 254 | text-transform: uppercase; 255 | font-family: "Montserrat", "Helvetica Neue", Helvetica, Arial, sans-serif; 256 | font-weight: 400; 257 | -webkit-transition: all 0.3s ease-in-out; 258 | -moz-transition: all 0.3s ease-in-out; 259 | transition: all 0.3s ease-in-out; 260 | border-radius: 0; 261 | } 262 | .btn-default { 263 | border: 1px solid #42DCA3; 264 | color: #42DCA3; 265 | background-color: transparent; 266 | } 267 | .btn-default:hover, 268 | .btn-default:focus { 269 | border: 1px solid #42DCA3; 270 | outline: none; 271 | color: black; 272 | background-color: #42DCA3; 273 | } 274 | ul.banner-social-buttons { 275 | margin-top: 0; 276 | } 277 | @media (max-width: 1199px) { 278 | ul.banner-social-buttons { 279 | margin-top: 15px; 280 | } 281 | } 282 | @media (max-width: 767px) { 283 | ul.banner-social-buttons li { 284 | display: block; 285 | margin-bottom: 20px; 286 | padding: 0; 287 | } 288 | ul.banner-social-buttons li:last-child { 289 | margin-bottom: 0; 290 | } 291 | } 292 | footer { 293 | padding: 50px 0; 294 | } 295 | footer p { 296 | margin: 0; 297 | } 298 | ::-moz-selection { 299 | text-shadow: none; 300 | background: #fcfcfc; 301 | background: rgba(255, 255, 255, 0.2); 302 | } 303 | ::selection { 304 | text-shadow: none; 305 | background: #fcfcfc; 306 | background: rgba(255, 255, 255, 0.2); 307 | } 308 | img::selection { 309 | background: transparent; 310 | } 311 | img::-moz-selection { 312 | background: transparent; 313 | } 314 | body { 315 | webkit-tap-highlight-color: rgba(255, 255, 255, 0.2); 316 | } 317 | #app, #appWrapper{ 318 | height:100%; 319 | background: url('${background}') no-repeat bottom center scroll; 320 | } 321 | 322 | 323 | /* LOGIN PAGE */ 324 | .card-container.card { 325 | max-width: 350px; 326 | padding: 40px 40px; 327 | } 328 | 329 | .btn { 330 | font-weight: 700; 331 | height: 36px; 332 | -moz-user-select: none; 333 | -webkit-user-select: none; 334 | user-select: none; 335 | cursor: default; 336 | } 337 | 338 | /* 339 | * Card component 340 | */ 341 | .card { 342 | background-color: #F7F7F7; 343 | /* just in case there no content*/ 344 | padding: 20px 25px 30px; 345 | margin: 0 auto 25px; 346 | margin-top: 50px; 347 | /* shadows and rounded borders */ 348 | -moz-border-radius: 2px; 349 | -webkit-border-radius: 2px; 350 | border-radius: 2px; 351 | -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 352 | -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 353 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); 354 | } 355 | 356 | .profile-img-card { 357 | width: 96px; 358 | height: 96px; 359 | margin: 0 auto 10px; 360 | display: block; 361 | -moz-border-radius: 50%; 362 | -webkit-border-radius: 50%; 363 | border-radius: 50%; 364 | } 365 | 366 | /* 367 | * Form styles 368 | */ 369 | .profile-name-card { 370 | font-size: 16px; 371 | font-weight: bold; 372 | text-align: center; 373 | margin: 10px 0 0; 374 | min-height: 1em; 375 | } 376 | 377 | .reauth-email { 378 | display: block; 379 | color: #404040; 380 | line-height: 2; 381 | margin-bottom: 10px; 382 | font-size: 14px; 383 | text-align: center; 384 | overflow: hidden; 385 | text-overflow: ellipsis; 386 | white-space: nowrap; 387 | -moz-box-sizing: border-box; 388 | -webkit-box-sizing: border-box; 389 | box-sizing: border-box; 390 | } 391 | 392 | .form-signin #inputEmail, 393 | .form-signin #inputPassword { 394 | direction: ltr; 395 | height: 44px; 396 | font-size: 16px; 397 | } 398 | 399 | .form-signin input[type=email], 400 | .form-signin input[type=password], 401 | .form-signin input[type=text], 402 | .form-signin button { 403 | width: 100%; 404 | display: block; 405 | margin-bottom: 10px; 406 | z-index: 1; 407 | position: relative; 408 | -moz-box-sizing: border-box; 409 | -webkit-box-sizing: border-box; 410 | box-sizing: border-box; 411 | } 412 | 413 | .form-signin .form-control:focus { 414 | border-color: rgb(104, 145, 162); 415 | outline: 0; 416 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162); 417 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162); 418 | } 419 | 420 | .btn.btn-signin { 421 | /*background-color: #4d90fe; */ 422 | background-color: rgb(104, 145, 162); 423 | /* background-color: linear-gradient(rgb(104, 145, 162), rgb(12, 97, 33));*/ 424 | padding: 0px; 425 | font-weight: 700; 426 | font-size: 14px; 427 | height: 36px; 428 | -moz-border-radius: 3px; 429 | -webkit-border-radius: 3px; 430 | border-radius: 3px; 431 | border: none; 432 | -o-transition: all 0.218s; 433 | -moz-transition: all 0.218s; 434 | -webkit-transition: all 0.218s; 435 | transition: all 0.218s; 436 | } 437 | 438 | .btn.btn-signin:hover, 439 | .btn.btn-signin:active, 440 | .btn.btn-signin:focus { 441 | background-color: rgb(12, 97, 33); 442 | } 443 | 444 | .forgot-password { 445 | color: rgb(104, 145, 162); 446 | } 447 | 448 | .forgot-password:hover, 449 | .forgot-password:active, 450 | .forgot-password:focus{ 451 | color: rgb(12, 97, 33); 452 | } 453 | label{ 454 | color: grey; 455 | } 456 | `; 457 | -------------------------------------------------------------------------------- /FrontEnd/app/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n.js 3 | * 4 | * This will setup the i18n language files and locale data for your app. 5 | * 6 | */ 7 | import { addLocaleData } from 'react-intl'; 8 | import { DEFAULT_LOCALE } from './containers/App/constants'; // eslint-disable-line 9 | 10 | import enLocaleData from 'react-intl/locale-data/en'; 11 | 12 | export const appLocales = [ 13 | 'en', 14 | ]; 15 | 16 | import enTranslationMessages from './translations/en.json'; 17 | 18 | addLocaleData(enLocaleData); 19 | 20 | export const formatTranslationMessages = (locale, messages) => { 21 | const defaultFormattedMessages = locale !== DEFAULT_LOCALE ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) : {}; 22 | const formattedMessages = {}; 23 | const messageKeys = Object.keys(messages); 24 | for (const messageKey of messageKeys) { 25 | if (locale === DEFAULT_LOCALE) { 26 | formattedMessages[messageKey] = messages[messageKey]; 27 | } else { 28 | formattedMessages[messageKey] = messages[messageKey] || defaultFormattedMessages[messageKey]; 29 | } 30 | } 31 | 32 | return formattedMessages; 33 | }; 34 | 35 | export const translationMessages = { 36 | en: formatTranslationMessages('en', enTranslationMessages), 37 | }; 38 | -------------------------------------------------------------------------------- /FrontEnd/app/img/intro-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hurkanyakay/reactjs-nodejs-rest-example/916b86e921753ba32db804006ba576a045bab9db/FrontEnd/app/img/intro-bg.jpg -------------------------------------------------------------------------------- /FrontEnd/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | React.js 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /FrontEnd/app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "React Boilerplate", 3 | "icons": [ 4 | { 5 | "src": "favicon.png", 6 | "sizes": "48x48", 7 | "type": "image/png", 8 | "density": 1.0 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "sizes": "96x96", 13 | "type": "image/png", 14 | "density": 2.0 15 | }, 16 | { 17 | "src": "favicon.png", 18 | "sizes": "144x144", 19 | "type": "image/png", 20 | "density": 3.0 21 | }, 22 | { 23 | "src": "favicon.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "density": 4.0 27 | } 28 | ], 29 | "start_url": "index.html", 30 | "display": "standalone", 31 | "orientation": "portrait", 32 | "background_color": "#FFFFFF" 33 | } -------------------------------------------------------------------------------- /FrontEnd/app/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { combineReducers } from 'redux-immutable'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 10 | import authReducer from 'containers/Login/reducer'; 11 | /* 12 | * routeReducer 13 | * 14 | * The reducer merges route location changes into our immutable state. 15 | * The change is necessitated by moving to react-router-redux@4 16 | * 17 | */ 18 | 19 | // Initial routing state 20 | const routeInitialState = fromJS({ 21 | locationBeforeTransitions: null, 22 | }); 23 | 24 | /** 25 | * Merge route into the global application state 26 | */ 27 | function routeReducer(state = routeInitialState, action) { 28 | switch (action.type) { 29 | /* istanbul ignore next */ 30 | case LOCATION_CHANGE: 31 | return state.merge({ 32 | locationBeforeTransitions: action.payload, 33 | }); 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | /** 40 | * Creates the main reducer with the asynchronously loaded ones 41 | */ 42 | export default function createReducer(asyncReducers) { 43 | return combineReducers({ 44 | route: routeReducer, 45 | language: languageProviderReducer, 46 | auth: authReducer, 47 | ...asyncReducers, 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /FrontEnd/app/routes.js: -------------------------------------------------------------------------------- 1 | // These are the pages you can go to. 2 | // They are all wrapped in the App component, which should contain the navbar etc 3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information 4 | // about the code splitting business 5 | import { getAsyncInjectors } from 'utils/asyncInjectors'; 6 | 7 | const errorLoading = (err) => { 8 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console 9 | }; 10 | 11 | const loadModule = (cb) => (componentModule) => { 12 | cb(null, componentModule.default); 13 | }; 14 | 15 | export default function createRoutes(store) { 16 | // Create reusable async injectors using getAsyncInjectors factory 17 | const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars 18 | 19 | return [ 20 | { 21 | path: '/', 22 | name: 'home', 23 | getComponent(nextState, cb) { 24 | const importModules = Promise.all([ 25 | System.import('containers/HomePage'), 26 | ]); 27 | 28 | const renderRoute = loadModule(cb); 29 | 30 | importModules.then(([component]) => { 31 | renderRoute(component); 32 | }); 33 | 34 | importModules.catch(errorLoading); 35 | }, 36 | }, { 37 | path: '/users', 38 | name: 'users', 39 | getComponent(nextState, cb) { 40 | const importModules = Promise.all([ 41 | System.import('containers/Users/reducer'), 42 | System.import('containers/Users/sagas'), 43 | System.import('containers/Users'), 44 | ]); 45 | 46 | const renderRoute = loadModule(cb); 47 | 48 | importModules.then(([reducer, sagas, component]) => { 49 | injectReducer('users', reducer.default); 50 | injectSagas(sagas.default); 51 | renderRoute(component); 52 | }); 53 | 54 | importModules.catch(errorLoading); 55 | }, 56 | }, { 57 | path: '/contact', 58 | name: 'contact', 59 | getComponent(nextState, cb) { 60 | const importModules = Promise.all([ 61 | System.import('containers/Contact/reducer'), 62 | System.import('containers/Contact/sagas'), 63 | System.import('containers/Contact'), 64 | ]); 65 | 66 | const renderRoute = loadModule(cb); 67 | 68 | importModules.then(([reducer, sagas, component]) => { 69 | injectReducer('contact', reducer.default); 70 | injectSagas(sagas.default); 71 | renderRoute(component); 72 | }); 73 | 74 | importModules.catch(errorLoading); 75 | }, 76 | }, { 77 | path: '/login', 78 | name: 'login', 79 | getComponent(nextState, cb) { 80 | const importModules = Promise.all([ 81 | System.import('containers/Login/reducer'), 82 | System.import('containers/Login/sagas'), 83 | System.import('containers/Login'), 84 | ]); 85 | 86 | const renderRoute = loadModule(cb); 87 | 88 | importModules.then(([reducer, sagas, component]) => { 89 | // injectReducer('login', reducer.default); 90 | injectSagas(sagas.default); 91 | renderRoute(component); 92 | }); 93 | 94 | importModules.catch(errorLoading); 95 | }, 96 | }, { 97 | path: '*', 98 | name: 'notfound', 99 | getComponent(nextState, cb) { 100 | System.import('containers/NotFoundPage') 101 | .then(loadModule(cb)) 102 | .catch(errorLoading); 103 | }, 104 | }, 105 | ]; 106 | } 107 | -------------------------------------------------------------------------------- /FrontEnd/app/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | import rootSaga from './containers/App/sagas'; 11 | 12 | const sagaMiddleware = createSagaMiddleware(); 13 | 14 | export default function configureStore(initialState = {}, history) { 15 | // Create the store with two middlewares 16 | // 1. sagaMiddleware: Makes redux-sagas work 17 | // 2. routerMiddleware: Syncs the location/URL path to the state 18 | const middlewares = [ 19 | sagaMiddleware, 20 | routerMiddleware(history), 21 | ]; 22 | 23 | const enhancers = [ 24 | applyMiddleware(...middlewares), 25 | ]; 26 | 27 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 28 | /* eslint-disable no-underscore-dangle */ 29 | const composeEnhancers = 30 | process.env.NODE_ENV !== 'production' && 31 | typeof window === 'object' && 32 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 33 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; 34 | /* eslint-enable */ 35 | 36 | const store = createStore( 37 | createReducer(), 38 | fromJS(initialState), 39 | composeEnhancers(...enhancers) 40 | ); 41 | 42 | // Extensions 43 | sagaMiddleware.run(rootSaga); 44 | store.runSaga = sagaMiddleware.run; 45 | store.asyncReducers = {}; // Async reducer registry 46 | 47 | // Make reducers hot reloadable, see http://mxs.is/googmo 48 | /* istanbul ignore next */ 49 | if (module.hot) { 50 | module.hot.accept('./reducers', () => { 51 | System.import('./reducers').then((reducerModule) => { 52 | const createReducers = reducerModule.default; 53 | const nextReducers = createReducers(store.asyncReducers); 54 | 55 | store.replaceReducer(nextReducers); 56 | }); 57 | }); 58 | } 59 | 60 | return store; 61 | } 62 | -------------------------------------------------------------------------------- /FrontEnd/app/tests/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from '../store'; // eslint-disable-line 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /FrontEnd/app/translations/en.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /FrontEnd/app/utils/AuthWrapper.js: -------------------------------------------------------------------------------- 1 | import { UserAuthWrapper } from 'redux-auth-wrapper' 2 | import { push } from 'react-router-redux'; 3 | 4 | export const requireAuthentication = UserAuthWrapper({ 5 | authSelector: state => state.get("auth") , 6 | predicate: auth => auth.get('isAuthenticated'), 7 | redirectAction: push, 8 | wrapperDisplayName: 'UserIsJWTAuthenticated' 9 | }); 10 | -------------------------------------------------------------------------------- /FrontEnd/app/utils/asyncInjectors.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isEmpty, isFunction, isObject, isString } from 'lodash'; 2 | import invariant from 'invariant'; 3 | import warning from 'warning'; 4 | import createReducer from 'reducers'; 5 | 6 | /** 7 | * Validate the shape of redux store 8 | */ 9 | export function checkStore(store) { 10 | const shape = { 11 | dispatch: isFunction, 12 | subscribe: isFunction, 13 | getState: isFunction, 14 | replaceReducer: isFunction, 15 | runSaga: isFunction, 16 | asyncReducers: isObject, 17 | }; 18 | invariant( 19 | conformsTo(store, shape), 20 | '(app/utils...) asyncInjectors: Expected a valid redux store' 21 | ); 22 | } 23 | 24 | /** 25 | * Inject an asynchronously loaded reducer 26 | */ 27 | export function injectAsyncReducer(store, isValid) { 28 | return function injectReducer(name, asyncReducer) { 29 | if (!isValid) checkStore(store); 30 | 31 | invariant( 32 | isString(name) && !isEmpty(name) && isFunction(asyncReducer), 33 | '(app/utils...) injectAsyncReducer: Expected `asyncReducer` to be a reducer function' 34 | ); 35 | 36 | if (Reflect.has(store.asyncReducers, name)) return; 37 | 38 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line no-param-reassign 39 | store.replaceReducer(createReducer(store.asyncReducers)); 40 | }; 41 | } 42 | 43 | /** 44 | * Inject an asynchronously loaded saga 45 | */ 46 | export function injectAsyncSagas(store, isValid) { 47 | return function injectSagas(sagas) { 48 | if (!isValid) checkStore(store); 49 | 50 | invariant( 51 | Array.isArray(sagas), 52 | '(app/utils...) injectAsyncSagas: Expected `sagas` to be an array of generator functions' 53 | ); 54 | 55 | warning( 56 | !isEmpty(sagas), 57 | '(app/utils...) injectAsyncSagas: Received an empty `sagas` array' 58 | ); 59 | 60 | sagas.map(store.runSaga); 61 | }; 62 | } 63 | 64 | /** 65 | * Helper for creating injectors 66 | */ 67 | export function getAsyncInjectors(store) { 68 | checkStore(store); 69 | 70 | return { 71 | injectReducer: injectAsyncReducer(store, true), 72 | injectSagas: injectAsyncSagas(store, true), 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /FrontEnd/app/utils/request.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | 3 | /** 4 | * Parses the JSON returned by a network request 5 | * 6 | * @param {object} response A response from a network request 7 | * 8 | * @return {object} The parsed JSON from the request 9 | */ 10 | function parseJSON(response) { 11 | return response.json(); 12 | } 13 | 14 | /** 15 | * Checks if a network request came back fine, and throws an error if not 16 | * 17 | * @param {object} response A response from a network request 18 | * 19 | * @return {object|undefined} Returns either the response, or throws an error 20 | */ 21 | function checkStatus(response) { 22 | if (response.status >= 200 && response.status < 300) { 23 | return response; 24 | }else{ 25 | const error = new Error(response.statusText); 26 | error.response = response; 27 | throw error; 28 | } 29 | } 30 | 31 | /** 32 | * Requests a URL, returning a promise 33 | * 34 | * @param {string} url The URL we want to request 35 | * @param {object} [options] The options we want to pass to "fetch" 36 | * 37 | * @return {object} The response data 38 | */ 39 | export default function request(url, options) { 40 | return fetch(url, options) 41 | .then(checkStatus) 42 | .then(parseJSON) 43 | .then(response => ({ response })) 44 | .catch(error => ({ error })); 45 | } 46 | -------------------------------------------------------------------------------- /FrontEnd/app/utils/tests/asyncInjectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test async injectors 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from 'store'; 7 | import { memoryHistory } from 'react-router'; 8 | import { put } from 'redux-saga/effects'; 9 | import { fromJS } from 'immutable'; 10 | 11 | import { 12 | injectAsyncReducer, 13 | injectAsyncSagas, 14 | getAsyncInjectors, 15 | } from 'utils/asyncInjectors'; 16 | 17 | // Fixtures 18 | 19 | const initialState = fromJS({ reduced: 'soon' }); 20 | 21 | const reducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case 'TEST': 24 | return state.set('reduced', action.payload); 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | function* testSaga() { 31 | yield put({ type: 'TEST', payload: 'yup' }); 32 | } 33 | 34 | const sagas = [ 35 | testSaga, 36 | ]; 37 | 38 | describe('asyncInjectors', () => { 39 | let store; 40 | 41 | describe('getAsyncInjectors', () => { 42 | before(() => { 43 | store = configureStore({}, memoryHistory); 44 | }); 45 | 46 | it('given a store, should return all async injectors', () => { 47 | const { injectReducer, injectSagas } = getAsyncInjectors(store); 48 | 49 | injectReducer('test', reducer); 50 | injectSagas(sagas); 51 | 52 | const actual = store.getState().get('test'); 53 | const expected = initialState.merge({ reduced: 'yup' }); 54 | 55 | expect(actual.toJS()).toEqual(expected.toJS()); 56 | }); 57 | 58 | it('should throw if passed invalid store shape', () => { 59 | let result = false; 60 | 61 | Reflect.deleteProperty(store, 'dispatch'); 62 | 63 | try { 64 | getAsyncInjectors(store); 65 | } catch (err) { 66 | result = err.name === 'Invariant Violation'; 67 | } 68 | 69 | expect(result).toEqual(true); 70 | }); 71 | }); 72 | 73 | describe('helpers', () => { 74 | before(() => { 75 | store = configureStore({}, memoryHistory); 76 | }); 77 | 78 | describe('injectAsyncReducer', () => { 79 | it('given a store, it should provide a function to inject a reducer', () => { 80 | const injectReducer = injectAsyncReducer(store); 81 | 82 | injectReducer('test', reducer); 83 | 84 | const actual = store.getState().get('test'); 85 | const expected = initialState; 86 | 87 | expect(actual.toJS()).toEqual(expected.toJS()); 88 | }); 89 | 90 | it('should throw if passed invalid name', () => { 91 | let result = false; 92 | 93 | const injectReducer = injectAsyncReducer(store); 94 | 95 | try { 96 | injectReducer('', reducer); 97 | } catch (err) { 98 | result = err.name === 'Invariant Violation'; 99 | } 100 | 101 | try { 102 | injectReducer(999, reducer); 103 | } catch (err) { 104 | result = err.name === 'Invariant Violation'; 105 | } 106 | 107 | expect(result).toEqual(true); 108 | }); 109 | 110 | it('should throw if passed invalid reducer', () => { 111 | let result = false; 112 | 113 | const injectReducer = injectAsyncReducer(store); 114 | 115 | try { 116 | injectReducer('bad', 'nope'); 117 | } catch (err) { 118 | result = err.name === 'Invariant Violation'; 119 | } 120 | 121 | try { 122 | injectReducer('coolio', 12345); 123 | } catch (err) { 124 | result = err.name === 'Invariant Violation'; 125 | } 126 | 127 | expect(result).toEqual(true); 128 | }); 129 | }); 130 | 131 | describe('injectAsyncSagas', () => { 132 | it('given a store, it should provide a function to inject a saga', () => { 133 | const injectSagas = injectAsyncSagas(store); 134 | 135 | injectSagas(sagas); 136 | 137 | const actual = store.getState().get('test'); 138 | const expected = initialState.merge({ reduced: 'yup' }); 139 | 140 | expect(actual.toJS()).toEqual(expected.toJS()); 141 | }); 142 | 143 | it('should throw if passed invalid saga', () => { 144 | let result = false; 145 | 146 | const injectSagas = injectAsyncSagas(store); 147 | 148 | try { 149 | injectSagas({ testSaga }); 150 | } catch (err) { 151 | result = err.name === 'Invariant Violation'; 152 | } 153 | 154 | try { 155 | injectSagas(testSaga); 156 | } catch (err) { 157 | result = err.name === 'Invariant Violation'; 158 | } 159 | 160 | expect(result).toEqual(true); 161 | }); 162 | }); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /FrontEnd/appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Set build version format here instead of in the admin panel 4 | version: "{build}" 5 | 6 | # Do not build on gh tags 7 | skip_tags: true 8 | 9 | # Test against these versions of Node.js 10 | environment: 11 | 12 | matrix: 13 | # Node versions to run 14 | - nodejs_version: 6 15 | - nodejs_version: 5 16 | - nodejs_version: 4 17 | 18 | # Fix line endings in Windows. (runs before repo cloning) 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | # Install scripts--runs after repo cloning 23 | install: 24 | # Install chrome 25 | - choco install -y googlechrome 26 | # Install the latest stable version of Node 27 | - ps: Install-Product node $env:nodejs_version 28 | - npm -g install npm 29 | - set PATH=%APPDATA%\npm;%PATH% 30 | - npm install 31 | 32 | # Disable automatic builds 33 | build: off 34 | 35 | # Post-install test scripts 36 | test_script: 37 | # Output debugging info 38 | - node --version 39 | - npm --version 40 | # run build and run tests 41 | - npm run build 42 | 43 | # Cache node_modules for faster builds 44 | cache: 45 | - node_modules -> package.json 46 | 47 | # remove, as appveyor doesn't support secure variables on pr builds 48 | # so `COVERALLS_REPO_TOKEN` cannot be set, without hard-coding in this file 49 | #on_success: 50 | #- npm run coveralls 51 | -------------------------------------------------------------------------------- /FrontEnd/internals/config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('path').resolve; 2 | const pullAll = require('lodash/pullAll'); 3 | const uniq = require('lodash/uniq'); 4 | 5 | const ReactBoilerplate = { 6 | // This refers to the react-boilerplate version this project is based on. 7 | version: '3.3.3', 8 | 9 | /** 10 | * The DLL Plugin provides a dramatic speed increase to webpack build and hot module reloading 11 | * by caching the module metadata for all of our npm dependencies. We enable it by default 12 | * in development. 13 | * 14 | * 15 | * To disable the DLL Plugin, set this value to false. 16 | */ 17 | dllPlugin: { 18 | defaults: { 19 | /** 20 | * we need to exclude dependencies which are not intended for the browser 21 | * by listing them here. 22 | */ 23 | exclude: [ 24 | 'chalk', 25 | 'compression', 26 | 'cross-env', 27 | 'express', 28 | 'ip', 29 | 'minimist', 30 | 'sanitize.css', 31 | ], 32 | 33 | /** 34 | * Specify any additional dependencies here. We include core-js and lodash 35 | * since a lot of our dependencies depend on them and they get picked up by webpack. 36 | */ 37 | include: ['core-js', 'eventsource-polyfill', 'babel-polyfill', 'lodash'], 38 | 39 | // The path where the DLL manifest and bundle will get built 40 | path: resolve('../node_modules/react-boilerplate-dlls'), 41 | }, 42 | 43 | entry(pkg) { 44 | const dependencyNames = Object.keys(pkg.dependencies); 45 | const exclude = pkg.dllPlugin.exclude || ReactBoilerplate.dllPlugin.defaults.exclude; 46 | const include = pkg.dllPlugin.include || ReactBoilerplate.dllPlugin.defaults.include; 47 | const includeDependencies = uniq(dependencyNames.concat(include)); 48 | 49 | return { 50 | reactBoilerplateDeps: pullAll(includeDependencies, exclude), 51 | }; 52 | }, 53 | }, 54 | }; 55 | 56 | module.exports = ReactBoilerplate; 57 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | {{#if wantCSS}} 14 | import styles from './styles.css'; 15 | {{/if}} 16 | 17 | class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | } 32 | 33 | export default {{ properCase name }}; 34 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/component/es6.pure.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | {{#if wantCSS}} 14 | import styles from './styles.css'; 15 | {{/if}} 16 | 17 | class {{ properCase name }} extends React.PureComponent { // eslint-disable-line react/prefer-stateless-function 18 | render() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | } 32 | 33 | export default {{ properCase name }}; 34 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add an unconnected component', 9 | prompts: [{ 10 | type: 'list', 11 | name: 'type', 12 | message: 'Select the type of component', 13 | default: 'Stateless Function', 14 | choices: () => ['Stateless Function', 'ES6 Class (Pure)', 'ES6 Class'], 15 | }, { 16 | type: 'input', 17 | name: 'name', 18 | message: 'What should it be called?', 19 | default: 'Button', 20 | validate: (value) => { 21 | if ((/.+/).test(value)) { 22 | return componentExists(value) ? 'A component or container with this name already exists' : true; 23 | } 24 | 25 | return 'The name is required'; 26 | }, 27 | }, { 28 | type: 'confirm', 29 | name: 'wantMessages', 30 | default: true, 31 | message: 'Do you want i18n messages (i.e. will this component use text)?', 32 | }], 33 | actions: (data) => { 34 | // Generate index.js and index.test.js 35 | let componentTemplate; 36 | 37 | switch (data.type) { 38 | case 'ES6 Class': { 39 | componentTemplate = './component/es6.js.hbs'; 40 | break; 41 | } 42 | case 'ES6 Class (Pure)': { 43 | componentTemplate = './component/es6.pure.js.hbs'; 44 | break; 45 | } 46 | case 'Stateless Function': { 47 | componentTemplate = './component/stateless.js.hbs'; 48 | break; 49 | } 50 | default: { 51 | componentTemplate = './component/es6.js.hbs'; 52 | } 53 | } 54 | 55 | const actions = [{ 56 | type: 'add', 57 | path: '../../app/components/{{properCase name}}/index.js', 58 | templateFile: componentTemplate, 59 | abortOnFail: true, 60 | }, { 61 | type: 'add', 62 | path: '../../app/components/{{properCase name}}/tests/index.test.js', 63 | templateFile: './component/test.js.hbs', 64 | abortOnFail: true, 65 | }]; 66 | 67 | // If they want a i18n messages file 68 | if (data.wantMessages) { 69 | actions.push({ 70 | type: 'add', 71 | path: '../../app/components/{{properCase name}}/messages.js', 72 | templateFile: './component/messages.js.hbs', 73 | abortOnFail: true, 74 | }); 75 | } 76 | 77 | return actions; 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/component/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{ properCase name }} Messages 3 | * 4 | * This contains all the text for the {{ properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.components.{{ properCase name }}.header', 11 | defaultMessage: 'This is the {{ properCase name}} component !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantMessages}} 10 | import { FormattedMessage } from 'react-intl'; 11 | import messages from './messages'; 12 | {{/if}} 13 | 14 | {{#if wantCSS}} 15 | import styles from './styles.css'; 16 | {{/if}} 17 | 18 | function {{ properCase name }}() { 19 | return ( 20 | {{#if wantCSS}} 21 |
    22 | {{else}} 23 |
    24 | {{/if}} 25 | {{#if wantMessages}} 26 | 27 | {{/if}} 28 |
    29 | ); 30 | } 31 | 32 | export default {{ properCase name }}; 33 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Container Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add a container component', 9 | prompts: [{ 10 | type: 'input', 11 | name: 'name', 12 | message: 'What should it be called?', 13 | default: 'Form', 14 | validate: (value) => { 15 | if ((/.+/).test(value)) { 16 | return componentExists(value) ? 'A component or container with this name already exists' : true; 17 | } 18 | 19 | return 'The name is required'; 20 | }, 21 | }, { 22 | type: 'list', 23 | name: 'component', 24 | message: 'Select a base component:', 25 | default: 'PureComponent', 26 | choices: () => ['PureComponent', 'Component'], 27 | }, { 28 | type: 'confirm', 29 | name: 'wantHeaders', 30 | default: false, 31 | message: 'Do you want headers?', 32 | }, { 33 | type: 'confirm', 34 | name: 'wantActionsAndReducer', 35 | default: true, 36 | message: 'Do you want an actions/constants/selectors/reducer tupel for this container?', 37 | }, { 38 | type: 'confirm', 39 | name: 'wantSagas', 40 | default: true, 41 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)', 42 | }, { 43 | type: 'confirm', 44 | name: 'wantMessages', 45 | default: true, 46 | message: 'Do you want i18n messages (i.e. will this component use text)?', 47 | }], 48 | actions: (data) => { 49 | // Generate index.js and index.test.js 50 | const actions = [{ 51 | type: 'add', 52 | path: '../../app/containers/{{properCase name}}/index.js', 53 | templateFile: './container/index.js.hbs', 54 | abortOnFail: true, 55 | }, { 56 | type: 'add', 57 | path: '../../app/containers/{{properCase name}}/tests/index.test.js', 58 | templateFile: './container/test.js.hbs', 59 | abortOnFail: true, 60 | }]; 61 | 62 | // If component wants messages 63 | if (data.wantMessages) { 64 | actions.push({ 65 | type: 'add', 66 | path: '../../app/containers/{{properCase name}}/messages.js', 67 | templateFile: './container/messages.js.hbs', 68 | abortOnFail: true, 69 | }); 70 | } 71 | 72 | // If they want actions and a reducer, generate actions.js, constants.js, 73 | // reducer.js and the corresponding tests for actions and the reducer 74 | if (data.wantActionsAndReducer) { 75 | // Actions 76 | actions.push({ 77 | type: 'add', 78 | path: '../../app/containers/{{properCase name}}/actions.js', 79 | templateFile: './container/actions.js.hbs', 80 | abortOnFail: true, 81 | }); 82 | actions.push({ 83 | type: 'add', 84 | path: '../../app/containers/{{properCase name}}/tests/actions.test.js', 85 | templateFile: './container/actions.test.js.hbs', 86 | abortOnFail: true, 87 | }); 88 | 89 | // Constants 90 | actions.push({ 91 | type: 'add', 92 | path: '../../app/containers/{{properCase name}}/constants.js', 93 | templateFile: './container/constants.js.hbs', 94 | abortOnFail: true, 95 | }); 96 | 97 | // Selectors 98 | actions.push({ 99 | type: 'add', 100 | path: '../../app/containers/{{properCase name}}/selectors.js', 101 | templateFile: './container/selectors.js.hbs', 102 | abortOnFail: true, 103 | }); 104 | actions.push({ 105 | type: 'add', 106 | path: '../../app/containers/{{properCase name}}/tests/selectors.test.js', 107 | templateFile: './container/selectors.test.js.hbs', 108 | abortOnFail: true, 109 | }); 110 | 111 | // Reducer 112 | actions.push({ 113 | type: 'add', 114 | path: '../../app/containers/{{properCase name}}/reducer.js', 115 | templateFile: './container/reducer.js.hbs', 116 | abortOnFail: true, 117 | }); 118 | actions.push({ 119 | type: 'add', 120 | path: '../../app/containers/{{properCase name}}/tests/reducer.test.js', 121 | templateFile: './container/reducer.test.js.hbs', 122 | abortOnFail: true, 123 | }); 124 | } 125 | 126 | // Sagas 127 | if (data.wantSagas) { 128 | actions.push({ 129 | type: 'add', 130 | path: '../../app/containers/{{properCase name}}/sagas.js', 131 | templateFile: './container/sagas.js.hbs', 132 | abortOnFail: true, 133 | }); 134 | actions.push({ 135 | type: 'add', 136 | path: '../../app/containers/{{properCase name}}/tests/sagas.test.js', 137 | templateFile: './container/sagas.test.js.hbs', 138 | abortOnFail: true, 139 | }); 140 | } 141 | 142 | return actions; 143 | }, 144 | }; 145 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | {{#if wantHeaders}} 10 | import Helmet from 'react-helmet'; 11 | {{/if}} 12 | {{#if wantActionsAndReducer}} 13 | import select{{properCase name}} from './selectors'; 14 | {{/if}} 15 | {{#if wantMessages}} 16 | import { FormattedMessage } from 'react-intl'; 17 | import messages from './messages'; 18 | {{/if}} 19 | {{#if wantCSS}} 20 | import styles from './styles.css'; 21 | {{/if}} 22 | 23 | export class {{ properCase name }} extends React.{{{ component }}} { // eslint-disable-line react/prefer-stateless-function 24 | render() { 25 | return ( 26 | {{#if wantCSS}} 27 |
    28 | {{else}} 29 |
    30 | {{/if}} 31 | {{#if wantHeaders}} 32 | 38 | {{/if}} 39 | {{#if wantMessages}} 40 | 41 | {{/if}} 42 |
    43 | ); 44 | } 45 | } 46 | 47 | {{#if wantActionsAndReducer}} 48 | const mapStateToProps = select{{properCase name}}(); 49 | {{/if}} 50 | 51 | function mapDispatchToProps(dispatch) { 52 | return { 53 | dispatch, 54 | }; 55 | } 56 | 57 | {{#if wantActionsAndReducer}} 58 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 59 | {{else}} 60 | export default connect(null, mapDispatchToProps)({{ properCase name }}); 61 | {{/if}} 62 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/messages.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * {{properCase name }} Messages 3 | * 4 | * This contains all the text for the {{properCase name }} component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export default defineMessages({ 9 | header: { 10 | id: 'app.containers.{{properCase name }}.header', 11 | defaultMessage: 'This is {{properCase name}} container !', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function {{ camelCase name }}Reducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default {{ camelCase name }}Reducer; 24 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('{{ camelCase name }}Reducer', () => { 6 | it('returns the initial state', () => { 7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/sagas.js.hbs: -------------------------------------------------------------------------------- 1 | // import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // Individual exports for testing 4 | export function* defaultSaga() { 5 | return; 6 | } 7 | 8 | // All sagas to be loaded 9 | export default [ 10 | defaultSaga, 11 | ]; 12 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/sagas.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | // import { take, call, put, select } from 'redux-saga/effects'; 7 | // import { defaultSaga } from '../sagas'; 8 | 9 | // const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('Expect to have unit tests specified', () => { 13 | expect(true).toEqual(false); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the {{ camelCase name }} state domain 5 | */ 6 | const select{{ properCase name }}Domain = () => (state) => state.get('{{ camelCase name }}'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by {{ properCase name }} 15 | */ 16 | 17 | const select{{ properCase name }} = () => createSelector( 18 | select{{ properCase name }}Domain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default select{{ properCase name }}; 23 | export { 24 | select{{ properCase name }}Domain, 25 | }; 26 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { select{{ properCase name }}Domain } from '../selectors'; 2 | // import { fromJS } from 'immutable'; 3 | import expect from 'expect'; 4 | 5 | // const selector = select{{ properCase name}}Domain(); 6 | 7 | describe('select{{ properCase name }}Domain', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect('Test case').toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | // import { {{ properCase name }} } from '../index'; 2 | 3 | import expect from 'expect'; 4 | // import { shallow } from 'enzyme'; 5 | // import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | it('Expect to have unit tests specified', () => { 9 | expect(true).toEqual(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generator/index.js 3 | * 4 | * Exports the generators so plop knows them 5 | */ 6 | 7 | const fs = require('fs'); 8 | const componentGenerator = require('./component/index.js'); 9 | const containerGenerator = require('./container/index.js'); 10 | const routeGenerator = require('./route/index.js'); 11 | const languageGenerator = require('./language/index.js'); 12 | 13 | module.exports = (plop) => { 14 | plop.setGenerator('component', componentGenerator); 15 | plop.setGenerator('container', containerGenerator); 16 | plop.setGenerator('route', routeGenerator); 17 | plop.setGenerator('language', languageGenerator); 18 | plop.addHelper('directory', (comp) => { 19 | try { 20 | fs.accessSync(`app/containers/${comp}`, fs.F_OK); 21 | return `containers/${comp}`; 22 | } catch (e) { 23 | return `components/${comp}`; 24 | } 25 | }); 26 | plop.addHelper('curly', (object, open) => (open ? '{' : '}')); 27 | }; 28 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/add-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1addLocaleData({{language}}LocaleData); 2 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/app-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 '{{language}}', 2 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/format-translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1 {{language}}: formatTranslationMessages('{{language}}', {{language}}TranslationMessages), 2 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Language Generator 3 | */ 4 | const exec = require('child_process').exec; 5 | 6 | module.exports = { 7 | description: 'Add a language', 8 | prompts: [{ 9 | type: 'input', 10 | name: 'language', 11 | message: 'What is the language you want to add i18n support for (e.g. "fr", "de")?', 12 | default: 'fr', 13 | validate: (value) => { 14 | if ((/.+/).test(value) && value.length === 2) { 15 | return true; 16 | } 17 | 18 | return '2 character language specifier is required'; 19 | }, 20 | }], 21 | 22 | actions: () => { 23 | const actions = []; 24 | actions.push({ 25 | type: 'modify', 26 | path: '../../app/i18n.js', 27 | pattern: /('react-intl\/locale-data\/[a-z]+';\n)(?!.*'react-intl\/locale-data\/[a-z]+';)/g, 28 | templateFile: './language/intl-locale-data.hbs', 29 | }); 30 | actions.push({ 31 | type: 'modify', 32 | path: '../../app/i18n.js', 33 | pattern: /(\s+'[a-z]+',\n)(?!.*\s+'[a-z]+',)/g, 34 | templateFile: './language/app-locale.hbs', 35 | }); 36 | actions.push({ 37 | type: 'modify', 38 | path: '../../app/i18n.js', 39 | pattern: /(from\s'.\/translations\/[a-z]+.json';\n)(?!.*from\s'.\/translations\/[a-z]+.json';)/g, 40 | templateFile: './language/translation-messages.hbs', 41 | }); 42 | actions.push({ 43 | type: 'modify', 44 | path: '../../app/i18n.js', 45 | pattern: /(addLocaleData\([a-z]+LocaleData\);\n)(?!.*addLocaleData\([a-z]+LocaleData\);)/g, 46 | templateFile: './language/add-locale-data.hbs', 47 | }); 48 | actions.push({ 49 | type: 'modify', 50 | path: '../../app/i18n.js', 51 | pattern: /([a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),\n)(?!.*[a-z]+:\sformatTranslationMessages\('[a-z]+',\s[a-z]+TranslationMessages\),)/g, 52 | templateFile: './language/format-translation-messages.hbs', 53 | }); 54 | actions.push({ 55 | type: 'add', 56 | path: '../../app/translations/{{language}}.json', 57 | templateFile: './language/translations-json.hbs', 58 | abortOnFail: true, 59 | }); 60 | actions.push({ 61 | type: 'modify', 62 | path: '../../app/app.js', 63 | pattern: /(System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),\n)(?!.*System\.import\('intl\/locale-data\/jsonp\/[a-z]+\.js'\),)/g, 64 | templateFile: './language/polyfill-intl-locale.hbs', 65 | }); 66 | actions.push( 67 | () => { 68 | const cmd = 'npm run extract-intl'; 69 | exec(cmd, (err, result, stderr) => { 70 | if (err || stderr) { 71 | throw err || stderr; 72 | } 73 | process.stdout.write(result); 74 | }); 75 | } 76 | ); 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/intl-locale-data.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}LocaleData from 'react-intl/locale-data/{{language}}'; 2 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/polyfill-intl-locale.hbs: -------------------------------------------------------------------------------- 1 | $1 System.import('intl/locale-data/jsonp/{{language}}.js'), 2 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/translation-messages.hbs: -------------------------------------------------------------------------------- 1 | $1import {{language}}TranslationMessages from './translations/{{language}}.json'; 2 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/language/translations-json.hbs: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/route/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Generator 3 | */ 4 | const fs = require('fs'); 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | function reducerExists(comp) { 8 | try { 9 | fs.accessSync(`app/containers/${comp}/reducer.js`, fs.F_OK); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | } 15 | 16 | function sagasExists(comp) { 17 | try { 18 | fs.accessSync(`app/containers/${comp}/sagas.js`, fs.F_OK); 19 | return true; 20 | } catch (e) { 21 | return false; 22 | } 23 | } 24 | 25 | function trimTemplateFile(template) { 26 | // Loads the template file and trims the whitespace and then returns the content as a string. 27 | return fs.readFileSync(`internals/generators/route/${template}`, 'utf8').replace(/\s*$/, ''); 28 | } 29 | 30 | module.exports = { 31 | description: 'Add a route', 32 | prompts: [{ 33 | type: 'input', 34 | name: 'component', 35 | message: 'Which component should the route show?', 36 | validate: (value) => { 37 | if ((/.+/).test(value)) { 38 | return componentExists(value) ? true : `"${value}" doesn't exist.`; 39 | } 40 | 41 | return 'The path is required'; 42 | }, 43 | }, { 44 | type: 'input', 45 | name: 'path', 46 | message: 'Enter the path of the route.', 47 | default: '/about', 48 | validate: (value) => { 49 | if ((/.+/).test(value)) { 50 | return true; 51 | } 52 | 53 | return 'path is required'; 54 | }, 55 | }], 56 | 57 | // Add the route to the routes.js file above the error route 58 | // TODO smarter route adding 59 | actions: (data) => { 60 | const actions = []; 61 | if (reducerExists(data.component)) { 62 | data.useSagas = sagasExists(data.component); // eslint-disable-line no-param-reassign 63 | actions.push({ 64 | type: 'modify', 65 | path: '../../app/routes.js', 66 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 67 | template: trimTemplateFile('routeWithReducer.hbs'), 68 | }); 69 | } else { 70 | actions.push({ 71 | type: 'modify', 72 | path: '../../app/routes.js', 73 | pattern: /(\s{\n\s{0,}path: '\*',)/g, 74 | template: trimTemplateFile('route.hbs'), 75 | }); 76 | } 77 | 78 | return actions; 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/route/route.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(location, cb) { 5 | System.import('{{{directory (properCase component)}}}') 6 | .then(loadModule(cb)) 7 | .catch(errorLoading); 8 | }, 9 | },$1 10 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/route/routeWithReducer.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(nextState, cb) { 5 | const importModules = Promise.all([ 6 | System.import('containers/{{ properCase component }}/reducer'), 7 | {{#if useSagas}} 8 | System.import('containers/{{ properCase component }}/sagas'), 9 | {{/if}} 10 | System.import('containers/{{ properCase component }}'), 11 | ]); 12 | 13 | const renderRoute = loadModule(cb); 14 | 15 | importModules.then(([reducer,{{#if useSagas}} sagas,{{/if}} component]) => { 16 | injectReducer('{{ camelCase component }}', reducer.default); 17 | {{#if useSagas}} 18 | injectSagas(sagas.default); 19 | {{/if}} 20 | renderRoute(component); 21 | }); 22 | 23 | importModules.catch(errorLoading); 24 | }, 25 | },$1 26 | -------------------------------------------------------------------------------- /FrontEnd/internals/generators/utils/componentExists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * componentExists 3 | * 4 | * Check whether the given component exist in either the components or containers directory 5 | */ 6 | 7 | const fs = require('fs'); 8 | const pageComponents = fs.readdirSync('app/components'); 9 | const pageContainers = fs.readdirSync('app/containers'); 10 | const components = pageComponents.concat(pageContainers); 11 | 12 | function componentExists(comp) { 13 | return components.indexOf(comp) >= 0; 14 | } 15 | 16 | module.exports = componentExists; 17 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shelljs = require('shelljs'); 4 | const animateProgress = require('./helpers/progress'); 5 | const chalk = require('chalk'); 6 | const addCheckMark = require('./helpers/checkmark'); 7 | 8 | const progress = animateProgress('Generating stats'); 9 | 10 | // Generate stats.json file with webpack 11 | shelljs.exec( 12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 13 | addCheckMark.bind(null, callback) // Output a checkmark on completion 14 | ); 15 | 16 | // Called after webpack has finished generating the stats.json file 17 | function callback() { 18 | clearInterval(progress); 19 | process.stdout.write( 20 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' + 21 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n') 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | const addCheckMark = require('./helpers/checkmark.js'); 3 | 4 | if (!which('git')) { 5 | echo('Sorry, this script requires git'); 6 | exit(1); 7 | } 8 | 9 | if (!test('-e', 'internals/templates')) { 10 | echo('The example is deleted already.'); 11 | exit(1); 12 | } 13 | 14 | process.stdout.write('Cleanup started...'); 15 | 16 | // Cleanup components folder 17 | rm('-rf', 'app/components/*'); 18 | 19 | // Cleanup containers folder 20 | rm('-rf', 'app/containers/*'); 21 | mkdir('-p', 'app/containers/App'); 22 | mkdir('-p', 'app/containers/NotFoundPage'); 23 | mkdir('-p', 'app/containers/HomePage'); 24 | cp('internals/templates/appContainer.js', 'app/containers/App/index.js'); 25 | cp('internals/templates/constants.js', 'app/containers/App/constants.js'); 26 | cp('internals/templates/notFoundPage/notFoundPage.js', 'app/containers/NotFoundPage/index.js'); 27 | cp('internals/templates/notFoundPage/messages.js', 'app/containers/NotFoundPage/messages.js'); 28 | cp('internals/templates/homePage/homePage.js', 'app/containers/HomePage/index.js'); 29 | cp('internals/templates/homePage/messages.js', 'app/containers/HomePage/messages.js'); 30 | 31 | // Handle Translations 32 | rm('-rf', 'app/translations/*') 33 | mkdir('-p', 'app/translations'); 34 | cp('internals/templates/translations/en.json', 35 | 'app/translations/en.json'); 36 | 37 | // move i18n file 38 | cp('internals/templates/i18n.js', 39 | 'app/i18n.js'); 40 | 41 | // Copy LanguageProvider 42 | mkdir('-p', 'app/containers/LanguageProvider'); 43 | mkdir('-p', 'app/containers/LanguageProvider/tests'); 44 | cp('internals/templates/languageProvider/actions.js', 45 | 'app/containers/LanguageProvider/actions.js'); 46 | cp('internals/templates/languageProvider/constants.js', 47 | 'app/containers/LanguageProvider/constants.js'); 48 | cp('internals/templates/languageProvider/languageProvider.js', 49 | 'app/containers/LanguageProvider/index.js'); 50 | cp('internals/templates/languageProvider/reducer.js', 51 | 'app/containers/LanguageProvider/reducer.js'); 52 | cp('internals/templates/languageProvider/selectors.js', 53 | 'app/containers/LanguageProvider/selectors.js'); 54 | 55 | // Copy selectors 56 | mkdir('app/containers/App/tests'); 57 | cp('internals/templates/selectors.js', 58 | 'app/containers/App/selectors.js'); 59 | cp('internals/templates/selectors.test.js', 60 | 'app/containers/App/tests/selectors.test.js'); 61 | 62 | // Utils 63 | rm('-rf', 'app/utils'); 64 | mkdir('app/utils'); 65 | mkdir('app/utils/tests'); 66 | cp('internals/templates/asyncInjectors.js', 67 | 'app/utils/asyncInjectors.js'); 68 | cp('internals/templates/asyncInjectors.test.js', 69 | 'app/utils/tests/asyncInjectors.test.js'); 70 | 71 | // Replace the files in the root app/ folder 72 | cp('internals/templates/app.js', 'app/app.js'); 73 | cp('internals/templates/index.html', 'app/index.html'); 74 | cp('internals/templates/reducers.js', 'app/reducers.js'); 75 | cp('internals/templates/routes.js', 'app/routes.js'); 76 | cp('internals/templates/store.js', 'app/store.js'); 77 | cp('internals/templates/store.test.js', 'app/tests/store.test.js'); 78 | 79 | // Remove the templates folder 80 | rm('-rf', 'internals/templates'); 81 | 82 | addCheckMark(); 83 | 84 | // Commit the changes 85 | if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) { 86 | echo('\nError: Git commit failed'); 87 | exit(1); 88 | } 89 | 90 | echo('\nCleanup done. Happy Coding!!!'); 91 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/dependencies.js: -------------------------------------------------------------------------------- 1 | // No need to build the DLL in production 2 | if (process.env.NODE_ENV === 'production') { 3 | process.exit(0); 4 | } 5 | 6 | require('shelljs/global'); 7 | 8 | const path = require('path'); 9 | const fs = require('fs'); 10 | const exists = fs.existsSync; 11 | const writeFile = fs.writeFileSync; 12 | 13 | const defaults = require('lodash/defaultsDeep'); 14 | const pkg = require(path.join(process.cwd(), 'package.json')); 15 | const config = require('../config'); 16 | const dllConfig = defaults(pkg.dllPlugin, config.dllPlugin.defaults); 17 | const outputPath = path.join(process.cwd(), dllConfig.path); 18 | const dllManifestPath = path.join(outputPath, 'package.json'); 19 | 20 | /** 21 | * I use node_modules/react-boilerplate-dlls by default just because 22 | * it isn't going to be version controlled and babel wont try to parse it. 23 | */ 24 | mkdir('-p', outputPath); 25 | 26 | echo('Building the Webpack DLL...'); 27 | 28 | /** 29 | * Create a manifest so npm install doesn't warn us 30 | */ 31 | if (!exists(dllManifestPath)) { 32 | writeFile( 33 | dllManifestPath, 34 | JSON.stringify(defaults({ 35 | name: 'react-boilerplate-dlls', 36 | private: true, 37 | author: pkg.author, 38 | repository: pkg.repository, 39 | version: pkg.version, 40 | }), null, 2), 41 | 'utf8' 42 | ); 43 | } 44 | 45 | // the BUILDING_DLL env var is set to avoid confusing the development environment 46 | exec('cross-env BUILDING_DLL=true webpack --display-chunks --color --config internals/webpack/webpack.dll.babel.js'); 47 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/extract-intl.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * This script will extract the internationalization messages from all components 4 | and package them in the translation json files in the translations file. 5 | */ 6 | const fs = require('fs'); 7 | const nodeGlob = require('glob'); 8 | const transform = require('babel-core').transform; 9 | 10 | const animateProgress = require('./helpers/progress'); 11 | const addCheckmark = require('./helpers/checkmark'); 12 | 13 | const pkg = require('../../package.json'); 14 | const i18n = require('../../app/i18n'); 15 | import { DEFAULT_LOCALE } from '../../app/containers/App/constants'; 16 | 17 | require('shelljs/global'); 18 | 19 | // Glob to match all js files except test files 20 | const FILES_TO_PARSE = 'app/**/!(*.test).js'; 21 | const locales = i18n.appLocales; 22 | 23 | const newLine = () => process.stdout.write('\n'); 24 | 25 | // Progress Logger 26 | let progress; 27 | const task = (message) => { 28 | progress = animateProgress(message); 29 | process.stdout.write(message); 30 | 31 | return (error) => { 32 | if (error) { 33 | process.stderr.write(error); 34 | } 35 | clearTimeout(progress); 36 | return addCheckmark(() => newLine()); 37 | } 38 | } 39 | 40 | // Wrap async functions below into a promise 41 | const glob = (pattern) => new Promise((resolve, reject) => { 42 | nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value))); 43 | }); 44 | 45 | const readFile = (fileName) => new Promise((resolve, reject) => { 46 | fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value))); 47 | }); 48 | 49 | const writeFile = (fileName, data) => new Promise((resolve, reject) => { 50 | fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value))); 51 | }); 52 | 53 | // Store existing translations into memory 54 | const oldLocaleMappings = []; 55 | const localeMappings = []; 56 | // Loop to run once per locale 57 | for (const locale of locales) { 58 | oldLocaleMappings[locale] = {}; 59 | localeMappings[locale] = {}; 60 | // File to store translation messages into 61 | const translationFileName = `app/translations/${locale}.json`; 62 | try { 63 | // Parse the old translation message JSON files 64 | const messages = JSON.parse(fs.readFileSync(translationFileName)); 65 | const messageKeys = Object.keys(messages); 66 | for (const messageKey of messageKeys) { 67 | oldLocaleMappings[locale][messageKey] = messages[messageKey]; 68 | } 69 | } catch (error) { 70 | if (error.code !== 'ENOENT') { 71 | process.stderr.write( 72 | `There was an error loading this translation file: ${translationFileName} 73 | \n${error}` 74 | ); 75 | } 76 | } 77 | } 78 | 79 | const extractFromFile = async (fileName) => { 80 | try { 81 | const code = await readFile(fileName); 82 | // Use babel plugin to extract instances where react-intl is used 83 | const { metadata: result } = await transform(code, { 84 | presets: pkg.babel.presets, 85 | plugins: [ 86 | ['react-intl'], 87 | ], 88 | }); 89 | for (const message of result['react-intl'].messages) { 90 | for (const locale of locales) { 91 | const oldLocaleMapping = oldLocaleMappings[locale][message.id]; 92 | // Merge old translations into the babel extracted instances where react-intl is used 93 | const newMsg = ( locale === DEFAULT_LOCALE) ? message.defaultMessage : ''; 94 | localeMappings[locale][message.id] = (oldLocaleMapping) 95 | ? oldLocaleMapping 96 | : newMsg; 97 | } 98 | } 99 | } catch (error) { 100 | process.stderr.write(`Error transforming file: ${fileName}\n${error}`); 101 | } 102 | }; 103 | 104 | (async function main() { 105 | const memoryTaskDone = task('Storing language files in memory'); 106 | const files = await glob(FILES_TO_PARSE); 107 | memoryTaskDone() 108 | 109 | const extractTaskDone = task('Run extraction on all files'); 110 | // Run extraction on all files that match the glob on line 16 111 | await Promise.all(files.map((fileName) => extractFromFile(fileName))); 112 | extractTaskDone() 113 | 114 | // Make the directory if it doesn't exist, especially for first run 115 | mkdir('-p', 'app/translations'); 116 | for (const locale of locales) { 117 | const translationFileName = `app/translations/${locale}.json`; 118 | 119 | try { 120 | const localeTaskDone = task( 121 | `Writing translation messages for ${locale} to: ${translationFileName}` 122 | ); 123 | 124 | // Sort the translation JSON file so that git diffing is easier 125 | // Otherwise the translation messages will jump around every time we extract 126 | let messages = {}; 127 | Object.keys(localeMappings[locale]).sort().forEach(function(key) { 128 | messages[key] = localeMappings[locale][key]; 129 | }); 130 | 131 | // Write to file the JSON representation of the translation messages 132 | const prettified = `${JSON.stringify(messages, null, 2)}\n`; 133 | 134 | await writeFile(translationFileName, prettified); 135 | 136 | localeTaskDone(); 137 | } catch (error) { 138 | localeTaskDone( 139 | `There was an error saving this translation file: ${translationFileName} 140 | \n${error}` 141 | ); 142 | } 143 | } 144 | 145 | process.exit() 146 | }()); 147 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | if (callback) callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 12 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const readline = require('readline'); 4 | 5 | /** 6 | * Adds an animated progress indicator 7 | * 8 | * @param {string} message The message to write next to the indicator 9 | * @param {number} amountOfDots The amount of dots you want to animate 10 | */ 11 | function animateProgress(message, amountOfDots) { 12 | if (typeof amountOfDots !== 'number') { 13 | amountOfDots = 3; 14 | } 15 | 16 | let i = 0; 17 | return setInterval(function() { 18 | readline.cursorTo(process.stdout, 0); 19 | i = (i + 1) % (amountOfDots + 1); 20 | const dots = new Array(i + 1).join('.'); 21 | process.stdout.write(message + dots); 22 | }, 500); 23 | } 24 | 25 | module.exports = animateProgress; 26 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | exec('npm -v', function (err, stdout, stderr) { 3 | if (err) throw err; 4 | if (parseFloat(stdout) < 3) { 5 | throw new Error('[ERROR: React Boilerplate] You need npm version @>=3'); 6 | process.exit(1); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/pagespeed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.stdin.resume(); 4 | process.stdin.setEncoding('utf8'); 5 | 6 | const ngrok = require('ngrok'); 7 | const psi = require('psi'); 8 | const chalk = require('chalk'); 9 | 10 | log('\nStarting ngrok tunnel'); 11 | 12 | startTunnel(runPsi); 13 | 14 | function runPsi(url) { 15 | log('\nStarting PageSpeed Insights'); 16 | psi.output(url).then(function (err) { 17 | process.exit(0); 18 | }); 19 | } 20 | 21 | function startTunnel(cb) { 22 | ngrok.connect(3000, function (err, url) { 23 | if (err) { 24 | log(chalk.red('\nERROR\n' + err)); 25 | process.exit(0); 26 | } 27 | 28 | log('\nServing tunnel from: ' + chalk.magenta(url)); 29 | cb(url); 30 | }); 31 | } 32 | 33 | function log(string) { 34 | process.stdout.write(string); 35 | } 36 | -------------------------------------------------------------------------------- /FrontEnd/internals/scripts/setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const shell = require('shelljs'); 6 | const exec = require('child_process').exec; 7 | const path = require('path'); 8 | const fs = require('fs'); 9 | const animateProgress = require('./helpers/progress'); 10 | const addCheckMark = require('./helpers/checkmark'); 11 | const readline = require('readline'); 12 | 13 | process.stdin.resume(); 14 | process.stdin.setEncoding('utf8'); 15 | 16 | process.stdout.write('\n'); 17 | let interval = animateProgress('Cleaning old repository'); 18 | process.stdout.write('Cleaning old repository'); 19 | 20 | cleanRepo(function () { 21 | clearInterval(interval); 22 | process.stdout.write('\nInstalling dependencies... (This might take a while)'); 23 | setTimeout(function () { 24 | readline.cursorTo(process.stdout, 0); 25 | interval = animateProgress('Installing dependencies'); 26 | }, 500); 27 | 28 | process.stdout.write('Installing dependencies'); 29 | installDeps(); 30 | }); 31 | 32 | /** 33 | * Deletes the .git folder in dir 34 | */ 35 | function cleanRepo(callback) { 36 | shell.rm('-rf', '.git/'); 37 | addCheckMark(callback); 38 | } 39 | 40 | /** 41 | * Initializes git again 42 | */ 43 | function initGit(callback) { 44 | exec('git init && git add . && git commit -m "Initial commit"', addCheckMark.bind(null, callback)); 45 | } 46 | 47 | /** 48 | * Deletes a file in the current directory 49 | */ 50 | function deleteFileInCurrentDir(file, callback) { 51 | fs.unlink(path.join(__dirname, file), callback); 52 | } 53 | 54 | /** 55 | * Installs dependencies 56 | */ 57 | function installDeps() { 58 | exec('yarn --version', function (err, stdout, stderr) { 59 | if (parseFloat(stdout) < 0.15 || err || process.env.USE_YARN === 'false') { 60 | exec('npm install', addCheckMark.bind(null, installDepsCallback)); 61 | } else { 62 | exec('yarn install', addCheckMark.bind(null, installDepsCallback)); 63 | } 64 | }); 65 | } 66 | 67 | /** 68 | * Callback function after installing dependencies 69 | */ 70 | function installDepsCallback(error) { 71 | clearInterval(interval); 72 | if (error) { 73 | process.stdout.write(error); 74 | } 75 | 76 | process.stdout.write('\n'); 77 | interval = animateProgress('Initialising new repository'); 78 | process.stdout.write('Initialising new repository'); 79 | initGit(function () { 80 | clearInterval(interval); 81 | process.stdout.write('\nDone!'); 82 | process.exit(0); 83 | }); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /FrontEnd/internals/testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack/webpack.test.babel'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const path = require('path'); 4 | 5 | module.exports = (config) => { 6 | config.set({ 7 | frameworks: ['mocha'], 8 | reporters: ['coverage', 'mocha'], 9 | browsers: process.env.TRAVIS // eslint-disable-line no-nested-ternary 10 | ? ['ChromeTravis'] 11 | : process.env.APPVEYOR 12 | ? ['IE'] : ['Chrome'], 13 | 14 | autoWatch: false, 15 | singleRun: true, 16 | 17 | client: { 18 | mocha: { 19 | grep: argv.grep, 20 | }, 21 | }, 22 | 23 | files: [ 24 | { 25 | pattern: './test-bundler.js', 26 | watched: false, 27 | served: true, 28 | included: true, 29 | }, 30 | ], 31 | 32 | preprocessors: { 33 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key 34 | }, 35 | 36 | webpack: webpackConfig, 37 | 38 | // make Webpack bundle generation quiet 39 | webpackMiddleware: { 40 | noInfo: true, 41 | stats: 'errors-only', 42 | }, 43 | 44 | customLaunchers: { 45 | ChromeTravis: { 46 | base: 'Chrome', 47 | flags: ['--no-sandbox'], 48 | }, 49 | }, 50 | 51 | coverageReporter: { 52 | dir: path.join(process.cwd(), 'coverage'), 53 | reporters: [ 54 | { type: 'lcov', subdir: 'lcov' }, 55 | { type: 'html', subdir: 'html' }, 56 | { type: 'text-summary' }, 57 | ], 58 | }, 59 | 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /FrontEnd/internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | 5 | // If we need to use Chai, we'll have already chaiEnzyme loaded 6 | import chai from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | chai.use(chaiEnzyme()); 9 | 10 | // Include all .js files under `app`, except app.js, reducers.js, and routes.js. 11 | // This is for code coverage 12 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes)).)*\.js$/); 13 | context.keys().forEach(context); 14 | -------------------------------------------------------------------------------- /FrontEnd/internals/webpack/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * COMMON WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = (options) => ({ 9 | entry: options.entry, 10 | output: Object.assign({ // Compile into js/build.js 11 | path: path.resolve(process.cwd(), 'build'), 12 | publicPath: '/', 13 | }, options.output), // Merge with env dependent settings 14 | module: { 15 | loaders: [{ 16 | test: /\.js$/, // Transform all .js files required somewhere with Babel 17 | loader: 'babel', 18 | exclude: /node_modules/, 19 | query: options.babelQuery, 20 | }, { 21 | // Do not transform vendor's CSS with CSS-modules 22 | // The point is that they remain in global scope. 23 | // Since we require these CSS files in our JS or CSS files, 24 | // they will be a part of our compilation either way. 25 | // So, no need for ExtractTextPlugin here. 26 | test: /\.css$/, 27 | include: /node_modules/, 28 | loaders: ['style-loader', 'css-loader'], 29 | }, { 30 | test: /\.(eot|svg|ttf|woff|woff2)$/, 31 | loader: 'file-loader', 32 | }, { 33 | test: /\.(jpg|png|gif)$/, 34 | loaders: [ 35 | 'file-loader', 36 | 'image-webpack?{progressive:true, optimizationLevel: 7, interlaced: false, pngquant:{quality: "65-90", speed: 4}}', 37 | ], 38 | }, { 39 | test: /\.html$/, 40 | loader: 'html-loader', 41 | }, { 42 | test: /\.json$/, 43 | loader: 'json-loader', 44 | }, { 45 | test: /\.(mp4|webm)$/, 46 | loader: 'url-loader?limit=10000', 47 | }], 48 | }, 49 | plugins: options.plugins.concat([ 50 | new webpack.ProvidePlugin({ 51 | // make fetch available 52 | fetch: 'exports?self.fetch!whatwg-fetch', 53 | }), 54 | 55 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 56 | // inside your code for any environment checks; UglifyJS will automatically 57 | // drop any unreachable code. 58 | new webpack.DefinePlugin({ 59 | 'process.env': { 60 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 61 | }, 62 | }), 63 | new webpack.NamedModulesPlugin(), 64 | ]), 65 | resolve: { 66 | modules: ['app', 'node_modules'], 67 | extensions: [ 68 | '.js', 69 | '.jsx', 70 | '.react.js', 71 | ], 72 | mainFields: [ 73 | 'browser', 74 | 'jsnext:main', 75 | 'main', 76 | ], 77 | }, 78 | devtool: options.devtool, 79 | target: 'web', // Make web variables accessible to webpack, e.g. window 80 | }); 81 | -------------------------------------------------------------------------------- /FrontEnd/internals/webpack/webpack.dev.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DEVELOPMENT WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const webpack = require('webpack'); 8 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | const logger = require('../../server/logger'); 10 | const cheerio = require('cheerio'); 11 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 12 | const dllPlugin = pkg.dllPlugin; 13 | 14 | const plugins = [ 15 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading 16 | new webpack.NoErrorsPlugin(), 17 | new HtmlWebpackPlugin({ 18 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js 19 | templateContent: templateContent(), // eslint-disable-line no-use-before-define 20 | }), 21 | ]; 22 | 23 | module.exports = require('./webpack.base.babel')({ 24 | // Add hot reloading in development 25 | entry: [ 26 | 'eventsource-polyfill', // Necessary for hot reloading with IE 27 | 'webpack-hot-middleware/client', 28 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js 29 | ], 30 | 31 | // Don't use hashes in dev mode for better performance 32 | output: { 33 | filename: '[name].js', 34 | chunkFilename: '[name].chunk.js', 35 | }, 36 | 37 | // Add development plugins 38 | plugins: dependencyHandlers().concat(plugins), // eslint-disable-line no-use-before-define 39 | 40 | // Tell babel that we want to hot-reload 41 | babelQuery: { 42 | presets: ['react-hmre'], 43 | }, 44 | 45 | // Emit a source map for easier debugging 46 | devtool: 'cheap-module-eval-source-map', 47 | }); 48 | 49 | /** 50 | * Select which plugins to use to optimize the bundle's handling of 51 | * third party dependencies. 52 | * 53 | * If there is a dllPlugin key on the project's package.json, the 54 | * Webpack DLL Plugin will be used. Otherwise the CommonsChunkPlugin 55 | * will be used. 56 | * 57 | */ 58 | function dependencyHandlers() { 59 | // Don't do anything during the DLL Build step 60 | if (process.env.BUILDING_DLL) { return []; } 61 | 62 | // If the package.json does not have a dllPlugin property, use the CommonsChunkPlugin 63 | if (!dllPlugin) { 64 | return [ 65 | new webpack.optimize.CommonsChunkPlugin({ 66 | name: 'vendor', 67 | children: true, 68 | minChunks: 2, 69 | async: true, 70 | }), 71 | ]; 72 | } 73 | 74 | const dllPath = path.resolve(process.cwd(), dllPlugin.path || 'node_modules/react-boilerplate-dlls'); 75 | 76 | /** 77 | * If DLLs aren't explicitly defined, we assume all production dependencies listed in package.json 78 | * Reminder: You need to exclude any server side dependencies by listing them in dllConfig.exclude 79 | * 80 | * @see https://github.com/mxstbr/react-boilerplate/tree/master/docs/general/webpack.md 81 | */ 82 | if (!dllPlugin.dlls) { 83 | const manifestPath = path.resolve(dllPath, 'reactBoilerplateDeps.json'); 84 | 85 | if (!fs.existsSync(manifestPath)) { 86 | logger.error('The DLL manifest is missing. Please run `npm run build:dll`'); 87 | process.exit(0); 88 | } 89 | 90 | return [ 91 | new webpack.DllReferencePlugin({ 92 | context: process.cwd(), 93 | manifest: require(manifestPath), // eslint-disable-line global-require 94 | }), 95 | ]; 96 | } 97 | 98 | // If DLLs are explicitly defined, we automatically create a DLLReferencePlugin for each of them. 99 | const dllManifests = Object.keys(dllPlugin.dlls).map((name) => path.join(dllPath, `/${name}.json`)); 100 | 101 | return dllManifests.map((manifestPath) => { 102 | if (!fs.existsSync(path)) { 103 | if (!fs.existsSync(manifestPath)) { 104 | logger.error(`The following Webpack DLL manifest is missing: ${path.basename(manifestPath)}`); 105 | logger.error(`Expected to find it in ${dllPath}`); 106 | logger.error('Please run: npm run build:dll'); 107 | 108 | process.exit(0); 109 | } 110 | } 111 | 112 | return new webpack.DllReferencePlugin({ 113 | context: process.cwd(), 114 | manifest: require(manifestPath), // eslint-disable-line global-require 115 | }); 116 | }); 117 | } 118 | 119 | /** 120 | * We dynamically generate the HTML content in development so that the different 121 | * DLL Javascript files are loaded in script tags and available to our application. 122 | */ 123 | function templateContent() { 124 | const html = fs.readFileSync( 125 | path.resolve(process.cwd(), 'app/index.html') 126 | ).toString(); 127 | 128 | if (!dllPlugin) { return html; } 129 | 130 | const doc = cheerio(html); 131 | const body = doc.find('body'); 132 | const dllNames = !dllPlugin.dlls ? ['reactBoilerplateDeps'] : Object.keys(dllPlugin.dlls); 133 | 134 | dllNames.forEach((dllName) => body.append(``)); 135 | 136 | return doc.toString(); 137 | } 138 | -------------------------------------------------------------------------------- /FrontEnd/internals/webpack/webpack.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WEBPACK DLL GENERATOR 3 | * 4 | * This profile is used to cache webpack's module 5 | * contexts for external library and framework type 6 | * dependencies which will usually not change often enough 7 | * to warrant building them from scratch every time we use 8 | * the webpack process. 9 | */ 10 | 11 | const { join } = require('path'); 12 | const defaults = require('lodash/defaultsDeep'); 13 | const webpack = require('webpack'); 14 | const pkg = require(join(process.cwd(), 'package.json')); 15 | const dllPlugin = require('../config').dllPlugin; 16 | 17 | if (!pkg.dllPlugin) { process.exit(0); } 18 | 19 | const dllConfig = defaults(pkg.dllPlugin, dllPlugin.defaults); 20 | const outputPath = join(process.cwd(), dllConfig.path); 21 | 22 | module.exports = require('./webpack.base.babel')({ 23 | context: process.cwd(), 24 | entry: dllConfig.dlls ? dllConfig.dlls : dllPlugin.entry(pkg), 25 | devtool: 'eval', 26 | output: { 27 | filename: '[name].dll.js', 28 | path: outputPath, 29 | library: '[name]', 30 | }, 31 | plugins: [ 32 | new webpack.DllPlugin({ name: '[name]', path: join(outputPath, '[name].json') }), // eslint-disable-line no-new 33 | ], 34 | }); 35 | -------------------------------------------------------------------------------- /FrontEnd/internals/webpack/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | // Important modules this config uses 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const OfflinePlugin = require('offline-plugin'); 6 | 7 | module.exports = require('./webpack.base.babel')({ 8 | // In production, we skip all hot-reloading stuff 9 | entry: [ 10 | path.join(process.cwd(), 'app/app.js'), 11 | ], 12 | 13 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets 14 | output: { 15 | filename: '[name].[chunkhash].js', 16 | chunkFilename: '[name].[chunkhash].chunk.js', 17 | }, 18 | 19 | plugins: [ 20 | new webpack.optimize.CommonsChunkPlugin({ 21 | name: 'vendor', 22 | children: true, 23 | minChunks: 2, 24 | async: true, 25 | }), 26 | 27 | // Merge all duplicate modules 28 | new webpack.optimize.DedupePlugin(), 29 | 30 | // Minify and optimize the index.html 31 | new HtmlWebpackPlugin({ 32 | template: 'app/index.html', 33 | minify: { 34 | removeComments: true, 35 | collapseWhitespace: true, 36 | removeRedundantAttributes: true, 37 | useShortDoctype: true, 38 | removeEmptyAttributes: true, 39 | removeStyleLinkTypeAttributes: true, 40 | keepClosingSlash: true, 41 | minifyJS: true, 42 | minifyCSS: true, 43 | minifyURLs: true, 44 | }, 45 | inject: true, 46 | }), 47 | 48 | // Put it in the end to capture all the HtmlWebpackPlugin's 49 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin 50 | new OfflinePlugin({ 51 | relativePaths: false, 52 | publicPath: '/', 53 | 54 | // No need to cache .htaccess. See http://mxs.is/googmp, 55 | // this is applied before any match in `caches` section 56 | excludes: ['.htaccess'], 57 | 58 | caches: { 59 | main: [':rest:'], 60 | 61 | // All chunks marked as `additional`, loaded after main section 62 | // and do not prevent SW to install. Change to `optional` if 63 | // do not want them to be preloaded at all (cached only when first loaded) 64 | additional: ['*.chunk.js'], 65 | }, 66 | 67 | // Removes warning for about `additional` section usage 68 | safeToUseOptionalCaches: true, 69 | 70 | AppCache: false, 71 | }), 72 | ], 73 | }); 74 | -------------------------------------------------------------------------------- /FrontEnd/internals/webpack/webpack.test.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST WEBPACK CONFIGURATION 3 | */ 4 | 5 | const webpack = require('webpack'); 6 | const modules = [ 7 | 'app', 8 | 'node_modules', 9 | ]; 10 | 11 | module.exports = { 12 | devtool: 'inline-source-map', 13 | module: { 14 | // Some libraries don't like being run through babel. 15 | // If they gripe, put them here. 16 | noParse: [ 17 | /node_modules(\\|\/)sinon/, 18 | /node_modules(\\|\/)acorn/, 19 | ], 20 | loaders: [ 21 | { test: /\.json$/, loader: 'json-loader' }, 22 | { test: /\.css$/, loader: 'null-loader' }, 23 | 24 | // sinon.js--aliased for enzyme--expects/requires global vars. 25 | // imports-loader allows for global vars to be injected into the module. 26 | // See https://github.com/webpack/webpack/issues/304 27 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 28 | loader: 'imports?define=>false,require=>false', 29 | }, 30 | { test: /\.js$/, 31 | loader: 'babel', 32 | exclude: [/node_modules/], 33 | }, 34 | { test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i, 35 | loader: 'null-loader', 36 | }, 37 | ], 38 | }, 39 | 40 | plugins: [ 41 | 42 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 43 | // inside your code for any environment checks; UglifyJS will automatically 44 | // drop any unreachable code. 45 | new webpack.DefinePlugin({ 46 | 'process.env': { 47 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 48 | }, 49 | })], 50 | 51 | // Some node_modules pull in Node-specific dependencies. 52 | // Since we're running in a browser we have to stub them out. See: 53 | // https://webpack.github.io/docs/configuration.html#node 54 | // https://github.com/webpack/node-libs-browser/tree/master/mock 55 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 56 | node: { 57 | fs: 'empty', 58 | child_process: 'empty', 59 | net: 'empty', 60 | tls: 'empty', 61 | }, 62 | 63 | // required for enzyme to work properly 64 | externals: { 65 | jsdom: 'window', 66 | 'react/addons': true, 67 | 'react/lib/ExecutionEnvironment': true, 68 | 'react/lib/ReactContext': 'window', 69 | }, 70 | resolve: { 71 | modules, 72 | alias: { 73 | // required for enzyme to work properly 74 | sinon: 'sinon/pkg/sinon', 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /FrontEnd/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "version": "", 4 | "description": "", 5 | "repository": { 6 | "type": "git", 7 | "url": "" 8 | }, 9 | "engines": { 10 | "npm": ">=3" 11 | }, 12 | "license": "MIT", 13 | "scripts": { 14 | "analyze:clean": "rimraf stats.json", 15 | "preanalyze": "npm run analyze:clean", 16 | "analyze": "node ./internals/scripts/analyze.js", 17 | "extract-intl": "babel-node --presets latest,stage-0 -- ./internals/scripts/extract-intl.js", 18 | "npmcheckversion": "node ./internals/scripts/npmcheckversion.js", 19 | "preinstall": "npm run npmcheckversion", 20 | "postinstall": "npm run build:dll", 21 | "prebuild": "npm run build:clean && npm run test", 22 | "build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.babel.js --color -p --progress", 23 | "build:clean": "npm run test:clean && rimraf ./build", 24 | "build:dll": "node ./internals/scripts/dependencies.js", 25 | "start": "cross-env NODE_ENV=development node server", 26 | "start:tunnel": "cross-env NODE_ENV=development ENABLE_TUNNEL=true node server", 27 | "start:production": "npm run build && npm run start:prod", 28 | "start:prod": "cross-env NODE_ENV=production node server", 29 | "pagespeed": "node ./internals/scripts/pagespeed.js", 30 | "presetup": "npm i chalk shelljs", 31 | "setup": "node ./internals/scripts/setup.js", 32 | "postsetup": "npm run build:dll", 33 | "clean": "shjs ./internals/scripts/clean.js", 34 | "clean:all": "npm run analyze:clean && npm run test:clean && npm run build:clean", 35 | "generate": "plop --plopfile internals/generators/index.js", 36 | "lint": "npm run lint:js", 37 | "lint:eslint": "eslint --ignore-path .gitignore --ignore-pattern internals/scripts", 38 | "lint:js": "npm run lint:eslint -- . ", 39 | "lint:staged": "lint-staged", 40 | "pretest": "npm run test:clean", 41 | "test:clean": "rimraf ./coverage", 42 | "test": "cross-env NODE_ENV=test karma start internals/testing/karma.conf.js --single-run", 43 | "test:watch": "npm run test -- --auto-watch --no-single-run", 44 | "test:firefox": "npm run test -- --browsers Firefox", 45 | "test:safari": "npm run test -- --browsers Safari", 46 | "test:ie": "npm run test -- --browsers IE", 47 | "coveralls": "cat ./coverage/lcov/lcov.info | coveralls" 48 | }, 49 | "lint-staged": { 50 | "*.js": "lint:eslint" 51 | }, 52 | "pre-commit": "lint:staged", 53 | "babel": { 54 | "presets": [ 55 | [ 56 | "latest", 57 | { 58 | "es2015": { 59 | "modules": false 60 | } 61 | } 62 | ], 63 | "react", 64 | "stage-0" 65 | ], 66 | "env": { 67 | "production": { 68 | "only": [ 69 | "app" 70 | ], 71 | "plugins": [ 72 | "transform-react-remove-prop-types", 73 | "transform-react-constant-elements", 74 | "transform-react-inline-elements" 75 | ] 76 | }, 77 | "test": { 78 | "plugins": [ 79 | "istanbul" 80 | ] 81 | } 82 | } 83 | }, 84 | "eslintConfig": { 85 | "parser": "babel-eslint", 86 | "extends": "airbnb", 87 | "env": { 88 | "browser": true, 89 | "node": true, 90 | "mocha": true, 91 | "es6": true 92 | }, 93 | "plugins": [ 94 | "redux-saga", 95 | "react", 96 | "jsx-a11y" 97 | ], 98 | "parserOptions": { 99 | "ecmaVersion": 6, 100 | "sourceType": "module", 101 | "ecmaFeatures": { 102 | "jsx": true 103 | } 104 | }, 105 | "rules": { 106 | "arrow-parens": [ 107 | "error", 108 | "always" 109 | ], 110 | "arrow-body-style": [ 111 | 2, 112 | "as-needed" 113 | ], 114 | "comma-dangle": [ 115 | 2, 116 | "always-multiline" 117 | ], 118 | "import/imports-first": 0, 119 | "import/newline-after-import": 0, 120 | "import/no-dynamic-require": 0, 121 | "import/no-extraneous-dependencies": 0, 122 | "import/no-named-as-default": 0, 123 | "import/no-unresolved": 2, 124 | "import/prefer-default-export": 0, 125 | "indent": [ 126 | 2, 127 | 2, 128 | { 129 | "SwitchCase": 1 130 | } 131 | ], 132 | "jsx-a11y/aria-props": 2, 133 | "jsx-a11y/heading-has-content": 0, 134 | "jsx-a11y/href-no-hash": 2, 135 | "jsx-a11y/label-has-for": 2, 136 | "jsx-a11y/mouse-events-have-key-events": 2, 137 | "jsx-a11y/role-has-required-aria-props": 2, 138 | "jsx-a11y/role-supports-aria-props": 2, 139 | "max-len": 0, 140 | "newline-per-chained-call": 0, 141 | "no-console": 1, 142 | "no-use-before-define": 0, 143 | "prefer-template": 2, 144 | "class-methods-use-this": 0, 145 | "react/forbid-prop-types": 0, 146 | "react/jsx-first-prop-new-line": [ 147 | 2, 148 | "multiline" 149 | ], 150 | "react/jsx-filename-extension": 0, 151 | "react/jsx-no-target-blank": 0, 152 | "react/require-extension": 0, 153 | "react/self-closing-comp": 0, 154 | "redux-saga/no-yield-in-race": 2, 155 | "redux-saga/yield-effects": 2, 156 | "require-yield": 0 157 | }, 158 | "settings": { 159 | "import/resolver": { 160 | "webpack": { 161 | "config": "./internals/webpack/webpack.test.babel.js" 162 | } 163 | } 164 | } 165 | }, 166 | "dllPlugin": { 167 | "path": "node_modules/react-boilerplate-dlls", 168 | "exclude": [ 169 | "chalk", 170 | "compression", 171 | "cross-env", 172 | "express", 173 | "ip", 174 | "minimist", 175 | "sanitize.css" 176 | ], 177 | "include": [ 178 | "core-js", 179 | "lodash", 180 | "eventsource-polyfill" 181 | ] 182 | }, 183 | "dependencies": { 184 | "babel-polyfill": "6.16.0", 185 | "chalk": "1.1.3", 186 | "compression": "1.6.2", 187 | "cross-env": "3.1.3", 188 | "express": "4.14.0", 189 | "fontfaceobserver": "2.0.5", 190 | "http-proxy-middleware": "^0.17.3", 191 | "immutable": "3.8.1", 192 | "intl": "1.2.5", 193 | "invariant": "2.2.1", 194 | "ip": "1.1.3", 195 | "jwt-decode": "^2.1.0", 196 | "lodash": "4.16.4", 197 | "minimist": "1.2.0", 198 | "react": "15.3.2", 199 | "react-dom": "15.3.2", 200 | "react-helmet": "3.1.0", 201 | "react-intl": "2.1.5", 202 | "react-redux": "4.4.5", 203 | "react-router": "3.0.0", 204 | "react-router-redux": "4.0.6", 205 | "react-router-scroll": "0.3.3", 206 | "redux": "3.6.0", 207 | "redux-auth-wrapper": "^0.9.0", 208 | "redux-immutable": "3.0.8", 209 | "redux-saga": "0.12.0", 210 | "reselect": "2.5.4", 211 | "sanitize.css": "4.1.0", 212 | "styled-components": "1.0.3", 213 | "warning": "3.0.0", 214 | "whatwg-fetch": "1.0.0" 215 | }, 216 | "devDependencies": { 217 | "babel-cli": "6.18.0", 218 | "babel-core": "6.18.0", 219 | "babel-eslint": "7.1.0", 220 | "babel-loader": "6.2.7", 221 | "babel-plugin-istanbul": "2.0.3", 222 | "babel-plugin-react-intl": "2.2.0", 223 | "babel-plugin-react-transform": "2.0.2", 224 | "babel-plugin-transform-react-constant-elements": "6.9.1", 225 | "babel-plugin-transform-react-inline-elements": "6.8.0", 226 | "babel-plugin-transform-react-remove-prop-types": "0.2.10", 227 | "babel-preset-latest": "6.16.0", 228 | "babel-preset-react": "6.16.0", 229 | "babel-preset-react-hmre": "1.1.1", 230 | "babel-preset-stage-0": "6.16.0", 231 | "chai": "3.5.0", 232 | "chai-enzyme": "0.5.2", 233 | "cheerio": "0.22.0", 234 | "coveralls": "2.11.14", 235 | "css-loader": "0.25.0", 236 | "enzyme": "2.5.1", 237 | "eslint": "3.9.0", 238 | "eslint-config-airbnb": "12.0.0", 239 | "eslint-config-airbnb-base": "9.0.0", 240 | "eslint-import-resolver-webpack": "0.6.0", 241 | "eslint-plugin-import": "2.0.1", 242 | "eslint-plugin-jsx-a11y": "2.2.3", 243 | "eslint-plugin-react": "6.4.1", 244 | "eslint-plugin-redux-saga": "0.1.5", 245 | "eventsource-polyfill": "0.9.6", 246 | "expect": "1.20.2", 247 | "expect-jsx": "2.6.0", 248 | "exports-loader": "0.6.3", 249 | "file-loader": "0.9.0", 250 | "html-loader": "0.4.4", 251 | "html-webpack-plugin": "2.24.0", 252 | "image-webpack-loader": "2.0.0", 253 | "imports-loader": "0.6.5", 254 | "json-loader": "0.5.4", 255 | "karma": "1.3.0", 256 | "karma-chrome-launcher": "2.0.0", 257 | "karma-coverage": "1.1.1", 258 | "karma-firefox-launcher": "1.0.0", 259 | "karma-ie-launcher": "1.0.0", 260 | "karma-mocha": "1.2.0", 261 | "karma-mocha-reporter": "2.2.0", 262 | "karma-safari-launcher": "1.0.0", 263 | "karma-sourcemap-loader": "0.3.7", 264 | "karma-webpack": "1.8.0", 265 | "lint-staged": "3.2.0", 266 | "mocha": "3.1.2", 267 | "ngrok": "2.2.3", 268 | "null-loader": "0.1.1", 269 | "offline-plugin": "3.4.2", 270 | "plop": "1.5.0", 271 | "pre-commit": "1.1.3", 272 | "psi": "2.0.4", 273 | "react-addons-test-utils": "15.3.2", 274 | "rimraf": "2.5.4", 275 | "shelljs": "0.7.5", 276 | "sinon": "2.0.0-pre", 277 | "style-loader": "0.13.1", 278 | "url-loader": "0.5.7", 279 | "webpack": "2.1.0-beta.25", 280 | "webpack-dev-middleware": "1.8.4", 281 | "webpack-hot-middleware": "2.13.1" 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /FrontEnd/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | const setup = require('./middlewares/frontendMiddleware'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | const ngrok = (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel ? require('ngrok') : false; 10 | const resolve = require('path').resolve; 11 | const app = express(); 12 | 13 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 14 | // app.use('/api', myApi); 15 | var proxy = require('http-proxy-middleware'); 16 | // proxy middleware options 17 | var options = { 18 | target: 'http://localhost:9000', // target host 19 | changeOrigin: true, // needed for virtual hosted sites 20 | pathRewrite: { 21 | '^/api/' : '/' // rewrite path 22 | } 23 | }; 24 | 25 | // create the proxy (without context) 26 | var Proxy = proxy(options); 27 | app.use('/api', Proxy); 28 | 29 | // In production we need to pass these values in instead of relying on webpack 30 | setup(app, { 31 | outputPath: resolve(process.cwd(), 'build'), 32 | publicPath: '/', 33 | }); 34 | 35 | // get the intended port number, use port 3000 if not provided 36 | const port = argv.port || process.env.PORT || 3000; 37 | 38 | // Start your app. 39 | app.listen(port, (err) => { 40 | if (err) { 41 | return logger.error(err.message); 42 | } 43 | 44 | // Connect to ngrok in dev mode 45 | if (ngrok) { 46 | ngrok.connect(port, (innerErr, url) => { 47 | if (innerErr) { 48 | return logger.error(innerErr); 49 | } 50 | 51 | logger.appStarted(port, url); 52 | }); 53 | } else { 54 | logger.appStarted(port); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /FrontEnd/server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | 13 | // Called whenever there's an error on the server we want to print 14 | error: (err) => { 15 | console.error(chalk.red(err)); 16 | }, 17 | 18 | // Called when express.js app starts on given port w/o errors 19 | appStarted: (port, tunnelStarted) => { 20 | console.log(`Server started ${chalk.green('✓')}`); 21 | 22 | // If the tunnel started, log that and the URL it's available at 23 | if (tunnelStarted) { 24 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 25 | } 26 | 27 | console.log(` 28 | ${chalk.bold('Access URLs:')}${divider} 29 | Localhost: ${chalk.magenta(`http://localhost:${port}`)} 30 | LAN: ${chalk.magenta(`http://${ip.address()}:${port}`) + 31 | (tunnelStarted ? `\n Proxy: ${chalk.magenta(tunnelStarted)}` : '')}${divider} 32 | ${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)} 33 | `); 34 | }, 35 | }; 36 | 37 | module.exports = logger; 38 | -------------------------------------------------------------------------------- /FrontEnd/server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const compression = require('compression'); 5 | const pkg = require(path.resolve(process.cwd(), 'package.json')); 6 | 7 | // Dev middleware 8 | const addDevMiddlewares = (app, webpackConfig) => { 9 | const webpack = require('webpack'); 10 | const webpackDevMiddleware = require('webpack-dev-middleware'); 11 | const webpackHotMiddleware = require('webpack-hot-middleware'); 12 | const compiler = webpack(webpackConfig); 13 | const middleware = webpackDevMiddleware(compiler, { 14 | noInfo: true, 15 | publicPath: webpackConfig.output.publicPath, 16 | silent: true, 17 | stats: 'errors-only', 18 | }); 19 | 20 | app.use(middleware); 21 | app.use(webpackHotMiddleware(compiler)); 22 | 23 | // Since webpackDevMiddleware uses memory-fs internally to store build 24 | // artifacts, we use it instead 25 | const fs = middleware.fileSystem; 26 | 27 | if (pkg.dllPlugin) { 28 | app.get(/\.dll\.js$/, (req, res) => { 29 | const filename = req.path.replace(/^\//, ''); 30 | res.sendFile(path.join(process.cwd(), pkg.dllPlugin.path, filename)); 31 | }); 32 | } 33 | 34 | app.get('*', (req, res) => { 35 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 36 | if (err) { 37 | res.sendStatus(404); 38 | } else { 39 | res.send(file.toString()); 40 | } 41 | }); 42 | }); 43 | }; 44 | 45 | // Production middlewares 46 | const addProdMiddlewares = (app, options) => { 47 | const publicPath = options.publicPath || '/'; 48 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 49 | 50 | // compression middleware compresses your server responses which makes them 51 | // smaller (applies also to assets). You can read more about that technique 52 | // and other good practices on official Express.js docs http://mxs.is/googmy 53 | app.use(compression()); 54 | app.use(publicPath, express.static(outputPath)); 55 | 56 | app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html'))); 57 | }; 58 | 59 | /** 60 | * Front-end middleware 61 | */ 62 | module.exports = (app, options) => { 63 | const isProd = process.env.NODE_ENV === 'production'; 64 | 65 | if (isProd) { 66 | addProdMiddlewares(app, options); 67 | } else { 68 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 69 | addDevMiddlewares(app, webpackConfig); 70 | } 71 | 72 | return app; 73 | }; 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 |
    React.Js/Node.Js Project
    6 | 7 |
    8 | ## Quick start 9 | 10 | 1. FrontEnd part: `cd Frontend` 11 | Run `npm run setup` to install dependencies
    12 | Run `npm start` for development
    13 | Run `npm run build` for production
    14 | Run `npm run test` for testing
    15 | 1. BackEnd part: `cd Backend` 16 | Run `npm install` to install dependencies
    17 | Run `npm start` for development
    18 | Run `npm run prod` for production
    19 | Run `npm run test` for testing
    20 | 21 | ## Frontend 22 | 23 |
    24 |
    Authentication
    25 |
    Only User page needs authentication and `redux-auth-wrapper` high order component library is wrapped that page for authentication. Trying access to Users page without authentication, HOC will redirect to login page and login page validates token in sessionStorage if exist. If it not exist, client stays in login page. If re-login successful, re-direct again to desired page.
    26 |
    Home, Login and Contact pages validate jwt token in mount state.
    27 |
    Credentials are email:john@doe.com, password:secret
    28 | 29 |
    Styling
    30 |
    Styles are in global-styles.js file which provide css injection to `head` tag
    31 | 32 |
    Sagas
    33 |
    For side effects, `redux-saga` is implemented. All the actions are sync and sagas listens stores and actions and catch the desired requests then fires back the request results
    34 | 35 |
    Main saga is in App container.This saga runs all the time, but other sagas destroy themselves after page change.
    36 | 37 |
    Proxy
    38 |
    All sagas uses '/api/*' paths for server. In development frontend node.js server rewrites all request to '/api/* -> /*' and backend server only handles '/*'.
    39 |
    40 | 41 | ## Backend 42 | 43 |
    44 |
    ES6
    45 |
    Babel compiler implemented for es6 features
    46 |
    Any DB system not included, only endpoints
    47 |
    /contact endpoint only logs datas, no saving or validation
    48 |
    49 | 50 | > Please note that there are only 2 React components and 3 endpoints have unit tests. 51 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hurkanyakay/reactjs-nodejs-rest-example/916b86e921753ba32db804006ba576a045bab9db/banner.png --------------------------------------------------------------------------------