├── db ├── models │ └── .gitkeep ├── database.js └── migrations │ ├── 20170629090125-email-migration.js │ ├── 20170211161011-add-password-column.js │ └── 20170124052953-create_tables-migration.js ├── src ├── containers │ ├── Todo │ │ ├── Todo.scss │ │ └── Todo.js │ ├── Items │ │ ├── Items.scss │ │ └── Items.js │ ├── Home │ │ ├── logo.png │ │ ├── jumbo1.jpg │ │ ├── flux-logo.png │ │ ├── Home.scss │ │ └── Home.js │ ├── About │ │ ├── kitten.jpg │ │ └── About.js │ ├── Chat │ │ ├── Chat.scss │ │ └── Chat.js │ ├── index.js │ ├── Login │ │ ├── Login.scss │ │ ├── ForgotPasswordForm.js │ │ ├── ForgotPassword.js │ │ ├── Login.js │ │ └── LoginForm.js │ ├── App │ │ ├── App.scss │ │ └── App.js │ ├── LoginSuccess │ │ └── LoginSuccess.js │ └── NotFound │ │ └── NotFound.js ├── helpers │ ├── Html.scss │ ├── ApiClient.js │ └── Html.js ├── console.js ├── server │ ├── __tests__ │ │ ├── sample-test.js │ │ ├── models-todo-test.js │ │ └── models-user-test.js │ ├── items │ │ ├── command.js │ │ ├── api.js │ │ └── getter.js │ ├── core │ │ ├── task.js │ │ └── api.js │ ├── todo │ │ ├── model.js │ │ └── api.js │ ├── user │ │ ├── api.js │ │ └── model.js │ └── index.js ├── components │ ├── InfoBar │ │ ├── InfoBar.scss │ │ └── InfoBar.js │ ├── index.js │ ├── MiniInfoBar │ │ └── MiniInfoBar.js │ ├── CounterButton │ │ └── CounterButton.js │ └── __tests__ │ │ └── InfoBar-test.js ├── redux │ ├── modules │ │ ├── counter.js │ │ ├── info.js │ │ ├── items.js │ │ └── auth.js │ ├── reducers.js │ ├── middleware │ │ └── clientMiddleware.js │ ├── configureStore.js │ └── react-hapines │ │ └── index.js ├── styles │ └── styles.scss ├── config.js ├── routes.js └── client.js ├── .eslintignore ├── .vscode └── settings.json ├── bin ├── packager.js ├── console.js └── server.js ├── issue.md ├── static ├── favicon.ico └── favicon.png ├── tools ├── tests.webpack.js ├── babel-require.js ├── packager.js ├── isomorphic-tools.js ├── webpack.production.js └── webpack.development.js ├── tsconfig.json ├── .gitignore ├── .editorconfig ├── .flowconfig ├── .sequelizerc ├── docker-compose.yml ├── LICENSE ├── .eslintrc.json ├── settings.sample.js ├── karma.conf.js ├── README.md └── package.json /db/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/containers/Todo/Todo.scss: -------------------------------------------------------------------------------- 1 | .todo { 2 | } 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | karma.conf.js 2 | tests.webpack.js 3 | -------------------------------------------------------------------------------- /src/containers/Items/Items.scss: -------------------------------------------------------------------------------- 1 | .items { 2 | } 3 | -------------------------------------------------------------------------------- /src/helpers/Html.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/styles.scss'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vsicons.presets.angular": false 3 | } -------------------------------------------------------------------------------- /bin/packager.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../tools/packager') 4 | -------------------------------------------------------------------------------- /issue.md: -------------------------------------------------------------------------------- 1 | # yarn issue 2 | 3 | https://github.com/karma-runner/karma-phantomjs-launcher/issues/120 4 | -------------------------------------------------------------------------------- /bin/console.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../tools/babel-require') 4 | require('../src/console') 5 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/HEAD/static/favicon.png -------------------------------------------------------------------------------- /tools/tests.webpack.js: -------------------------------------------------------------------------------- 1 | var context = require.context('../src', true, /-test\.js$/); 2 | context.keys().forEach(context); 3 | -------------------------------------------------------------------------------- /src/containers/Home/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/HEAD/src/containers/Home/logo.png -------------------------------------------------------------------------------- /src/containers/About/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/HEAD/src/containers/About/kitten.jpg -------------------------------------------------------------------------------- /src/containers/Home/jumbo1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/HEAD/src/containers/Home/jumbo1.jpg -------------------------------------------------------------------------------- /src/containers/Home/flux-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/HEAD/src/containers/Home/flux-logo.png -------------------------------------------------------------------------------- /src/console.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { command, modules } from './server/core' 4 | 5 | modules.install() 6 | command.execute(process.argv[2], process.argv[3]) 7 | -------------------------------------------------------------------------------- /src/server/__tests__/sample-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | describe('test', () => { 4 | it('sample test', () => { 5 | expect(1).to.equal(1) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // remove experimental decorator vscode warning when coding js file 2 | { 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "allowJs": true 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/InfoBar/InfoBar.scss: -------------------------------------------------------------------------------- 1 | .infoBar { 2 | font-variant: italics; 3 | margin-bottom: 20px; 4 | text-align: center; 5 | } 6 | 7 | .time { 8 | } 9 | 10 | .button { 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | *.iml 5 | webpack-assets.json 6 | webpack-stats.json 7 | npm-debug.log 8 | *.database 9 | *.swp 10 | *.sql 11 | pgdata 12 | settings.js 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /src/containers/Chat/Chat.scss: -------------------------------------------------------------------------------- 1 | .chat { 2 | input { 3 | padding: 5px 10px; 4 | border-radius: 5px; 5 | border: 1px solid #ccc; 6 | } 7 | form { 8 | margin: 30px 0; 9 | :global(.btn) { 10 | margin-left: 10px; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/.* 3 | .*/node_modules/fbjs/.* 4 | .*/node_modules/react-side-effect/.* 5 | 6 | [include] 7 | 8 | [libs] 9 | ./webpack/declaration.js 10 | 11 | [options] 12 | module.ignore_non_literal_requires=true 13 | suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe 14 | -------------------------------------------------------------------------------- /src/server/items/command.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // import logger from '../logger' 4 | // import { command } from '../core' 5 | // import getter from './getter' 6 | // 7 | // command.route('items', 'get', async () => { 8 | // const data = await getter() 9 | // logger.info(`gathered items: ${data.length}`) 10 | // }) 11 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | require('./tools/babel-require'); 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | config: path.resolve('db', 'database.js'), 7 | 'migrations-path': path.resolve('db', 'migrations'), 8 | 'models-path': path.resolve('db', 'models'), 9 | 'seeders-path': path.resolve('db', 'seeders'), 10 | } 11 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Point of contact for component modules 3 | * 4 | * ie: import { CounterButton, InfoBar } from 'components' 5 | * 6 | */ 7 | 8 | export CounterButton from './CounterButton/CounterButton' 9 | export InfoBar from './InfoBar/InfoBar' 10 | export MiniInfoBar from './MiniInfoBar/MiniInfoBar' 11 | -------------------------------------------------------------------------------- /db/database.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const file = fs.readFileSync(path.resolve('./package.json')) 5 | const config = JSON.parse(file).babel 6 | require('babel-register')(config) 7 | const settings = require('../settings') 8 | 9 | module.exports = settings[process.env.NODE_ENV || 'development'].database 10 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export App from './App/App' 4 | export Chat from './Chat/Chat' 5 | export Home from './Home/Home' 6 | export Items from './Items/Items' 7 | export About from './About/About' 8 | export Login from './Login/Login' 9 | export LoginSuccess from './LoginSuccess/LoginSuccess' 10 | export Todo from './Todo/Todo' 11 | export NotFound from './NotFound/NotFound' 12 | -------------------------------------------------------------------------------- /src/server/core/task.js: -------------------------------------------------------------------------------- 1 | import { server } from 'hails' 2 | 3 | const { scheduler } = server 4 | 5 | scheduler.register('/core/testCamelCase', (job, done) => { 6 | server.broadcast({ 7 | type: 'schedule.requested-alert', 8 | now: new Date(), 9 | description: 'see src/server/core/task.js', 10 | }) 11 | done() 12 | }) 13 | 14 | setInterval(() => { 15 | scheduler.now('/core/testCamelCase') 16 | }, 3000) 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | db: 2 | container_name: db 3 | image: postgres:9.4.10 4 | ports: 5 | - 15432:5432 6 | volumes: 7 | - ./pgdata:/var/lib/postgresql/data 8 | 9 | redis: 10 | container_name: redis 11 | image: redis:3.0 12 | command: redis-server /usr/local/etc/redis/redis.conf 13 | ports: 14 | - 16379:6379 15 | volumes: 16 | - ./docker/redis.conf:/usr/local/etc/redis/redis.conf 17 | -------------------------------------------------------------------------------- /src/redux/modules/counter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const INCREMENT = 'counter/INCREMENT' 4 | 5 | const initialState = { 6 | count: 0 7 | } 8 | 9 | export default function reducer(state = initialState, action = {}) { 10 | switch (action.type) { 11 | case INCREMENT: 12 | const {count} = state 13 | return { 14 | count: count + 1 15 | } 16 | default: 17 | return state 18 | } 19 | } 20 | 21 | export function increment() { 22 | return { 23 | type: INCREMENT 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /db/migrations/20170629090125-email-migration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | const { STRING } = Sequelize 4 | return queryInterface.addColumn('users', 'email', { 5 | type: STRING, 6 | }).then(() => ( 7 | queryInterface.sequelize.query( // password 1234 8 | `UPDATE users 9 | SET email = 'tester@hrfb.com' 10 | WHERE id = 1;`, 11 | ) 12 | )) 13 | }, 14 | down: queryInterface => ( 15 | queryInterface.removeColumn('users', 'email') 16 | ), 17 | } 18 | -------------------------------------------------------------------------------- /src/components/MiniInfoBar/MiniInfoBar.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | const MainInfoBar = (props) => { 5 | const { time } = props 6 | return ( 7 |

