├── 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 |
18 | You have clicked me {count} time{count === 1 ? '' : 's'}.
19 |
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 |
24 | {' '}
25 | Reload from server
26 |
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 | {' '}Log Out
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 |
21 |
22 |
23 |
24 | hapi react fullstack boilerplate
25 |
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 |
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 |
63 | {showKitten ? 'No! Take it away!' : 'Yes! Please!'}
64 |
65 |
66 |
67 | {showKitten &&
}
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 |
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 tag with all required css files. */}
58 | {Object.keys(assets.styles).length === 0 ?
59 | Object.keys(assets.assets).filter(it => (
60 | /* only css assets */
61 | it.slice(-5) === '.scss'
62 | )).map(it => (
63 |
64 | ))
65 | :
66 | null
67 | }
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | )
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/containers/Login/Login.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: "off" */
2 |
3 | import React, { Component, PropTypes } from 'react'
4 | import { connect } from 'react-redux'
5 | import Helmet from 'react-helmet'
6 | import { reduxForm, SubmissionError } from 'redux-form'
7 | import * as authActions from '../../redux/modules/auth'
8 | import LoginForm from './LoginForm'
9 |
10 | const defaultLoginError = { error: false, message: '' }
11 |
12 | const responseFacebook = (response) => {
13 | authActions.loginFacebook(response)
14 | }
15 |
16 | @connect(
17 | store => ({
18 | user: store.auth.user && store.auth.user.username ? store.auth.user : null,
19 | loginError: store.auth.loginError,
20 | loggingIn: store.auth.loggingIn,
21 | }),
22 | authActions,
23 | )
24 | @reduxForm({
25 | form: 'loginForm',
26 | })
27 | export default class extends Component {
28 | static displayName = 'Login'
29 |
30 | static propTypes = {
31 | user: PropTypes.object,
32 | loginError: PropTypes.object,
33 | login: PropTypes.func,
34 | logout: PropTypes.func,
35 | }
36 |
37 | state = {
38 | loginError: defaultLoginError,
39 | }
40 |
41 | onSubmit = (orig) => {
42 | event.preventDefault()
43 | const values = Object.assign({
44 | email: '',
45 | password: '',
46 | }, orig)
47 | const seo = {} // submission error object
48 | let errorFound = false
49 | 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,}))$/
50 | if (!reEmail.test(values.email)) {
51 | seo.email = 'email is invalid'
52 | errorFound = true
53 | }
54 | if (values.email.trim() === '') {
55 | seo.email = 'email is required'
56 | errorFound = true
57 | }
58 | if (values.password === '') {
59 | seo.password = 'password is required'
60 | errorFound = true
61 | }
62 | if (errorFound) {
63 | throw new SubmissionError({
64 | ...seo,
65 | _error: 'Login failed!',
66 | })
67 | }
68 |
69 | // 로그인 요청
70 | this.props.login(values.email, values.password)
71 | return false
72 | }
73 |
74 | componentWillReceiveProps(props) {
75 | if (!this.props.user && props.user) { // logged in
76 | props.router.push('/')
77 | return
78 | }
79 | this.setState({
80 | loginError: (!this.props.loginError && props.loginError ?
81 | props.loginError : defaultLoginError),
82 | })
83 | }
84 |
85 | render() {
86 | const styles = require('./Login.scss')
87 | const {
88 | user, logout, loggingIn,
89 | } = this.props
90 | const { loginError } = this.state
91 | return (
92 |
93 |
94 |
95 |
96 | {user ?
97 |
98 |
Hi, you are currently logged in as {user.username}.
99 |
100 | {' '}Log Out
101 |
102 |
103 | :
104 |
110 | }
111 |
112 | )
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/containers/Items/Items.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { connect } from 'react-redux'
3 | import { asyncConnect } from 'redux-connect'
4 |
5 | import * as itemsActions from '../../redux/modules/items'
6 |
7 | const { isLoaded, load: loadItems } = itemsActions
8 |
9 | @asyncConnect([{
10 | promise: ({ store: { dispatch, getState } }) => {
11 | if (!isLoaded(getState())) {
12 | return dispatch(loadItems())
13 | }
14 | return undefined
15 | },
16 | }])
17 | @connect(store => ({
18 | items: store.items.data,
19 | error: store.items.error,
20 | loading: store.items.loading,
21 | }),
22 | { ...itemsActions })
23 | export default class extends Component {
24 |
25 | static propTypes = {
26 | items: PropTypes.array,
27 | error: PropTypes.object,
28 | loading: PropTypes.bool,
29 | load: PropTypes.func.isRequired,
30 | // editStart: PropTypes.func.isRequired,
31 | }
32 |
33 | renderTable = (error, items) => {
34 | if (!error && !items) {
35 | return (
36 |
37 | loading...
38 |
39 | )
40 | }
41 | if (error) {
42 | return (
43 |
44 | failed to load items.
45 |
46 | )
47 | }
48 | return (
49 |
50 |
51 |
52 |
53 | Package
54 | Version
55 | Note
56 |
57 |
58 |
59 |
60 | {items.map(it => (
61 |
62 | {it.id}
63 | {it.name.text}
64 | {it.version.text}
65 | {it.note}
66 |
67 | ))}
68 |
69 |
70 |
71 |
72 |
73 | footer
74 |
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | render() {
82 | const { items, error, loading, load } = this.props
83 | let refreshClassName = 'refresh'
84 | if (loading) {
85 | refreshClassName = 'asterisk'
86 | }
87 | return (
88 |
89 |
90 |
91 |
92 | Items
93 |
94 | (fetching data asynchronously)
95 |
96 |
97 |
98 |
99 |
100 | A table may be formatted to emphasize a first column that defines a row content.
101 |
102 |
103 |
104 | {loading ?
105 | Loading
106 | :
107 |
108 |
109 | {' '}
110 | Reload
111 |
112 | }
113 |
114 |
115 |
Packages Tracked by DistroWatch
116 |
https://distrowatch.com/packages.php
117 |
118 | {this.renderTable(error, items)}
119 |
120 | )
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: "off" */
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom/server'
5 | import PrettyError from 'pretty-error'
6 | import { match } from 'react-router'
7 | import { syncHistoryWithStore } from 'react-router-redux'
8 | import { ReduxAsyncConnect, loadOnServer } from 'redux-connect'
9 | import createHistory from 'react-router/lib/createMemoryHistory'
10 | import { Provider } from 'react-redux'
11 | import fs from 'fs'
12 | import path from 'path'
13 | import { server, logger } from 'hails'
14 |
15 | import { configureStore } from '../redux/configureStore'
16 | import ApiClient from '../helpers/ApiClient'
17 | import Html from '../helpers/Html'
18 | import getRoutes from '../routes'
19 | import settingsFile from '../../settings'
20 |
21 | const settings = settingsFile[process.env.NODE_ENV]
22 | settings.plugins = [
23 | require('hapi-nested-route'),
24 | ]
25 |
26 | const start = async () => {
27 | try {
28 | const done = await server.init(settings)
29 | const pretty = new PrettyError()
30 |
31 | server.route({
32 | method: '*',
33 | path: '/{p*}',
34 | handler: (request, reply) => {
35 | if (request.path !== '/') {
36 | const fPath = path.resolve(`${__dirname}/../../static/${request.path}`)
37 | try {
38 | if (fs.statSync(fPath)) {
39 | return reply.file(fPath)
40 | }
41 | } catch (e) {
42 | /* empty */
43 | }
44 | }
45 |
46 | if (DEVELOPMENT) {
47 | // Do not cache webpack stats: the script file would change since
48 | // hot module replacement is enabled in the development env
49 | webpackIsomorphicTools.refresh()
50 | }
51 | const client = new ApiClient(request)
52 | const memoryHistory = createHistory(request.path)
53 | const store = configureStore(memoryHistory, client)
54 | const history = syncHistoryWithStore(memoryHistory, store)
55 |
56 | function hydrateOnClient() {
57 | return reply(`${ReactDOM.renderToString(