├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .sequelizerc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin ├── console.js ├── packager.js └── server.js ├── db ├── database.js ├── migrations │ ├── 20170124052953-create_tables-migration.js │ ├── 20170211161011-add-password-column.js │ └── 20170629090125-email-migration.js └── models │ └── .gitkeep ├── docker-compose.yml ├── issue.md ├── karma.conf.js ├── package.json ├── settings.sample.js ├── src ├── client.js ├── components │ ├── CounterButton │ │ └── CounterButton.js │ ├── InfoBar │ │ ├── InfoBar.js │ │ └── InfoBar.scss │ ├── MiniInfoBar │ │ └── MiniInfoBar.js │ ├── __tests__ │ │ └── InfoBar-test.js │ └── index.js ├── config.js ├── console.js ├── containers │ ├── About │ │ ├── About.js │ │ └── kitten.jpg │ ├── App │ │ ├── App.js │ │ └── App.scss │ ├── Chat │ │ ├── Chat.js │ │ └── Chat.scss │ ├── Home │ │ ├── Home.js │ │ ├── Home.scss │ │ ├── flux-logo.png │ │ ├── jumbo1.jpg │ │ └── logo.png │ ├── Items │ │ ├── Items.js │ │ └── Items.scss │ ├── Login │ │ ├── ForgotPassword.js │ │ ├── ForgotPasswordForm.js │ │ ├── Login.js │ │ ├── Login.scss │ │ └── LoginForm.js │ ├── LoginSuccess │ │ └── LoginSuccess.js │ ├── NotFound │ │ └── NotFound.js │ ├── Todo │ │ ├── Todo.js │ │ └── Todo.scss │ └── index.js ├── helpers │ ├── ApiClient.js │ ├── Html.js │ └── Html.scss ├── redux │ ├── configureStore.js │ ├── middleware │ │ └── clientMiddleware.js │ ├── modules │ │ ├── auth.js │ │ ├── counter.js │ │ ├── info.js │ │ └── items.js │ ├── react-hapines │ │ └── index.js │ └── reducers.js ├── routes.js ├── server │ ├── __tests__ │ │ ├── models-todo-test.js │ │ ├── models-user-test.js │ │ └── sample-test.js │ ├── core │ │ ├── api.js │ │ └── task.js │ ├── index.js │ ├── items │ │ ├── api.js │ │ ├── command.js │ │ └── getter.js │ ├── todo │ │ ├── api.js │ │ └── model.js │ └── user │ │ ├── api.js │ │ └── model.js └── styles │ └── styles.scss ├── static ├── favicon.ico └── favicon.png ├── tools ├── babel-require.js ├── isomorphic-tools.js ├── packager.js ├── tests.webpack.js ├── webpack.development.js └── webpack.production.js ├── tsconfig.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | karma.conf.js 2 | tests.webpack.js 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "vsicons.presets.angular": false 3 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-react-fullstack-boilerplate 2 | Hapi, Sequelize, React, Redux, Bootstrap, Kuejs, etc. 3 | 4 | react full web stack with rendering server, hapi echosystem for REAL Production. 5 | 6 | ## DEMO 7 | 8 | [heroku deployed demo](https://hapi-react-fullstack-bp.herokuapp.com/) 9 | 10 | ## Features 11 | * [HapiJS](https://github.com/hapijs/hapi) 12 | * [ReactJS](https://github.com/reactjs) 13 | * SSR 14 | * Webpack 2 (new webpack config structure) 15 | * [React-Router](https://github.com/rackt/react-router) 16 | * [Redux](https://github.com/reactjs/redux) 17 | * Document Head [React-Helmet](https://github.com/nfl/react-helmet) 18 | * [BabelJs](https://babeljs.io/) 19 | * Linting with eslint & jscs 20 | * Testing with karma, mocha 21 | * API Documentation [Swagger](https://github.com/glennjones/hapi-swagger) 22 | * [Sequelize](https://github.com/sequelize/sequelize) - cover traditional web apps. 23 | * session based authentication 24 | * scheduling job queue (for async jobs) 25 | * react-hapines 26 | * sass loader, node sass 27 | * react-ga 28 | 29 | ## Extra features 30 | * [hails](https://github.com/eseom/hails) - Django style Hapi module structure 31 | * custom console execution with the context 32 | ```bash 33 | yarn hails exec items get # see src/server/items/command.js 34 | ``` 35 | 36 | ## Usage 37 | **development** 38 | 39 | ```bash 40 | git clone https://github.com/eseom/hapi-react-fullstack-boilerplate.git 41 | cd 42 | yarn 43 | cp settings.sample.js settings.js 44 | vim settings.js (edit database connection or something) 45 | yarn hails db:upgrade # for db migration 46 | yarn dev 47 | ``` 48 | 49 | **db** 50 | ```bash 51 | yarn hails db:create # create a sequelize migration file in db/migrations/ 52 | yarn hails db:up # process pending migrations 53 | yarn hails db:down # revert the last migration 54 | yarn hails db:status # show migration status 55 | ``` 56 | 57 | **testing** 58 | 59 | ```bash 60 | yarn test 61 | yarn test:node 62 | yarn test:node:watch 63 | ``` 64 | 65 | **production** 66 | 67 | ```bash 68 | yarn build 69 | 70 | # acording to settings.js 71 | [PORT=4000 ] \ 72 | DATABASE_URL=postgres://@:/ \ 73 | REDIS_URL=redis://:@:/ \ 74 | yarn start 75 | ``` 76 | 77 | **API Interface** 78 | 79 | Path: /documentation 80 | 81 | ## Editor Configuration 82 | - **vscode** 83 | ``` 84 | code --install-extension dbaeumer.vscode-eslint 85 | ``` 86 | - **Atom** 87 | ```bash 88 | apm install editorconfig es6-javascript atom-ternjs javascript-snippets linter linter-eslint language-babel autocomplete-modules file-icons 89 | ``` 90 | 91 | ## Todo 92 | * [ ] oauth2 authentication... 93 | 94 | ## License 95 | The MIT License (MIT) 96 | 97 | Copyright (c) 2015 Roberto Ortis 98 | 99 | Permission is hereby granted, free of charge, to any person obtaining a copy 100 | of this software and associated documentation files (the "Software"), to deal 101 | in the Software without restriction, including without limitation the rights 102 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 103 | copies of the Software, and to permit persons to whom the Software is 104 | furnished to do so, subject to the following conditions: 105 | 106 | The above copyright notice and this permission notice shall be included in all 107 | copies or substantial portions of the Software. 108 | 109 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 110 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 111 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 112 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 113 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 114 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 115 | SOFTWARE. 116 | 117 | ## Reference 118 | 119 | ** initially forked from** 120 | 121 | * [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example) by [erikras](https://github.com/erikras) 122 | -------------------------------------------------------------------------------- /bin/console.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../tools/babel-require') 4 | require('../src/console') 5 | -------------------------------------------------------------------------------- /bin/packager.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../tools/packager') 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /db/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/31d460bb2fa243139062b8522d5e02fc9d8f0172/db/models/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /issue.md: -------------------------------------------------------------------------------- 1 | # yarn issue 2 | 3 | https://github.com/karma-runner/karma-phantomjs-launcher/issues/120 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-hapi-fullstack-boilerplate", 3 | "description": "a fullstack react webapp with redux, hmr, hapi, sequelize", 4 | "author": "EunseomEom (http://github.com/eseom)", 5 | "license": "MIT", 6 | "version": "0.1.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/eseom/react-hapi-fullstack-boilerplate" 10 | }, 11 | "homepage": "https://github.com/eseom/react-hapi-fullstack-boilerplate", 12 | "keywords": [ 13 | "react", 14 | "isomorphic", 15 | "universal", 16 | "webpack", 17 | "hapi", 18 | "sequelize", 19 | "hot reloading", 20 | "react-hot-reloader", 21 | "redux", 22 | "starter", 23 | "boilerplate", 24 | "babel" 25 | ], 26 | "babel": { 27 | "presets": [ 28 | [ 29 | "env", 30 | { 31 | "es2015": true 32 | } 33 | ], 34 | "stage-0", 35 | "react" 36 | ], 37 | "plugins": [ 38 | "transform-decorators-legacy" 39 | ] 40 | }, 41 | "main": "bin/server.js", 42 | "scripts": { 43 | "start": "npm-run-all server", 44 | "server": "better-npm-run server", 45 | "exec": "better-npm-run exec", 46 | "build": "better-npm-run build", 47 | "postinstall": "node node_modules/phantomjs-prebuilt/install.js; yarn build", 48 | "lint": "eslint -c .eslintrc src", 49 | "dev": "rm webpack-assets.json; npm-run-all --parallel dev:*", 50 | "dev:server": "better-npm-run dev:server", 51 | "dev:packager": "better-npm-run dev:packager", 52 | "test": "karma start", 53 | "test:node": "better-npm-run test:node", 54 | "test:node:watch": "better-npm-run test:node:watch" 55 | }, 56 | "betterScripts": { 57 | "server": { 58 | "command": "node bin/server", 59 | "env": { 60 | "NODE_PATH": "./src", 61 | "NODE_ENV": "production" 62 | } 63 | }, 64 | "exec": { 65 | "command": "node bin/console", 66 | "env": { 67 | "NODE_ENV": "production" 68 | } 69 | }, 70 | "dev:server": { 71 | "command": "nodemon bin/server", 72 | "env": { 73 | "NODE_PATH": "./src", 74 | "NODE_ENV": "development" 75 | } 76 | }, 77 | "dev:packager": { 78 | "command": "node bin/packager", 79 | "env": { 80 | "NODE_PATH": "./src" 81 | } 82 | }, 83 | "build": { 84 | "command": "webpack --verbose --colors --display-error-details --config ./tools/webpack.production.js", 85 | "env": { 86 | "NODE_ENV": "production" 87 | } 88 | }, 89 | "test:node": { 90 | "command": "mocha $(find src/server -name '*-test.js') --compilers js:./tools/babel-require", 91 | "env": { 92 | "NODE_ENV": "test" 93 | } 94 | }, 95 | "test:node:watch": { 96 | "command": "mocha $(find src/server -name '*-test.js') --compilers js:./tools/babel-require --watch", 97 | "env": { 98 | "NODE_ENV": "test" 99 | } 100 | } 101 | }, 102 | "dependencies": { 103 | "autoprefixer": "^6.7.6", 104 | "babel-eslint": "^7.2.2", 105 | "babel-loader": "^6.3.2", 106 | "babel-plugin-syntax-decorators": "^6.13.0", 107 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 108 | "babel-polyfill": "^6.23.0", 109 | "babel-preset-env": "^1.2.1", 110 | "babel-preset-latest": "^6.22.0", 111 | "babel-preset-react": "^6.23.0", 112 | "babel-preset-react-hmre": "^1.1.1", 113 | "babel-preset-stage-0": "^6.22.0", 114 | "babel-register": "^6.23.0", 115 | "bcrypt-nodejs": "^0.0.3", 116 | "better-npm-run": "^0.0.14", 117 | "bootstrap": "4.0.0-alpha.6", 118 | "cheerio": "^0.22.0", 119 | "google-map-react": "^0.22.3", 120 | "hails": "^0.2.11", 121 | "hapi-nested-route": "^0.1.1", 122 | "karma-cli": "^1.0.1", 123 | "karma-mocha": "^1.3.0", 124 | "karma-mocha-reporter": "^2.2.2", 125 | "karma-phantomjs-launcher": "^1.0.2", 126 | "karma-sourcemap-loader": "^0.3.7", 127 | "karma-webpack": "^2.0.2", 128 | "mocha": "^3.2.0", 129 | "nodemon": "^1.11.0", 130 | "phantomjs-prebuilt": "^2.1.14", 131 | "pretty-error": "^2.0.2", 132 | "react": "^15.4.2", 133 | "react-addons-css-transition-group": "^15.4.2", 134 | "react-addons-transition-group": "^15.4.2", 135 | "react-dom": "^15.4.2", 136 | "react-facebook-login": "^3.6.2", 137 | "react-ga": "^2.2.0", 138 | "react-helmet": "^4.0.0", 139 | "react-redux": "^5.0.3", 140 | "react-router": "^3.0.2", 141 | "react-router-redux": "^4.0.8", 142 | "redux": "^3.6.0", 143 | "redux-connect": "^5.0.0", 144 | "redux-form": "^6.5.0", 145 | "redux-thunk": "^2.2.0", 146 | "scroll-behavior": "0.3.2", 147 | "serialize-javascript": "^1.3.0", 148 | "superagent": "^3.5.0", 149 | "winston": "^2.3.1" 150 | }, 151 | "devDependencies": { 152 | "chai": "^3.5.0", 153 | "clean-webpack-plugin": "^0.1.15", 154 | "css-loader": "^0.26.2", 155 | "eslint": "^3.17.0", 156 | "eslint-config-airbnb": "^14.1.0", 157 | "eslint-loader": "^1.6.3", 158 | "eslint-plugin-import": "^2.2.0", 159 | "eslint-plugin-jsx-a11y": "^4.0.0", 160 | "eslint-plugin-react": "^6.10.0", 161 | "extract-text-webpack-plugin": "^2.1.0", 162 | "file-loader": "^0.10.1", 163 | "karma": "^1.5.0", 164 | "node-sass": "^4.5.0", 165 | "npm-run-all": "^4.0.2", 166 | "react-addons-test-utils": "^15.4.2", 167 | "sass-loader": "^6.0.3", 168 | "strip-loader": "^0.1.2", 169 | "style-loader": "^0.13.2", 170 | "url-loader": "^0.5.8", 171 | "webpack": "^2.2.1", 172 | "webpack-dev-middleware": "^1.10.1", 173 | "webpack-hot-middleware": "^2.17.1", 174 | "webpack-isomorphic-tools": "^3.0.1" 175 | }, 176 | "engines": { 177 | "node": ">8.0.0", 178 | "npm": ">5.0.0", 179 | "yarn": ">0.24.5" 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/About/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/31d460bb2fa243139062b8522d5e02fc9d8f0172/src/containers/About/kitten.jpg -------------------------------------------------------------------------------- /src/containers/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import Helmet from 'react-helmet' 4 | import { push } from 'react-router-redux' 5 | import { asyncConnect } from 'redux-connect' 6 | import { Link } from 'react-router' 7 | 8 | import { isLoaded as isInfoLoaded, load as loadInfo } from '../../redux/modules/info' 9 | import { isLoaded as isAuthLoaded, load as loadAuth, logout } from '../../redux/modules/auth' 10 | import { InfoBar } from '../../components' 11 | import config from '../../config' 12 | import '../../helpers/Html.scss' 13 | 14 | import styles from './App.scss' 15 | 16 | @asyncConnect([{ 17 | promise: ({ store: { dispatch, getState } }) => { 18 | const promises = [] 19 | 20 | if (!isInfoLoaded(getState())) { 21 | promises.push(dispatch(loadInfo())) 22 | } 23 | if (!isAuthLoaded(getState())) { 24 | promises.push(dispatch(loadAuth())) 25 | } 26 | 27 | return Promise.all(promises) 28 | }, 29 | }]) 30 | @connect( 31 | state => ({ 32 | user: state.auth.user && state.auth.user.username ? state.auth.user : null, 33 | }), 34 | { logout, pushState: push }) 35 | export default class App extends Component { 36 | static propTypes = { 37 | children: PropTypes.node.isRequired, 38 | user: PropTypes.object, 39 | logout: PropTypes.func.isRequired, 40 | pushState: PropTypes.func.isRequired, 41 | routes: PropTypes.array, 42 | } 43 | 44 | static contextTypes = { 45 | store: PropTypes.object.isRequired, 46 | } 47 | 48 | static state = { 49 | menuVisible: false, 50 | } 51 | 52 | constructor(props) { 53 | super(props) 54 | this.state = { 55 | activeItem: props.routes[1].path || 'home', 56 | } 57 | } 58 | 59 | componentWillReceiveProps(nextProps) { 60 | if (!this.props.user && nextProps.user) { 61 | // login 62 | this.props.pushState('/loginSuccess') 63 | } else if (this.props.user && !nextProps.user) { 64 | // logout 65 | this.props.pushState('/') 66 | } 67 | this.setState({ 68 | menuVisible: false, 69 | }) 70 | } 71 | 72 | handleLogout = (event) => { 73 | event.preventDefault() 74 | this.props.logout() 75 | } 76 | 77 | toggleMenu = () => { 78 | this.setState({ menuVisible: !this.state.menuVisible }) 79 | } 80 | 81 | renderHeader() { 82 | const { user } = this.props 83 | const { activeItem } = this.state 84 | return ( 85 | 133 | ) 134 | } 135 | 136 | render() { 137 | return ( 138 |
139 | 140 | 141 | {this.renderHeader()} 142 |
143 |