8 | The info bar was last loaded at 9 | {' '} 10 | {time && new Date(time).toString()} 11 |

12 | ) 13 | } 14 | 15 | MainInfoBar.propTypes = { 16 | time: PropTypes.number, 17 | } 18 | 19 | export default connect(store => ({ time: store.info.data.time }))(MainInfoBar) 20 | -------------------------------------------------------------------------------- /src/containers/Login/Login.scss: -------------------------------------------------------------------------------- 1 | .loginPage { 2 | margin-top: 36px; 3 | width: 400px; 4 | .description { 5 | padding: 5px; 6 | font-size: 90%; 7 | color: #f53c3c; 8 | } 9 | } 10 | 11 | .error { 12 | border: 1px solid #f53c3c !important; 13 | } 14 | .facebookButton{ 15 | width: 100%; 16 | background: #3b5998; 17 | border: 0; 18 | border-radius: 4px; 19 | color: white; 20 | padding: 10px; 21 | &:hover { 22 | background: #2d4373; 23 | } 24 | } 25 | .submitButton { 26 | border-radius: 20px; 27 | font-weight: bold; 28 | } -------------------------------------------------------------------------------- /db/migrations/20170211161011-add-password-column.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | const { STRING } = Sequelize 4 | return queryInterface.addColumn('users', 'password', { 5 | type: STRING, 6 | }).then(() => ( 7 | queryInterface.sequelize.query( // password 1234 8 | `UPDATE users 9 | SET password = '$2a$10$Fnh/BI5zerG4EGESnBN0B.x7yU6ny4F2g1gFUjoTTlD0fhuip2Fm2' 10 | WHERE id = 1;`, 11 | ) 12 | )) 13 | }, 14 | down: queryInterface => ( 15 | queryInterface.removeColumn('users', 'password') 16 | ), 17 | } 18 | -------------------------------------------------------------------------------- /src/redux/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer } from 'react-router-redux' 3 | import { reducer as reduxAsyncConnect } from 'redux-connect' 4 | import { reducer as form } from 'redux-form' 5 | import { reducer as hapines } from './react-hapines' 6 | 7 | import auth from './modules/auth' 8 | import counter from './modules/counter' 9 | import info from './modules/info' 10 | import items from './modules/items' 11 | 12 | export default combineReducers({ 13 | routing: routerReducer, 14 | reduxAsyncConnect, 15 | hapines, 16 | auth, 17 | form, 18 | counter, 19 | info, 20 | items, 21 | }) 22 | -------------------------------------------------------------------------------- /src/server/items/api.js: -------------------------------------------------------------------------------- 1 | import Boom from 'boom' 2 | import { server } from 'hails' 3 | import getItems from './getter' 4 | 5 | const nestedRoute = server.route.nested('/api') 6 | 7 | nestedRoute.get('/items', { 8 | tags: ['api'], 9 | }, async (request, reply) => { 10 | const items = await getItems() 11 | if (items) { 12 | reply(items) 13 | } else { 14 | reply(Boom.serverUnavailable('Widget load fails 33% of the time. You were unlucky.', {})) 15 | } 16 | }) 17 | 18 | nestedRoute({ 19 | config: { 20 | tags: ['api'], 21 | }, 22 | path: '/items2', 23 | method: 'get', 24 | handler: (request, reply) => { 25 | reply({}) 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/server/core/api.js: -------------------------------------------------------------------------------- 1 | import Boom from 'boom' 2 | import { server } from 'hails' 3 | 4 | const nestedRoute = server.route.nested('/api') 5 | 6 | nestedRoute.get('/load-info', { 7 | tags: ['api'], 8 | }, async (request, reply) => { 9 | reply({ 10 | message: 'This came from the api server', 11 | time: Date.now(), 12 | }) 13 | }) 14 | 15 | server.route.get('/favicon.ico', {}, { 16 | file: `${__dirname}/../static/favicon.ico`, 17 | }) 18 | 19 | server.route.get('/static/{p*}', {}, { 20 | directory: { 21 | path: '../static', 22 | }, 23 | }) 24 | 25 | server.route.any('/api/{p*}', {}, async (request, reply) => { 26 | reply(Boom.notFound('NOT FOUND')) 27 | }) 28 | -------------------------------------------------------------------------------- /src/components/CounterButton/CounterButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import { increment } from '../../redux/modules/counter' 5 | 6 | @connect( 7 | store => ({ count: store.counter.count }), 8 | { increment }, 9 | ) 10 | export default class CounterButton extends Component { 11 | 12 | render() { 13 | const { count, increment } = this.props // eslint-disable-line no-shadow 14 | let { className } = this.props 15 | className += ' btn btn-sm btn-default' 16 | return ( 17 | 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint import/no-extraneous-dependencies: "off" */ 3 | 4 | require('../tools/babel-require') 5 | const path = require('path') 6 | 7 | const rootDir = path.resolve(__dirname, '..') 8 | 9 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 10 | 11 | global.CLIENT = false 12 | global.SERVER = true 13 | global.DISABLE_SSR = false 14 | global.DEVELOPMENT = process.env.NODE_ENV !== 'production' 15 | 16 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 17 | const WebpackIsomorphicTools = require('webpack-isomorphic-tools') 18 | global.webpackIsomorphicTools = new WebpackIsomorphicTools(require('../tools/isomorphic-tools')) 19 | .server(rootDir, () => { 20 | require('../src/server') 21 | }) 22 | -------------------------------------------------------------------------------- /tools/babel-require.js: -------------------------------------------------------------------------------- 1 | try { 2 | const config = require('../package.json').babel 3 | 4 | // issue with npm linking 5 | // https://github.com/webpack/webpack/issues/1866 6 | // https://github.com/babel/babel-loader/issues/166#issuecomment-196888445 7 | // match with babel config in package.json 8 | config.presets = config.presets.map(item => { 9 | if(Array.isArray(item)) { 10 | return require.resolve(`babel-preset-${item[0]}`) 11 | } else { 12 | return require.resolve(`babel-preset-${item}`) 13 | } 14 | }) 15 | config.plugins = config.plugins.map(item => require.resolve(`babel-plugin-${item}`)) 16 | // end issue with npm linking 17 | 18 | require('babel-register')(config) 19 | require('babel-polyfill') 20 | } catch (err) { 21 | console.error('==> ERROR: Error parsing your config in package.json.') 22 | console.error(err) 23 | } 24 | -------------------------------------------------------------------------------- /src/containers/Todo/Todo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import Helmet from 'react-helmet' 4 | 5 | @connect(() => ({}), {}) 6 | export default class Form extends Component { 7 | static propTypes = { 8 | } 9 | 10 | render() { 11 | return ( 12 |
13 | 14 | 15 |
16 |
17 |

18 | Todo 19 |
20 | (crud example) 21 |

22 |
23 |
24 | 25 |
26 | TODO: todo list 27 |
28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/server/__tests__/models-todo-test.js: -------------------------------------------------------------------------------- 1 | /* eslint prefer-arrow-callback: "off" */ 2 | 3 | import { expect } from 'chai' 4 | import { before, beforeEach, describe, it } from 'mocha' 5 | import { models, sequelize } from '../core' 6 | 7 | const { User, Todo } = models 8 | 9 | before(async function () { 10 | await sequelize.sync({ force: true }) 11 | }) 12 | 13 | describe('TodoTest', () => { 14 | let user 15 | 16 | beforeEach(async function () { 17 | user = await User.create({ 18 | username: 'todo-user', 19 | password: 'todopassword', 20 | passwordConfirmation: 'todopassword', 21 | }) 22 | }) 23 | 24 | const saveTodo = () => Todo.create({ 25 | title: 'todo title1', 26 | userId: user.id, 27 | }) 28 | 29 | it('save a todo', async function () { 30 | await saveTodo() 31 | const todo = await Todo.find() 32 | expect(todo.title).equal('todo title1') 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/server/todo/model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { server } from 'hails' 3 | 4 | const { sequelize, DataTypes } = server 5 | 6 | const { INTEGER, STRING, DATE } = DataTypes 7 | 8 | const Todo = sequelize.define('todos', { 9 | id: { type: INTEGER, primaryKey: true, autoIncrement: true, scopes: ['public'] }, 10 | title: STRING, 11 | userId: { 12 | field: 'user_id', 13 | type: INTEGER, 14 | reference: { model: 'users', key: 'id' }, 15 | }, 16 | createdAt: { type: DATE, field: 'created_at' }, 17 | updatedAt: { type: DATE, field: 'updated_at' }, 18 | }, 19 | { 20 | classMethods: { 21 | associate: (models) => { 22 | Todo.belongsTo(models.User, { 23 | onDelete: 'CASCADE', 24 | foreignKey: { 25 | allowNull: false, 26 | }, 27 | }) 28 | }, 29 | }, 30 | createdAt: 'createdAt', 31 | updatedAt: 'updatedAt', 32 | }) 33 | 34 | export { 35 | Todo, 36 | } 37 | -------------------------------------------------------------------------------- /src/redux/middleware/clientMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default function clientMiddleware(client) { 4 | return ({dispatch, getState}) => { 5 | return next => action => { 6 | if (typeof action === 'function') { 7 | return action(dispatch, getState) 8 | } 9 | 10 | const { promise, types, ...rest } = action; // eslint-disable-line no-redeclare 11 | if (!promise) { 12 | return next(action) 13 | } 14 | 15 | const [REQUEST, SUCCESS, FAILURE] = types 16 | next({...rest, type: REQUEST}) 17 | 18 | const actionPromise = promise(client) 19 | actionPromise.then( 20 | (result) => next({...rest, result, type: SUCCESS}), 21 | (error) => next({...rest, error, type: FAILURE}) 22 | ).catch((error)=> { 23 | console.error('MIDDLEWARE ERROR:', error) 24 | next({...rest, error, type: FAILURE}) 25 | }) 26 | 27 | return actionPromise 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/redux/modules/info.js: -------------------------------------------------------------------------------- 1 | const LOAD = 'info/LOAD' 2 | const LOAD_SUCCESS = 'info/LOAD_SUCCESS' 3 | const LOAD_FAIL = 'info/LOAD_FAIL' 4 | 5 | const initialState = { 6 | loaded: false, 7 | } 8 | 9 | export default function info(state = initialState, action = {}) { 10 | switch (action.type) { 11 | case LOAD: 12 | return { 13 | ...state, 14 | loading: true, 15 | } 16 | case LOAD_SUCCESS: 17 | return { 18 | ...state, 19 | loading: false, 20 | loaded: true, 21 | data: action.result, 22 | } 23 | case LOAD_FAIL: 24 | return { 25 | ...state, 26 | loading: false, 27 | loaded: false, 28 | error: action.error, 29 | } 30 | default: 31 | return state 32 | } 33 | } 34 | 35 | export function isLoaded(globalState) { 36 | return globalState.info && globalState.info.loaded 37 | } 38 | 39 | export function load() { 40 | return { 41 | types: [LOAD, LOAD_SUCCESS, LOAD_FAIL], 42 | promise: client => client.get('/api/load-info'), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/containers/Home/Home.scss: -------------------------------------------------------------------------------- 1 | .home { 2 | dd { 3 | margin-bottom: 15px; 4 | } 5 | } 6 | .masthead { 7 | background: #2d2d2d; 8 | padding: 40px 20px; 9 | color: white; 10 | text-align: center; 11 | .logo { 12 | $size: 200px; 13 | margin: auto; 14 | height: $size; 15 | width: $size; 16 | border-radius: $size / 2; 17 | border: 1px solid cyan; 18 | box-shadow: inset 0 0 10px cyan; 19 | vertical-align: middle; 20 | p { 21 | line-height: $size; 22 | margin: 0px; 23 | } 24 | img { 25 | width: 75%; 26 | margin: auto; 27 | } 28 | } 29 | h1 { 30 | color: cyan; 31 | font-size: 4em; 32 | } 33 | h2 { 34 | color: #ddd; 35 | font-size: 2em; 36 | margin: 20px; 37 | } 38 | a { 39 | color: #ddd; 40 | } 41 | p { 42 | margin: 10px; 43 | } 44 | .humility { 45 | color: humility; 46 | a { 47 | color: humility; 48 | } 49 | } 50 | .github { 51 | font-size: 1.5em; 52 | } 53 | } 54 | 55 | .counterContainer { 56 | text-align: center; 57 | margin: 20px; 58 | } 59 | -------------------------------------------------------------------------------- /src/server/__tests__/models-user-test.js: -------------------------------------------------------------------------------- 1 | /* eslint prefer-arrow-callback: "off" */ 2 | 3 | import { expect } from 'chai' 4 | import { describe, it, before, beforeEach } from 'mocha' 5 | import { models, sequelize } from '../core' 6 | 7 | const { User } = models 8 | 9 | before(async function () { 10 | await sequelize.sync({ force: true }) 11 | }) 12 | 13 | describe('UserTest', () => { 14 | let u 15 | 16 | beforeEach(() => { 17 | u = User.build({ 18 | username: 'example-user', 19 | password: 'foobar', 20 | passwordConfirmation: 'foobar', 21 | }) 22 | }) 23 | 24 | it('save an user', async function () { 25 | const user = await u.save() 26 | expect(user.username).equal('example-user') 27 | }) 28 | 29 | it('get the saved user', async function () { 30 | const user = await User.find({ 31 | where: { 32 | username: 'example-user', 33 | }, 34 | }) 35 | expect(user.username).equal('example-user') 36 | expect(user.authenticate('foobar2')).equal(false) 37 | expect(user.authenticate('foobar')).equal(true) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/dist/css/bootstrap.min.css"; 2 | 3 | body, h1, h2, h3, h4, h5, h6, input, textarea, button { 4 | font-family: 'source sans pro', sans-serif; 5 | font-weight: 400; 6 | color: #484848; 7 | } 8 | 9 | .navbar-brand { 10 | font-family: 'Poppins', sans-serif !important; 11 | } 12 | 13 | body { 14 | background-color: #FAFAFA; 15 | } 16 | 17 | h2.title { 18 | font-weight: 300; 19 | &> span { 20 | font-size: 50%; 21 | font-weight: 400; 22 | opacity: 0.6; 23 | } 24 | } 25 | 26 | .headline { 27 | h2, h3 { 28 | font-weight: 300; 29 | } 30 | } 31 | 32 | input::placeholder { 33 | font-style: italic; 34 | font-size: 80%; 35 | color: #cdcdcd; 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Erik Rasmussen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: "off" */ 2 | 3 | import { createStore as _createStore, applyMiddleware, compose } from 'redux' 4 | import { routerMiddleware } from 'react-router-redux' 5 | import thunk from 'redux-thunk' 6 | import createMiddleware from './middleware/clientMiddleware' 7 | 8 | export const configureStore = (history, client, data) => { 9 | // Sync dispatched route actions to the history 10 | const reduxRouterMiddleware = routerMiddleware(history) 11 | 12 | const middleware = [createMiddleware(client), reduxRouterMiddleware, thunk] 13 | 14 | let finalCreateStore 15 | if (DEVELOPMENT && CLIENT) { 16 | finalCreateStore = compose( 17 | applyMiddleware(...middleware), 18 | )(_createStore) 19 | } else { 20 | finalCreateStore = applyMiddleware(...middleware)(_createStore) 21 | } 22 | 23 | const reducers = require('./reducers').default 24 | const store = finalCreateStore(reducers, data) 25 | 26 | if (DEVELOPMENT && module.hot) { 27 | module.hot.accept('./reducers', () => { 28 | store.replaceReducer(require('./reducers').default) 29 | }) 30 | } 31 | 32 | return store 33 | } 34 | -------------------------------------------------------------------------------- /src/components/InfoBar/InfoBar.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { bindActionCreators } from 'redux' 3 | import { connect } from 'react-redux' 4 | 5 | import { load } from '../../redux/modules/info' 6 | 7 | const styles = require('./InfoBar.scss') 8 | 9 | const InfoBar = (props) => { 10 | const { info, load } = props // eslint-disable-line no-shadow 11 | return ( 12 |
13 |
14 | This is an info bar 15 | {' '} 16 | {info ? info.message : 'no info!'} 17 | {' '} 18 | {info && `at ${new Date(info.time)}`} 19 |
20 |
21 | 27 |
28 | ) 29 | } 30 | 31 | InfoBar.propTypes = { 32 | info: PropTypes.object, 33 | load: PropTypes.func.isRequired, 34 | } 35 | 36 | export default connect( 37 | store => ({ info: store.info.data }), 38 | dispatch => bindActionCreators({ load }, dispatch))(InfoBar) 39 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | app: { 3 | title: 'hapi-react-fullstack-boilerplate', 4 | subtitle: 'Hapi, Sequelize, React, Redux, Bootstrap, etc.', 5 | description: 'React Full web stack with rendering server and hapi echosystem', 6 | head: { 7 | titleTemplate: 'hapi-react-fullstack-boilerplate: %s', 8 | meta: [ 9 | { name: 'description', content: 'All the modern best practices in one example.' }, 10 | { charset: 'utf-8' }, 11 | { property: 'og:site_name', content: 'hapi-react-fullstack-boilerplate' }, 12 | { property: 'og:image', content: '' }, 13 | { property: 'og:locale', content: 'en_US' }, 14 | { property: 'og:title', content: 'hapi-react-fullstack-boilerplate' }, 15 | { property: 'og:description', content: 'All the modern best practices in one example.' }, 16 | { property: 'og:card', content: 'summary' }, 17 | { property: 'og:site', content: '@eseom' }, 18 | { property: 'og:creator', content: '@eseom' }, 19 | { property: 'og:image:width', content: '200' }, 20 | { property: 'og:image:height', content: '200' }, 21 | ], 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react", 5 | "jsx-a11y", 6 | "import" 7 | ], 8 | "settings": { 9 | "import/resolver": { 10 | "webpack": { 11 | "config": "./tools/webpack.development.js" 12 | } 13 | } 14 | }, 15 | "parser": "babel-eslint", 16 | "rules": { 17 | "semi": [1, "never"], 18 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 19 | "global-require": 0, 20 | "import/prefer-default-export": 0, 21 | "react/prefer-stateless-function": 0, 22 | "import/no-extraneous-dependencies": 0, 23 | "no-unused-vars": 1, 24 | "no-script-url": 0, 25 | "func-names": 0, 26 | "react/sort-comp": 0, 27 | "react/require-default-props": 0, 28 | "react/forbid-prop-types": 0, 29 | "react/prop-types": 0, 30 | "react/no-array-index-key": 0 31 | }, 32 | "env": { 33 | "browser": true, 34 | "node": true 35 | }, 36 | "globals": { 37 | "DEVELOPMENT": true, 38 | "CLIENT": true, 39 | "SERVER": true, 40 | "DISABLE_SSR": true, 41 | "webpackIsomorphicTools": true, 42 | "socket": true, 43 | "settings": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/server/items/getter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import cheerio from 'cheerio' 4 | import fetch from 'node-fetch' 5 | 6 | export default async () => { 7 | const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1' 8 | const url = 'https://distrowatch.com/packages.php' 9 | 10 | const body = await fetch(url, { 11 | headers: { 12 | 'User-Agent': userAgent, 13 | }, 14 | }).then(b => b.text()) 15 | 16 | const $ = cheerio.load(body) 17 | const ret = [] 18 | $('.Auto tr').each((i1, tr) => { 19 | if (i1 === 0) return 20 | const nameNode = $('th', tr) 21 | const name = { 22 | link: $('a', nameNode).attr('href'), 23 | text: nameNode.text(), 24 | } 25 | const versionNode = $('td', tr) 26 | let version 27 | versionNode.each((i2, td) => { 28 | if (i2 === 0) { 29 | version = { 30 | link: $('a', td).attr('href'), 31 | text: $('a', td).text(), 32 | } 33 | } 34 | }) 35 | ret.push({ 36 | id: i1, 37 | name, 38 | version, 39 | note: $('.Note1', tr).text(), 40 | }) 41 | }) 42 | return ret 43 | } 44 | -------------------------------------------------------------------------------- /src/redux/modules/items.js: -------------------------------------------------------------------------------- 1 | const LOAD = 'items/LOAD' 2 | const LOAD_SUCCESS = 'items/LOAD_SUCCESS' 3 | const LOAD_FAIL = 'items/LOAD_FAIL' 4 | 5 | const initialState = { 6 | loaded: false, 7 | editing: {}, 8 | saveError: {}, 9 | } 10 | 11 | export default function reducer(state = initialState, action = {}) { 12 | switch (action.type) { 13 | case LOAD: 14 | return { 15 | ...state, 16 | loading: true, 17 | } 18 | case LOAD_SUCCESS: 19 | return { 20 | ...state, 21 | loading: false, 22 | loaded: true, 23 | data: action.result, 24 | error: null, 25 | } 26 | case LOAD_FAIL: 27 | return { 28 | ...state, 29 | loading: false, 30 | loaded: false, 31 | data: null, 32 | error: action.error, 33 | } 34 | default: 35 | return state 36 | } 37 | } 38 | 39 | export function isLoaded(globalState) { 40 | return globalState.items && globalState.items.loaded 41 | } 42 | 43 | export function load() { 44 | return { 45 | types: [LOAD, LOAD_SUCCESS, LOAD_FAIL], 46 | promise: client => client.get('/api/items'), // params not used, just shown as demonstration 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /db/migrations/20170124052953-create_tables-migration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | const { STRING, INTEGER, DATE } = Sequelize 4 | return queryInterface.createTable('users', { 5 | id: { type: INTEGER, primaryKey: true, autoIncrement: true, scopes: ['public'] }, 6 | username: STRING, 7 | created_at: DATE, 8 | updated_at: DATE, 9 | }).then(() => { 10 | queryInterface.createTable('todos', { 11 | id: { type: INTEGER, primaryKey: true, autoIncrement: true, scopes: ['public'] }, 12 | title: STRING, 13 | user_id: { 14 | type: INTEGER, 15 | references: { 16 | model: 'users', 17 | key: 'id', 18 | }, 19 | onUpdate: 'cascade', 20 | onDelete: 'cascade', 21 | }, 22 | created_at: DATE, 23 | updated_at: DATE, 24 | }) 25 | }).then(() => ( 26 | queryInterface.sequelize.query( 27 | `INSERT INTO users 28 | (id, username, created_at, updated_at) 29 | VALUES 30 | (1, 'tester', current_timestamp, current_timestamp);`) 31 | )) 32 | }, 33 | down: queryInterface => ( 34 | queryInterface.dropTable('users').then(() => ( 35 | queryInterface.dropTable('todos') 36 | )) 37 | ), 38 | } 39 | -------------------------------------------------------------------------------- /src/helpers/ApiClient.js: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent' 2 | 3 | const methods = ['get', 'post', 'put', 'patch', 'del'] 4 | 5 | // redundant to server/server.js 6 | let port 7 | if (process.env.PORT) { 8 | port = process.env.PORT 9 | } else if (DEVELOPMENT) { 10 | port = 3000 11 | } else { 12 | port = 8080 13 | } 14 | 15 | function formatUrl(path) { 16 | const adjustedPath = path[0] !== '/' ? `/${path}` : path 17 | if (SERVER) { 18 | return `http://${process.env.HOST || 'localhost'}:${port}${adjustedPath}` 19 | } 20 | // Prepend `/api` to relative URL, to proxy to API server. 21 | return adjustedPath 22 | } 23 | 24 | export default class ApiClient { 25 | constructor(req) { 26 | methods.forEach(method => ( 27 | this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => { 28 | const request = superagent[method](formatUrl(path)) 29 | 30 | if (params) { 31 | request.query(params) 32 | } 33 | 34 | if (SERVER && req.headers.cookie) { 35 | request.set('cookie', req.headers.cookie) 36 | } 37 | 38 | if (data) { 39 | request.send(data) 40 | } 41 | 42 | request.end((err, { body } = {}) => (err ? reject(body || err) : resolve(body))) 43 | }) 44 | )) 45 | } 46 | static empty() {} 47 | } 48 | -------------------------------------------------------------------------------- /src/containers/App/App.scss: -------------------------------------------------------------------------------- 1 | headerLink { 2 | a { 3 | position: relative; 4 | } 5 | a:before { 6 | content: ""; 7 | position: absolute; 8 | height: 2px; 9 | bottom: 0; 10 | left: 8px; 11 | right: 9px; 12 | background-color: #000; 13 | visibility: hidden; 14 | -webkit-transform: scaleX(0); 15 | transform: scaleX(0); 16 | -webkit-transition: all 0.3s ease-in-out 0s; 17 | transition: all 0.3s ease-in-out 0s; 18 | } 19 | a:hover:before { 20 | visibility: visible; 21 | -webkit-transform: scaleX(1); 22 | transform: scaleX(1); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /settings.sample.js: -------------------------------------------------------------------------------- 1 | const redisDSN = 'redis://:dev@localhost:16379/10' 2 | 3 | module.exports = { 4 | development: { 5 | version: '0.0.1', 6 | connection: { 7 | port: 3000, 8 | }, 9 | modules: [ 10 | 'core', 11 | 'user', 12 | 'todo', 13 | 'items', 14 | ], 15 | viewEngine: { 16 | type: 'nunjucks', 17 | }, 18 | scheduler: { 19 | enable: true, 20 | broker: { 21 | redis: redisDSN, 22 | }, 23 | schedules: [ 24 | ['*/10 * * * * *', 'user.test'], 25 | ], 26 | }, 27 | redis: redisDSN, 28 | useSequelize: true, 29 | database: { 30 | storage: 'test.database', 31 | dialect: 'sqlite', 32 | }, 33 | database_pgsql: { 34 | url: process.env.DATABASE_URL, 35 | options: { 36 | logging: false, 37 | dialect: 'postgres', 38 | protocol: 'postgres', 39 | dialectOptions: { 40 | ssl: false, 41 | }, 42 | }, 43 | use_env_variable: 'DATABASE_URL', 44 | migrationStorageTableName: 'sequelize_meta', 45 | }, 46 | database_test: { 47 | storage: ':memory:', 48 | dialect: 'sqlite', 49 | }, 50 | exportToClient: { // export to browser 51 | gacode: 'UA-000000000-1', 52 | mockObject: { 53 | test: 1, 54 | }, 55 | }, 56 | }, 57 | } 58 | -------------------------------------------------------------------------------- /src/server/user/api.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import Boom from 'boom' 3 | import { server, models } from 'hails' 4 | 5 | const { User } = models 6 | const nestedRoute = server.route.nested('/api') 7 | 8 | nestedRoute.get('/load-auth', { 9 | tags: ['api'], 10 | }, async (request, reply) => { 11 | reply(request.yar.get('user') || null) 12 | }) 13 | 14 | nestedRoute.post('/login', { 15 | tags: ['api'], 16 | validate: { 17 | payload: { 18 | email: Joi.string().required(), 19 | password: Joi.string().required(), 20 | }, 21 | }, 22 | }, async (request, reply) => { 23 | const email = request.payload.email 24 | const user = await User.find({ 25 | where: { 26 | email, 27 | }, 28 | }) 29 | if (!user) { 30 | setTimeout(() => { // delay 2 second for testing 31 | reply(Boom.unauthorized(`no such user: ${email}`)) 32 | }, 2000) 33 | return 34 | } 35 | const authenticated = user.authenticate(request.payload.password) 36 | if (authenticated) { 37 | setTimeout(() => { // delay 1 second for testing 38 | request.yar.set('user', user) 39 | reply(user) 40 | }, 1000) 41 | } else { 42 | setTimeout(() => { // delay 2 second for testing 43 | reply(Boom.unauthorized('password mismatch')) 44 | }, 2000) 45 | } 46 | }) 47 | 48 | nestedRoute.get('/logout', { 49 | tags: ['api'], 50 | }, async (request, reply) => { 51 | request.yar.clear('user') 52 | reply({ result: true }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/containers/LoginSuccess/LoginSuccess.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import * as authActions from '../../redux/modules/auth' 5 | 6 | @connect( 7 | state => ({ user: state.auth.user && state.auth.user.username ? state.auth.user : null }), 8 | authActions) 9 | export default class LoginSuccess extends Component { 10 | static propTypes = { 11 | user: PropTypes.object, 12 | logout: PropTypes.func, 13 | } 14 | 15 | render() { 16 | const { user, logout } = this.props 17 | if (!user) { 18 | return null 19 | } 20 | return ( 21 |
22 |

Login Success

23 |
24 |
25 | Hi, {user.username}. You have just successfully logged in, 26 | and were forwarded here 27 | by componentWillReceiveProps() in App.js, 28 | which is listening to 29 | the auth reducer via redux @connect. How exciting! 30 |
31 |

32 | The same function will forward you to / should 33 | you chose to log out. The choice is yours... 34 |

35 |
36 | 37 |
38 |
39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { IndexRoute, Route } from 'react-router' 3 | import { isLoaded as isAuthLoaded, load as loadAuth } from './redux/modules/auth' 4 | import { 5 | App, 6 | Chat, 7 | Home, 8 | Items, 9 | About, 10 | Login, 11 | LoginSuccess, 12 | Todo, 13 | NotFound, 14 | } from './containers' 15 | 16 | export default (store) => { 17 | const requireLogin = (nextState, replace, cb) => { 18 | function checkAuth() { 19 | const { auth: { user } } = store.getState() 20 | if (!user) { 21 | // oops, not logged in, so can't be here! 22 | replace('/') 23 | } 24 | cb() 25 | } 26 | 27 | if (!isAuthLoaded(store.getState())) { 28 | store.dispatch(loadAuth()).then(checkAuth) 29 | } else { 30 | checkAuth() 31 | } 32 | } 33 | 34 | /** 35 | * Please keep routes in alphabetical order 36 | */ 37 | return ( 38 |
39 | 40 | { /* Home (main) route */ } 41 | 42 | 43 | { /* Routes requiring login */ } 44 | 45 | 46 | 47 | 48 | 49 | { /* Routes */ } 50 | 51 | 52 | 53 | 54 | 55 | 56 | { /* Catch all route */ } 57 | 58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /tools/packager.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: "off" */ 2 | /* eslint-disable no-console */ 3 | 4 | const webpack = require('webpack') 5 | const Hapi = require('hapi') 6 | const webpackDevMiddleware = require('webpack-dev-middleware') 7 | const webpackHotMiddleware = require('webpack-hot-middleware') 8 | const webpackConfig = require('./webpack.development') 9 | 10 | const compiler = webpack(webpackConfig) 11 | const host = 'localhost' 12 | const port = 3001 13 | const serverOptions = { 14 | contentBase: `http://${host}:${port}`, 15 | quiet: true, 16 | noInfo: false, 17 | hot: true, 18 | inline: true, 19 | lazy: false, 20 | publicPath: webpackConfig.output.publicPath, 21 | headers: { 'Access-Control-Allow-Origin': '*' }, 22 | stats: { colors: true }, 23 | } 24 | 25 | const server = new Hapi.Server() 26 | const devMiddleware = webpackDevMiddleware(compiler, serverOptions) 27 | const hotMiddleware = webpackHotMiddleware(compiler) 28 | 29 | server.app.webpackCompiler = compiler // eslint-disable-line no-param-reassign 30 | 31 | server.connection({ 32 | host, 33 | port, 34 | }) 35 | 36 | server.ext('onRequest', (request, reply) => { 37 | const req = request.raw.req 38 | const res = request.raw.res 39 | devMiddleware(req, res, (err) => { 40 | if (err) return reply(err) 41 | return reply.continue() 42 | }) 43 | }) 44 | 45 | server.ext('onRequest', (request, reply) => { 46 | const req = request.raw.req 47 | const res = request.raw.res 48 | hotMiddleware(req, res, (err) => { 49 | if (err) return reply(err) 50 | return reply.continue() 51 | }) 52 | }) 53 | 54 | server.start() 55 | 56 | console.log(`🚧 webpack packager has started at ${server.info.uri}`) 57 | -------------------------------------------------------------------------------- /src/server/todo/api.js: -------------------------------------------------------------------------------- 1 | import Boom from 'boom' 2 | import Joi from 'joi' 3 | import { server, logger, models } from 'hails' 4 | 5 | const { Todo } = models 6 | const nestedRoute = server.route.nested('/api/todos') 7 | 8 | nestedRoute.get('/', { 9 | tags: ['api'], 10 | }, async (request, reply) => { 11 | const todos = await Todo.findAll() 12 | logger.info('todo list %d', todos.length) 13 | reply({ 14 | url: '/todo', 15 | todos, 16 | }) 17 | }) 18 | 19 | nestedRoute.post('/', { 20 | tags: ['api'], 21 | validate: { 22 | payload: { 23 | title: Joi.string(), 24 | }, 25 | }, 26 | }, async (request, reply) => { 27 | const todo = await Todo.create({ 28 | title: request.payload.title, 29 | userId: 1, 30 | }) 31 | logger.info('add todo: %d %s', todo.id, todo.title) 32 | reply({ result: true, id: todo.id }) 33 | }) 34 | 35 | nestedRoute.put('/{id}', { 36 | tags: ['api'], 37 | }, async (request, reply) => { 38 | const todo = await Todo.findOne({ 39 | where: { 40 | id: request.params.id, 41 | }, 42 | }) 43 | if (todo === null) { 44 | return reply(Boom.notFound(`id ${request.params.id}`)) 45 | } 46 | todo.title = request.payload.title 47 | logger.info('put todo: %d %s', todo.id, todo.title) 48 | await todo.save() 49 | return reply({ result: true, id: todo.id }) 50 | }) 51 | 52 | nestedRoute.del('/{id}', { 53 | tags: ['api'], 54 | validate: { 55 | params: { 56 | id: Joi.number(), 57 | }, 58 | }, 59 | }, async (request, reply) => { 60 | const todo = await Todo.findOne({ id: request.params.id }) 61 | logger.info('delete todo: %d %s', todo.id, todo.title) 62 | await todo.destroy() 63 | reply({ result: true, id: todo.id }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/containers/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | import Helmet from 'react-helmet' 4 | 5 | export default class NotFound extends Component { 6 | static propTypes = { 7 | location: PropTypes.object, 8 | } 9 | 10 | static state = { 11 | menuVisible: false, 12 | } 13 | 14 | toggleMenu = () => { 15 | this.setState({ menuVisible: !this.state.menuVisible }) 16 | } 17 | 18 | renderHeader() { 19 | return ( 20 | 26 | ) 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 | 33 | 34 | {this.renderHeader()} 35 |
36 |

404 not found!

37 | 38 |
39 |
There is no such path.
40 |

41 | {this.props.location.pathname} 42 |

43 |
44 | 45 | back to the main 46 | 47 |
48 |
49 | * This page is not belongs to Container/App.js 50 |
51 |
52 |
53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/__tests__/InfoBar-test.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: "off" */ 2 | /* eslint react/no-find-dom-node: "off" */ 3 | 4 | import React from 'react' 5 | import ReactDOM from 'react-dom' 6 | import { renderIntoDocument } from 'react-addons-test-utils' 7 | import { expect } from 'chai' 8 | import { Provider } from 'react-redux' 9 | import { browserHistory } from 'react-router' 10 | 11 | import { configureStore } from '../../redux/configureStore' 12 | import ApiClient from '../../helpers/ApiClient' 13 | import { InfoBar } from '../../components' 14 | 15 | const client = new ApiClient() 16 | 17 | describe('InfoBar', () => { 18 | const mockStore = { 19 | info: { 20 | load: () => {}, 21 | loaded: true, 22 | loading: false, 23 | data: { 24 | message: 'This came from the api server', 25 | time: Date.now(), 26 | }, 27 | }, 28 | } 29 | const store = configureStore(browserHistory, client, mockStore) 30 | const renderer = renderIntoDocument( 31 | 32 | 33 | , 34 | ) 35 | const dom = ReactDOM.findDOMNode(renderer) 36 | 37 | it('should render correctly', () => expect(renderer).to.be.ok) 38 | 39 | it('should render with correct value', () => { 40 | const text = dom.getElementsByTagName('strong')[0].textContent 41 | expect(text).to.equal(mockStore.info.data.message) 42 | }) 43 | 44 | it('should render with a reload button', () => { 45 | const text = dom.getElementsByTagName('button')[0].textContent 46 | expect(text).to.be.a('string') 47 | }) 48 | 49 | it('should render the correct className', () => { 50 | const styles = require('../../components/InfoBar/InfoBar.scss') 51 | expect(styles.infoBar).to.be.a('string') 52 | expect(dom.className).to.include(styles.infoBar) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/redux/react-hapines/index.js: -------------------------------------------------------------------------------- 1 | /* code from vue-nes */ 2 | 3 | import Nes from 'nes' 4 | 5 | const SOCKET_CONNECTED = '@react-hapines/CONNECTED' 6 | const SOCKET_DISCONNECTED = '@react-hapines/DISCONNECTED' 7 | const SOCKET_MESSAGE = '@react-hapines/MESSAGE' 8 | 9 | class Socket { 10 | constructor(store) { 11 | this.store = store 12 | } 13 | connect(url) { 14 | if (this.client) { 15 | this.client.disconnect() 16 | } 17 | this.client = new Nes.Client(url) 18 | this.client.connect(() => { 19 | this.client.onUpdate = (update) => { 20 | this.store.dispatch({ 21 | type: SOCKET_MESSAGE, 22 | message: update, 23 | }) 24 | } 25 | console.log('[SOCKET] registered onUpdate handler') 26 | }) 27 | this.client.onConnect = () => { 28 | this.store.dispatch({ type: SOCKET_CONNECTED }) 29 | console.log('[SOCKET] connected') 30 | } 31 | this.client.onDisconnect = () => { 32 | this.store.dispatch({ type: SOCKET_DISCONNECTED }) 33 | console.log('[SOCKET] disconnected') 34 | } 35 | return this // for chaining 36 | } 37 | } 38 | 39 | const initialState = { 40 | connected: false, 41 | sequence: -1, 42 | message: {}, 43 | } 44 | 45 | export const reducer = (state = initialState, action = {}) => { 46 | switch (action.type) { 47 | case SOCKET_CONNECTED: { 48 | return { 49 | ...state, 50 | connected: true, 51 | } 52 | } 53 | case SOCKET_DISCONNECTED: { 54 | return { 55 | ...state, 56 | connected: true, 57 | } 58 | } 59 | case SOCKET_MESSAGE: { 60 | const sequence = state.sequence + 1 61 | return { 62 | state, 63 | sequence, 64 | message: action.message, 65 | } 66 | } 67 | default: 68 | return state 69 | } 70 | } 71 | 72 | export const connect = (store, url) => new Socket(store).connect(url) 73 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint no-console: "off" */ 3 | 4 | import React from 'react' 5 | import ReactDOM from 'react-dom' 6 | import ReactGA from 'react-ga' 7 | import { Provider } from 'react-redux' 8 | import { Router, browserHistory } from 'react-router' 9 | import { syncHistoryWithStore } from 'react-router-redux' 10 | import { ReduxAsyncConnect } from 'redux-connect' 11 | import useScroll from 'scroll-behavior/lib/useStandardScroll' 12 | 13 | import { configureStore } from './redux/configureStore' 14 | import ApiClient from './helpers/ApiClient' 15 | import getRoutes from './routes' 16 | import { connect as connectNes } from './redux/react-hapines' 17 | 18 | const client = new ApiClient() 19 | const bHistory = useScroll(() => browserHistory)() 20 | const dest = document.getElementById('content') 21 | const store = configureStore(bHistory, client, window.processedStore) 22 | const history = syncHistoryWithStore(bHistory, store) 23 | 24 | // hapi-nes websocket 25 | const wsUrl = `ws${window.location.protocol === 'https:' ? 's' : ''}://${window.location.host}` 26 | global.socket = connectNes(store, wsUrl) 27 | 28 | // google analytics 29 | ReactGA.initialize(settings.gacode) 30 | const logPageView = () => { 31 | ReactGA.set({ page: window.location.pathname + window.location.search }) 32 | ReactGA.pageview(window.location.pathname + window.location.search) 33 | } 34 | 35 | const RootComponent = () => ( 36 | 37 | 40 | !item.deferred} /> 41 | } 42 | history={history} 43 | > 44 | {getRoutes(store)} 45 | 46 | 47 | ) 48 | 49 | ReactDOM.render( 50 | , 51 | dest, 52 | ) 53 | 54 | if (DEVELOPMENT && module.hot) { 55 | module.hot.accept(() => { 56 | ReactDOM.render( 57 | , 58 | dest, 59 | ) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /src/containers/Chat/Chat.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | @connect( 5 | state => ({ user: state.auth.user }), 6 | ) 7 | export default class Chat extends Component { 8 | 9 | static propTypes = { 10 | user: PropTypes.object, 11 | } 12 | 13 | state = { 14 | message: '', 15 | messages: [], 16 | } 17 | 18 | componentDidMount() { 19 | if (socket) { 20 | socket.on('msg', this.onMessageReceived) 21 | setTimeout(() => { 22 | socket.emit('history', { offset: 0, length: 100 }) 23 | }, 100) 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | if (socket) { 29 | socket.removeListener('msg', this.onMessageReceived) 30 | } 31 | } 32 | 33 | onMessageReceived = (data) => { 34 | const messages = this.state.messages 35 | messages.push(data) 36 | this.setState({ messages }) 37 | } 38 | 39 | handleSubmit = (event) => { 40 | event.preventDefault() 41 | 42 | const msg = this.state.message 43 | 44 | this.setState({ message: '' }) 45 | 46 | socket.emit('msg', { 47 | from: this.props.user.username, 48 | text: msg, 49 | }) 50 | } 51 | 52 | render() { 53 | const style = require('./Chat.scss') 54 | const { user } = this.props 55 | 56 | return ( 57 |
58 |

Chat

59 | 60 | {user && 61 |
62 |
    63 | {this.state.messages.map(msg => ( 64 |
  • {msg.from}: {msg.text}
  • 65 | ))} 66 |
67 |
68 | { 73 | this.setState({ message: event.target.value }) 74 | }} 75 | /> 76 | 77 |
78 |
79 | } 80 |
81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tools/isomorphic-tools.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: "off" */ 2 | 3 | const WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin') 4 | 5 | // https://github.com/halt-hammerzeit/webpack-isomorphic-tools 6 | module.exports = { 7 | assets: { 8 | images: { 9 | extensions: [ 10 | 'jpeg', 11 | 'jpg', 12 | 'png', 13 | 'gif', 14 | ], 15 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser, 16 | }, 17 | fonts: { 18 | extensions: [ 19 | 'woff', 20 | 'woff2', 21 | 'ttf', 22 | 'eot', 23 | ], 24 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser, 25 | }, 26 | svg: { 27 | extension: 'svg', 28 | parser: WebpackIsomorphicToolsPlugin.url_loader_parser, 29 | }, 30 | style_modules: { 31 | extensions: ['scss'], 32 | filter: (module, regex, options, log) => { 33 | if (options.development) { 34 | // in development mode there's webpack "style-loader", 35 | // so the module.name is not equal to module.name 36 | return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log) 37 | } 38 | // in production mode there's no webpack "style-loader", 39 | // so the module.name will be equal to the asset path 40 | return regex.test(module.name) 41 | }, 42 | path: (module, options, log) => { 43 | if (options.development) { 44 | // in development mode there's webpack "style-loader", 45 | // so the module.name is not equal to module.name 46 | return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log) 47 | } 48 | // in production mode there's no webpack "style-loader", 49 | // so the module.name will be equal to the asset path 50 | return module.name 51 | }, 52 | parser: (module, options, log) => { 53 | if (options.development) { 54 | return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log) 55 | } 56 | // in production mode there's Extract Text Loader which extracts CSS text away 57 | return module.source 58 | }, 59 | }, 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /src/containers/About/About.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: "off" */ 2 | 3 | import React, { Component } from 'react' 4 | import Helmet from 'react-helmet' 5 | import { MiniInfoBar } from '../../components' 6 | 7 | export default class About extends Component { 8 | 9 | state = { 10 | showKitten: false, 11 | } 12 | 13 | handleToggleKitten = () => this.setState({ showKitten: !this.state.showKitten }) 14 | 15 | render() { 16 | const { showKitten } = this.state 17 | const kitten = require('./kitten.jpg') 18 | return ( 19 |
20 | 21 | 22 |
23 |
24 |

25 | About us 26 |
27 | (widgets) 28 |

29 |
30 |
31 | 32 |
33 | This project was created by Eunseok Eom 34 | (@eseom), 35 | based on 36 | (@erikras)'s project, 37 | react-redux-universal-hot-example. 38 |
39 | 40 |

41 | Mini Bar (not that kind) 42 |

43 | 44 |

45 | Hey! You found the mini info bar! The following component is 46 | display-only. Note that it shows the same 47 | time as the info bar. 48 |

49 | 50 |
51 | 52 |
53 | 54 |

Images

55 | 56 |

57 | Psst! Would you like to see a kitten? 58 | 65 |

66 | 67 | {showKitten &&
kitten
} 68 |
69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/containers/Login/ForgotPasswordForm.js: -------------------------------------------------------------------------------- 1 | /* eslint no-throw-literal: "off" */ 2 | 3 | import React, { Component } from 'react' 4 | import { Link } from 'react-router' 5 | import { reduxForm, Field } from 'redux-form' 6 | 7 | const styles = require('./Login.scss') 8 | 9 | const renderField = ({ 10 | input, 11 | label, 12 | type, 13 | meta: { touched, error }, 14 | }) => 15 |
16 | 17 | {touched && error &&
{error}
} 18 |
19 | 20 | @reduxForm({ 21 | form: 'joinForm', 22 | }) 23 | export default class extends Component { 24 | static displayName = 'JoinForm' 25 | 26 | render() { 27 | const { submitValidate, handleSubmit, submitting } = this.props 28 | return ( 29 |
30 |

Forgot password?

31 | 32 |

33 | 34 |

35 | 36 |

Enter your email to help us identify you.

37 | 38 | 44 | {/* 45 | 51 | */} 52 |
53 | {/* {error && {error}} */} 54 | 58 | {/* 59 | 63 | */} 64 | 65 |
66 | Already have an BeAmong ID? Login 67 |
68 |
69 | 70 |
71 | ) 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /src/server/user/model.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint no-param-reassign: "off" */ 3 | 4 | import bcrypt from 'bcrypt-nodejs' 5 | import { server } from 'hails' 6 | 7 | const { sequelize, DataTypes } = server 8 | 9 | const hasSecurePassword = (user, options, callback) => { 10 | if (user.password !== user.passwordConfirmation) { 11 | throw new Error("Password confirmation doesn't match Password") 12 | } 13 | bcrypt.hash(user.get('password'), null, null, (err, hash) => { 14 | if (err) return callback(err) 15 | user.set('passwordDigest', hash) 16 | return callback(null, options) 17 | }) 18 | } 19 | 20 | const { INTEGER, STRING, VIRTUAL, DATE } = DataTypes 21 | 22 | // https://nodeontrain.xyz/tuts/secure_password/ 23 | const User = sequelize.define('users', { 24 | id: { type: INTEGER, primaryKey: true, autoIncrement: true, scopes: ['public'] }, 25 | username: STRING, 26 | passwordDigest: { 27 | field: 'password', 28 | type: STRING, 29 | validate: { 30 | notEmpty: true, 31 | }, 32 | }, 33 | password: { 34 | field: 'passwordVirtual', 35 | type: VIRTUAL, 36 | allowNull: false, 37 | validate: { 38 | notEmpty: true, 39 | }, 40 | }, 41 | passwordConfirmation: { 42 | type: VIRTUAL, 43 | }, 44 | createdAt: { type: DATE, field: 'created_at' }, 45 | updatedAt: { type: DATE, field: 'updated_at' }, 46 | }, 47 | { 48 | indexes: [{ unique: true, fields: ['username'] }], 49 | instanceMethods: { 50 | authenticate: function authenticate(value) { 51 | if (bcrypt.compareSync(value, this.passwordDigest)) return true 52 | return false 53 | }, 54 | toJSON: function toJSON() { 55 | const values = Object.assign({}, this.get()) 56 | delete values.passwordDigest 57 | return values 58 | }, 59 | }, 60 | classMethods: { 61 | associate: (models) => { 62 | User.hasMany(models.Todo) 63 | }, 64 | }, 65 | createdAt: 'createdAt', 66 | updatedAt: 'updatedAt', 67 | }) 68 | 69 | User.beforeCreate((user, options, callback) => { 70 | user.username = user.username.toLowerCase() 71 | if (user.password) { 72 | return hasSecurePassword(user, options, callback) 73 | } 74 | return callback(null, options) 75 | }) 76 | User.beforeUpdate((user, options, callback) => { 77 | user.username = user.username.toLowerCase() 78 | if (user.password) hasSecurePassword(user, options, callback) 79 | return callback(null, options) 80 | }) 81 | 82 | export { 83 | User, 84 | } 85 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var babelrcObject = require('./package.json').babel 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | browsers: ['PhantomJS'], 8 | 9 | singleRun: !!process.env.CI, 10 | 11 | frameworks: [ 'mocha' ], 12 | 13 | files: [ 14 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 15 | './tools/tests-webpack.js' 16 | ], 17 | 18 | preprocessors: { 19 | './tools/tests.webpack.js': [ 'webpack', 'sourcemap' ] 20 | }, 21 | 22 | reporters: [ 'mocha' ], 23 | 24 | plugins: [ 25 | require("karma-webpack"), 26 | require("karma-mocha"), 27 | require("karma-mocha-reporter"), 28 | require("karma-phantomjs-launcher"), 29 | require("karma-sourcemap-loader") 30 | ], 31 | 32 | webpack: { 33 | devtool: 'inline-source-map', 34 | module: { 35 | loaders: [ 36 | { test: /\.(jpe?g|png|gif|svg)$/, loader: 'url', query: {limit: 10240} }, 37 | { test: /\.js$/, exclude: /node_modules/, use: [{ loader: 'babel-loader', options: babelrcObject }]}, 38 | { test: /\.json$/, loader: 'json-loader' }, 39 | { 40 | test: /Html\.scss$/, 41 | use: [ 42 | { loader: 'style-loader' }, 43 | { 44 | loader: 'css-loader', 45 | options: { 46 | sourceMap: true, 47 | modules: false, 48 | }, 49 | }, 50 | { 51 | loader: 'sass-loader', 52 | }, 53 | ], 54 | }, 55 | { 56 | test: /\.scss$/, 57 | exclude: /Html\.scss$/, 58 | use: [ 59 | { loader: 'style-loader' }, 60 | { 61 | loader: 'css-loader', 62 | options: { 63 | sourceMap: true, 64 | modules: true, 65 | }, 66 | }, 67 | { 68 | loader: 'sass-loader', 69 | }, 70 | ], 71 | }, 72 | ] 73 | }, 74 | resolve: { 75 | modules: [ 76 | 'src', 77 | 'node_modules' 78 | ], 79 | extensions: ['.json', '.js'] 80 | }, 81 | plugins: [ 82 | new webpack.IgnorePlugin(/\.json$/), 83 | new webpack.NoErrorsPlugin(), 84 | new webpack.DefinePlugin({ 85 | CLIENT: true, 86 | SERVER: false, 87 | DEVELOPMENT: true, 88 | }) 89 | ] 90 | }, 91 | webpackServer: { 92 | noInfo: true 93 | } 94 | }); 95 | }; 96 | -------------------------------------------------------------------------------- /src/redux/modules/auth.js: -------------------------------------------------------------------------------- 1 | const LOAD = 'auth/LOAD' 2 | const LOAD_SUCCESS = 'auth/LOAD_SUCCESS' 3 | const LOAD_FAIL = 'auth/LOAD_FAIL' 4 | const LOGIN = 'auth/LOGIN' 5 | const LOGIN_SUCCESS = 'auth/LOGIN_SUCCESS' 6 | const LOGIN_FAIL = 'auth/LOGIN_FAIL' 7 | const LOGOUT = 'auth/LOGOUT' 8 | const LOGOUT_SUCCESS = 'auth/LOGOUT_SUCCESS' 9 | const LOGOUT_FAIL = 'auth/LOGOUT_FAIL' 10 | 11 | const initialState = { 12 | loaded: false, 13 | } 14 | 15 | export default function reducer(state = initialState, action = {}) { 16 | switch (action.type) { 17 | case LOAD: 18 | return { 19 | ...state, 20 | loading: true, 21 | } 22 | case LOAD_SUCCESS: 23 | return { 24 | ...state, 25 | loading: false, 26 | loaded: true, 27 | user: action.result, 28 | } 29 | case LOAD_FAIL: 30 | return { 31 | ...state, 32 | loading: false, 33 | loaded: false, 34 | error: action.error, 35 | } 36 | case LOGIN: 37 | return { 38 | ...state, 39 | loggingIn: true, 40 | loginError: null, 41 | } 42 | case LOGIN_SUCCESS: 43 | return { 44 | ...state, 45 | loggingIn: false, 46 | user: action.result, 47 | loginError: null, 48 | } 49 | case LOGIN_FAIL: 50 | return { 51 | ...state, 52 | loggingIn: false, 53 | user: null, 54 | loginError: action.error, 55 | } 56 | case LOGOUT: 57 | return { 58 | ...state, 59 | loggingOut: true, 60 | } 61 | case LOGOUT_SUCCESS: 62 | return { 63 | ...state, 64 | loggingOut: false, 65 | user: null, 66 | } 67 | case LOGOUT_FAIL: 68 | return { 69 | ...state, 70 | loggingOut: false, 71 | logoutError: action.error, 72 | } 73 | default: 74 | return state 75 | } 76 | } 77 | 78 | export function isLoaded(globalState) { 79 | return globalState.auth && globalState.auth.loaded 80 | } 81 | 82 | export function load() { 83 | return { 84 | types: [LOAD, LOAD_SUCCESS, LOAD_FAIL], 85 | promise: client => client.get('/api/load-auth'), 86 | } 87 | } 88 | 89 | export function login(email, password) { 90 | return { 91 | types: [LOGIN, LOGIN_SUCCESS, LOGIN_FAIL], 92 | promise: client => client.post('/api/login', { 93 | data: { 94 | email, 95 | password, 96 | }, 97 | }), 98 | } 99 | } 100 | 101 | export function logout() { 102 | return { 103 | types: [LOGOUT, LOGOUT_SUCCESS, LOGOUT_FAIL], 104 | promise: client => client.get('/api/logout'), 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/containers/Login/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: "off" */ 2 | 3 | import React, { Component, PropTypes } from 'react' 4 | import { connect } from 'react-redux' 5 | import { Link } from 'react-router' 6 | import Helmet from 'react-helmet' 7 | import { reduxForm, SubmissionError } from 'redux-form' 8 | import * as authActions from '../../redux/modules/auth' 9 | import ForgotPasswordForm from './ForgotPasswordForm' 10 | 11 | @connect( 12 | store => ({ 13 | }), 14 | authActions, 15 | ) 16 | @reduxForm({ 17 | form: 'loginForm', 18 | }) 19 | export default class extends Component { 20 | static displayName = 'Login' 21 | 22 | static propTypes = { 23 | user: PropTypes.object, 24 | logout: PropTypes.func, 25 | } 26 | 27 | state = { 28 | requested: false, 29 | } 30 | 31 | onSubmit = (orig) => { 32 | const values = Object.assign({ 33 | email: '', 34 | }, orig) 35 | const seo = {} // submission error object 36 | let errorFound = false 37 | const reEmail = /^(([^<>()[\]\\.,;:\s@\\"]+(\.[^<>()[\]\\.,;:\s@\\"]+)*)|(\\".+\\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 38 | if (!reEmail.test(values.email)) { 39 | seo.email = 'email is invalid' 40 | errorFound = true 41 | } 42 | if (values.email.trim() === '') { 43 | seo.email = 'email is required' 44 | errorFound = true 45 | } 46 | if (errorFound) { 47 | throw new SubmissionError({ 48 | ...seo, 49 | _error: 'Login failed!', 50 | }) 51 | } 52 | 53 | // 비밀번호 찾기 요청 54 | // this.props.findPassword(values.email) 55 | 56 | this.setState({ 57 | requested: true, 58 | }) 59 | } 60 | 61 | render() { 62 | const styles = require('./Login.scss') 63 | return ( 64 |
65 | 66 | 67 | 68 | {this.state.requested ? 69 |
70 |

Check your email

71 |
72 | 73 |
74 |
75 | Email sent that you could find your account by. 76 | You would receive an email containing instructions on how to create a new password. 77 |
78 |
79 | Back to Login 80 |
81 |
82 | : 83 | 86 | } 87 |
88 | ) 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/helpers/Html.js: -------------------------------------------------------------------------------- 1 | /* eslint react/no-danger: "off" */ 2 | /* eslint global-require: "off" */ 3 | /* eslint import/no-dynamic-require: "off" */ 4 | /* eslint no-underscore-dangle: "off" */ 5 | 6 | import React, { Component, PropTypes } from 'react' 7 | import ReactDOM from 'react-dom/server' 8 | import serialize from 'serialize-javascript' 9 | import Helmet from 'react-helmet' 10 | 11 | /** 12 | * Wrapper component containing HTML metadata and boilerplate tags. 13 | * Used in server-side code only to wrap the string output of the 14 | * rendered route component. 15 | * 16 | * The only thing this component doesn't (and can't) include is the 17 | * HTML doctype declaration, which is added to the rendered output 18 | * by the server.js file. 19 | */ 20 | export default class Html extends Component { 21 | static propTypes = { 22 | assets: PropTypes.object, 23 | component: PropTypes.node, 24 | store: PropTypes.object, 25 | } 26 | 27 | render() { 28 | const { assets, component, store } = this.props 29 | const content = component ? ReactDOM.renderToString(component) : '' 30 | const head = Helmet.rewind() 31 | 32 | return ( 33 | 34 | 35 | {head.base.toComponent()} 36 | {head.title.toComponent()} 37 | {head.meta.toComponent()} 38 | {head.link.toComponent()} 39 | {head.script.toComponent()} 40 | 41 | 42 | 43 | 44 | 45 | 46 | {/* --- production mode --- */} 47 | {/* styles (will be present only in production with webpack extract text plugin) */} 48 | {Object.keys(assets.styles).length !== 0 ? 49 | Object.keys(assets.styles).map(style => ( 50 | 54 | )) : null} 55 | 56 | {/* --- development mode --- */} 57 | {/* outputs a