144 |

145 | {this.props.children} 146 |
147 |
148 | 149 |
150 | 151 |
152 |
153 | ) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/containers/Home/Home.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: "off" */ 2 | 3 | import React, { Component } from 'react' 4 | import Helmet from 'react-helmet' 5 | import { connect } from 'react-redux' 6 | import { CounterButton } from '../../components' 7 | import config from '../../config' 8 | 9 | @connect(store => ({ 10 | message: store.hapines.message, 11 | }), {}) 12 | export default class Home extends Component { 13 | render() { 14 | const styles = { 15 | thumb1: { 16 | backgroundPosition: '50% 50% !important', 17 | backgroundSize: 'cover !important', 18 | width: '100%', 19 | height: 300, 20 | }, 21 | } 22 | const items = [ 23 | 'HapiJS', 24 | 'ReactJS 15', 25 | 'SSR (server side rendering)', 26 | 'no local proxy for development and production', 27 | 'Webpack2 + React-transform and react-transform-hmr', 28 | 'React-Router 3', 29 | 'Redux', 30 | 'Document Head React-Helmet', 31 | 'BabelJs', 32 | 'Linting with eslint with airbnb javascript', 33 | 'Testing with karma, mocha', 34 | 'API Documentation Swagger', 35 | 'Sequelize - covers traditional web apps.', 36 | 'session based authentication', 37 | 'react-hapines', 38 | 'sass loader, node sass', 39 | ] 40 | return ( 41 |
42 | 43 | 44 |
45 |
46 |

47 | {config.app.title} 48 |
49 | (list, link, message, redux) 50 |

51 |
52 | 53 |
54 | last time from server through react-hapines 55 | { 56 | this.props.message.now ? 57 |
58 |
59 | {new Date(this.props.message.now).toLocaleString()} 60 |
61 | 68 |
69 | : 70 |
71 | no message yet 72 |
73 | } 74 |
75 | 76 |
77 |

78 | {config.app.description} 79 |

80 |

81 | 86 | {' '} 87 | VIEW ON GITHUB 88 | 89 |

90 |
91 | 92 | 96 |
97 | 98 |

Features

99 |
    100 | {items.map((it, index) => ( 101 |
  • {it}
  • 102 | ))} 103 |
104 | 105 |

simple redux example

106 |

107 | 108 |

109 | 110 |
111 |
112 |

113 |    114 | Think like a man of action and
act like man of thought. 115 |

116 |
117 |
118 | 119 |

120 | 121 |

    122 | {items.map((it, index) => ( 123 |
  • {it}
  • 124 | ))} 125 |
126 |
127 | 128 | ) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /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/containers/Home/flux-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/31d460bb2fa243139062b8522d5e02fc9d8f0172/src/containers/Home/flux-logo.png -------------------------------------------------------------------------------- /src/containers/Home/jumbo1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/31d460bb2fa243139062b8522d5e02fc9d8f0172/src/containers/Home/jumbo1.jpg -------------------------------------------------------------------------------- /src/containers/Home/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eseom/hapi-react-fullstack-boilerplate/31d460bb2fa243139062b8522d5e02fc9d8f0172/src/containers/Home/logo.png -------------------------------------------------------------------------------- /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 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {items.map(it => ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | ))} 68 | 69 | 70 | 71 | 72 | 75 | 76 | 77 |
53 | PackageVersionNote
{it.id}{it.name.text}{it.version.text}{it.note}
73 | footer 74 |
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 | 106 | : 107 | 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/containers/Items/Items.scss: -------------------------------------------------------------------------------- 1 | .items { 2 | } 3 | -------------------------------------------------------------------------------- /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/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/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 | 101 |
102 |
103 | : 104 | 110 | } 111 |
112 | ) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/containers/Login/LoginForm.js: -------------------------------------------------------------------------------- 1 | /* eslint no-throw-literal: "off" */ 2 | 3 | import React, { Component } from 'react' 4 | import FacebookLogin from 'react-facebook-login' 5 | import { Link } from 'react-router' 6 | import { reduxForm, Field } from 'redux-form' 7 | 8 | const styles = require('./Login.scss') 9 | 10 | const renderField = ({ 11 | input, 12 | label, 13 | type, 14 | meta: { touched, error }, 15 | }) => 16 |
17 | 18 | {touched && error &&
{error}
} 19 |
20 | 21 | @reduxForm({ 22 | form: 'joinForm', 23 | }) 24 | export default class extends Component { 25 | static displayName = 'LoginForm' 26 | 27 | state = { 28 | loginError: {}, 29 | } 30 | 31 | componentWillReceiveProps(nextProps) { 32 | // 부모로부터 받은 loginError 가 새로 생성되었을 경우만 state 를 갱신함 33 | if (this.props.loginError.message !== '' && nextProps.loginError.message !== '') { 34 | this.setState({ 35 | loginError: {}, 36 | }) 37 | } else if (this.props.loginError.message === '' && nextProps.loginError.message !== '') { 38 | this.setState({ 39 | loginError: nextProps.loginError, 40 | }) 41 | } 42 | } 43 | 44 | render() { 45 | const { submitValidate, handleSubmit, loggingIn } = this.props 46 | return ( 47 |
48 |

Login sign in with you ID

49 | 50 |

51 | 52 |

53 |
54 | This will *log you in* as this user, 55 | storing the email in the session of the API server. 56 |
57 |
58 | email: tester@hrfb.com, password: 1234 59 |
60 |
61 | 62 |
63 | 64 |
65 | } 72 | /> 73 |
74 | 75 |
76 | 77 |
or login with email
78 | 79 | 85 | 91 |
92 | {this.state.loginError.message &&
{this.state.loginError.message}
} 96 | 97 | 98 | Forgot password? 99 | 100 | 105 | 106 |
107 | Not a member yet? Get an ID 108 |
109 |
110 | 111 |
112 |
113 | The api respond delayed 114 | 1 second on succeed, 115 | 2 seconds on failure. 116 |
117 |
118 | ) 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /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/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/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/containers/Todo/Todo.scss: -------------------------------------------------------------------------------- 1 | .todo { 2 | } 3 | -------------------------------------------------------------------------------- /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/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/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