├── screenshot.jpg ├── src ├── electron │ ├── .gitignore │ ├── src │ │ ├── globals.js │ │ └── state.js │ ├── package.json │ ├── index.js │ └── index.html ├── shared │ ├── stores │ │ ├── app.js │ │ ├── ui │ │ │ ├── PostCreateModal.js │ │ │ ├── AppBar.js │ │ │ ├── AppNav.js │ │ │ ├── SnackBar.js │ │ │ └── Auth.js │ │ ├── ui.js │ │ ├── auth.js │ │ └── post.js │ ├── styles │ │ ├── AppNav.css │ │ ├── AppLayout.css │ │ ├── AppBar.css │ │ ├── PostList.css │ │ ├── _.modal.js │ │ ├── _.material.js │ │ ├── Home.css │ │ ├── _.global.css │ │ ├── MenuLinkDX.css │ │ ├── _.custom.css │ │ └── _.mixins.js │ ├── components │ │ ├── PostInfo.jsx │ │ ├── form │ │ │ ├── inputs │ │ │ │ └── MaterialTextField.jsx │ │ │ ├── AuthLogin.jsx │ │ │ ├── AuthRegister.jsx │ │ │ └── controls │ │ │ │ └── FormControls.jsx │ │ ├── AppNav.jsx │ │ ├── MenuLinksSX.jsx │ │ ├── PostSearch.jsx │ │ ├── AuthModal.jsx │ │ ├── Pagination.jsx │ │ ├── PostListHeader.jsx │ │ ├── PostFilter.jsx │ │ ├── PostDetailsHeader.jsx │ │ ├── PostListBar.jsx │ │ ├── AuthForm.jsx │ │ ├── AppBar.jsx │ │ ├── PostDetails.jsx │ │ ├── PostList.jsx │ │ ├── PostCreateModal.jsx │ │ └── MenuLinksDX.jsx │ ├── containers │ │ ├── NotFound.jsx │ │ ├── Auth.jsx │ │ ├── Messages.jsx │ │ ├── Message.jsx │ │ ├── Home.jsx │ │ ├── AppLayout.jsx │ │ ├── Breakpoints.jsx │ │ └── Packages.jsx │ ├── forms │ │ ├── _.bindings.js │ │ ├── auth.js │ │ ├── _.extend.js │ │ ├── post.js │ │ └── user.js │ ├── stores.js │ ├── app.js │ └── routes.jsx ├── web │ ├── bootstrap.js │ ├── middleware │ │ ├── serveStatic.js │ │ ├── hot.js │ │ └── routing.js │ ├── App.jsx │ ├── views │ │ └── index.ejs │ ├── client.jsx │ ├── server.js │ └── ssr.js ├── api │ ├── hooks │ │ ├── addDelay.js │ │ ├── timestamp.js │ │ ├── setupJWTPayload.js │ │ └── setUUID.js │ ├── middleware │ │ ├── notFound.js │ │ └── logger.js │ ├── services │ │ ├── post │ │ │ ├── config.js │ │ │ ├── hooks.after.js │ │ │ ├── hooks.before.js │ │ │ └── model.js │ │ └── user │ │ │ ├── config.js │ │ │ ├── hooks.after.js │ │ │ ├── model.js │ │ │ └── hooks.before.js │ ├── connector.js │ ├── auth.js │ ├── autoloader.js │ └── server.js ├── seeds │ ├── handlers │ │ └── development.js │ └── factories │ │ ├── post.js │ │ └── user.js └── utils │ ├── jwt.js │ ├── seeder.runner.js │ ├── authorize.hoc.jsx │ ├── services.autoload.js │ ├── server.start.js │ └── logger.js ├── .gitignore ├── public └── static │ └── img │ └── bg.jpg ├── run ├── start.api.js ├── global.js ├── start.web.js └── start.seeder.js ├── webpack ├── .eslintrc ├── globals.js ├── config.client.js ├── config.server.js ├── loaders.js ├── config.server.build.js ├── config.client.dev.js ├── config.server.dev.js └── config.client.build.js ├── config ├── feathers │ ├── production.json │ └── default.json ├── hot.js ├── expose.js ├── vendor.js ├── postcss.js └── dir.js ├── .env ├── .babelrc ├── .eslintrc ├── CHANGELOG.md ├── webpack.config.client.babel.js ├── LICENSE ├── webpack.config.babel.js ├── README.md ├── package.json └── DOCUMENTATION.md /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxhound87/rfx-stack/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /src/electron/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .DS_Store 3 | npm-debug.log 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /src/shared/stores/app.js: -------------------------------------------------------------------------------- 1 | export default class AppStore { 2 | 3 | ssrLocation = null; 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /public/build 2 | /run/build 3 | /node_modules 4 | .DS_Store 5 | npm-debug.log 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /public/static/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxhound87/rfx-stack/HEAD/public/static/img/bg.jpg -------------------------------------------------------------------------------- /run/start.api.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./global'); 3 | require('../src/api/server'); 4 | -------------------------------------------------------------------------------- /src/electron/src/globals.js: -------------------------------------------------------------------------------- 1 | /* 2 | Globals 3 | */ 4 | global.ELECTRON = 'ELECTRON'; 5 | global.HOT = 'HOT'; 6 | -------------------------------------------------------------------------------- /src/shared/styles/AppNav.css: -------------------------------------------------------------------------------- 1 | .drawer a { 2 | padding: 15px; 3 | } 4 | 5 | .drawer a:hover { 6 | background: #3A506B; 7 | } 8 | -------------------------------------------------------------------------------- /webpack/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/web/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | Store Bootstrap 3 | @return array of promises 4 | */ 5 | export default store => ([ 6 | store.auth.authenticate(), 7 | ]); 8 | -------------------------------------------------------------------------------- /config/feathers/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "secret": "JNoSF4uRkGB1zhccVkpX3ulB/1KErbj/tuhCTY0dkpVNyYTU8bJiNC4ErCUK4sDSe/YMjuC0kqtlYnWuK8tHsQ==" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/styles/AppLayout.css: -------------------------------------------------------------------------------- 1 | .su { 2 | min-height: 400px; 3 | margin-left: 256px; 4 | } 5 | 6 | .content { 7 | padding-top: 0; 8 | display: block; 9 | } 10 | -------------------------------------------------------------------------------- /src/api/hooks/addDelay.js: -------------------------------------------------------------------------------- 1 | // Add a delay to test slower connections 2 | export function addDelay(delay) { 3 | return (hook, next) => { 4 | setTimeout(next, delay); 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /run/global.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const path = require('path'); 3 | const dir = require('../config/dir').default; 4 | 5 | dotenv.config(); 6 | global.DIR = dir(path); 7 | -------------------------------------------------------------------------------- /src/api/hooks/timestamp.js: -------------------------------------------------------------------------------- 1 | export function timestamp(name) { 2 | return (hook, next) => { 3 | const data = hook.data; 4 | data[name] = new Date(); 5 | next(); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/electron/src/state.js: -------------------------------------------------------------------------------- 1 | /* eslint no-underscore-dangle: 0 */ 2 | 3 | /* 4 | Initial State 5 | */ 6 | window.__STATE = { 7 | app: { ssrLocation: '#/' }, 8 | ui: {}, 9 | }; 10 | -------------------------------------------------------------------------------- /src/api/middleware/notFound.js: -------------------------------------------------------------------------------- 1 | import errors from 'feathers-errors'; 2 | 3 | export default function () { 4 | return (req, res, next) => next(new errors.NotFound('Page not found')); 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/styles/AppBar.css: -------------------------------------------------------------------------------- 1 | .bar { 2 | z-index: 999; 3 | background: none; 4 | color: #5BC0BE; 5 | } 6 | 7 | .leftShifted { 8 | left: 256px; 9 | } 10 | 11 | .openNavBtn:hover { 12 | color: #6FFFE9; 13 | } 14 | -------------------------------------------------------------------------------- /run/start.web.js: -------------------------------------------------------------------------------- 1 | require('./global'); 2 | 3 | // neded for css import on node 4 | require('css-modules-require-hook')({ 5 | generateScopedName: '[name]__[local]___[hash:base64:5]', 6 | }); 7 | 8 | require('../src/web/server'); 9 | -------------------------------------------------------------------------------- /src/api/services/post/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | model: 'post', 3 | namespace: '/post', 4 | options: { 5 | id: 'uuid', 6 | paginate: { 7 | default: 25, 8 | max: 50, 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/services/user/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | model: 'user', 3 | namespace: '/user', 4 | options: { 5 | id: 'uuid', 6 | paginate: { 7 | default: 25, 8 | max: 50, 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/shared/styles/PostList.css: -------------------------------------------------------------------------------- 1 | .postList ul { 2 | list-style: none; 3 | padding: 0; 4 | } 5 | 6 | .postList ul li { 7 | border-bottom: 1px solid #ccc; 8 | } 9 | 10 | .postList h3 { 11 | font-weight: 500; 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/stores/ui/PostCreateModal.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { toggle } from 'rfx-core'; 3 | 4 | @toggle('open', 'isOpen') 5 | export default class PostCreateModal { 6 | 7 | @observable isOpen = false; 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/stores/ui/AppBar.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { toggle } from 'rfx-core'; 3 | 4 | @toggle('toggleAccountMenu', 'accountMenuIsOpen') 5 | export default class AppBar { 6 | 7 | @observable accountMenuIsOpen = false; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/hooks/setupJWTPayload.js: -------------------------------------------------------------------------------- 1 | export function setupJWTPayload() { 2 | return (hook) => { 3 | // eslint-disable-next-line 4 | hook.data.payload = { 5 | userId: hook.params.user.id, 6 | }; 7 | 8 | return Promise.resolve(hook); 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /config/hot.js: -------------------------------------------------------------------------------- 1 | /* 2 | Webpack Dev Middleware Config 3 | */ 4 | export const wdmc = ({ 5 | historyApiFallback: false, 6 | quiet: true, 7 | hot: true, 8 | }); 9 | 10 | /* 11 | Webpack Hot Middleware Config 12 | */ 13 | export const whmc = ({ 14 | // ... 15 | }); 16 | -------------------------------------------------------------------------------- /src/api/connector.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export function connector(config) { 4 | const { host, port, name } = config; 5 | const uri = ['mongodb://', host, ':', port, '/', name].join(''); 6 | mongoose.Promise = global.Promise; 7 | return mongoose.connect(uri); 8 | } 9 | -------------------------------------------------------------------------------- /src/shared/stores/ui/AppNav.js: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | import { toggle } from 'rfx-core'; 3 | 4 | @toggle('open', 'isOpen') 5 | @toggle('dock', 'isDocked') 6 | export default class AppNav { 7 | 8 | @observable isOpen = false; 9 | @observable isDocked = false; 10 | } 11 | -------------------------------------------------------------------------------- /run/start.seeder.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./global'); 3 | 4 | const getenv = require('getenv'); 5 | const env = require('../config/expose'); 6 | 7 | global.CONFIG = getenv.multi(env).default; 8 | 9 | require('../src/utils/seeder.runner') 10 | .default('./src/seeds/'); 11 | -------------------------------------------------------------------------------- /src/shared/components/PostInfo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | export default observer(({ itemsFound, currentPage, totalPages }) => ( 5 |
6 | {itemsFound} Items found - Page {currentPage} of {totalPages} 7 |
8 | )); 9 | -------------------------------------------------------------------------------- /src/web/middleware/serveStatic.js: -------------------------------------------------------------------------------- 1 | import serveStatic from 'serve-static'; 2 | 3 | export function serveStaticMiddleware() { 4 | const app = this; 5 | const Dir = global.DIR; 6 | 7 | app.use('/build', serveStatic(Dir.staticBuild)); 8 | app.use('/static', serveStatic(Dir.static)); 9 | } 10 | -------------------------------------------------------------------------------- /webpack/globals.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | const Dir = global.DIR; 4 | 5 | export default { 6 | resolve: { 7 | modules: ['node_modules'], 8 | extensions: ['.js', '.jsx', '.json'], 9 | alias: { 10 | react: path.join(Dir.modules, 'react'), 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/seeds/handlers/development.js: -------------------------------------------------------------------------------- 1 | import { userSeederDevelopment as userSeeder } from '@/seeds/factories/user'; 2 | import { postSeederDevelopment as postSeeder } from '@/seeds/factories/post'; 3 | 4 | export function handle() { 5 | return [ 6 | userSeeder(), 7 | postSeeder(50), 8 | ]; 9 | } 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_CONFIG_DIR=./config/feathers/ 2 | 3 | BROWSERSYNC_HOST=localhost 4 | BROWSERSYNC_PORT=3100 5 | 6 | WEB_HOST=localhost 7 | WEB_PORT=3000 8 | 9 | API_HOST=localhost 10 | API_PORT=9090 11 | 12 | IO_HOST=localhost 13 | IO_PORT=9090 14 | 15 | DB_HOST=localhost 16 | DB_NAME=aggregator 17 | DB_PORT=27017 18 | -------------------------------------------------------------------------------- /config/expose.js: -------------------------------------------------------------------------------- 1 | /* 2 | Expose Env to Client Side 3 | */ 4 | export default ({ 5 | web: { 6 | host: 'WEB_HOST', 7 | port: 'WEB_PORT', 8 | }, 9 | api: { 10 | host: 'API_HOST', 11 | port: 'API_PORT', 12 | }, 13 | io: { 14 | host: 'IO_HOST', 15 | port: 'IO_PORT', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/api/services/post/hooks.after.js: -------------------------------------------------------------------------------- 1 | import hooks from 'feathers-hooks'; 2 | 3 | /** 4 | Hook: after 5 | Service: post 6 | */ 7 | export default { 8 | all: [ 9 | hooks.remove('__v', '_id'), 10 | ], 11 | find: [], 12 | get: [], 13 | create: [], 14 | update: [], 15 | patch: [], 16 | remove: [], 17 | }; 18 | -------------------------------------------------------------------------------- /src/api/services/user/hooks.after.js: -------------------------------------------------------------------------------- 1 | import hooks from 'feathers-hooks'; 2 | 3 | /** 4 | Hook: after 5 | Service: user 6 | */ 7 | export default { 8 | all: [ 9 | hooks.remove('__v', 'password'), 10 | ], 11 | find: [ 12 | ], 13 | get: [], 14 | create: [], 15 | update: [], 16 | patch: [], 17 | remove: [], 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/components/form/inputs/MaterialTextField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import TextField from 'material-ui/TextField'; 4 | 5 | export default observer(({ field, type = 'text', placeholder = null }) => ( 6 |
7 |
8 |
9 | )); 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": [ 4 | "transform-decorators-legacy", 5 | "transform-class-properties", 6 | "transform-runtime", 7 | ["babel-root-import", [{ 8 | "rootPathPrefix": "~", 9 | "rootPathSuffix": "." 10 | }, { 11 | "rootPathPrefix": "@", 12 | "rootPathSuffix": "src" 13 | }]] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/shared/containers/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | export default class NotFound extends Component { 5 | 6 | static fetchData() {} 7 | 8 | render() { 9 | return ( 10 |
11 | 12 |

Not Found

13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/api/services/post/hooks.before.js: -------------------------------------------------------------------------------- 1 | import { hooks as auth } from 'feathers-authentication'; 2 | import { setUUID } from '@/api/hooks/setUUID'; 3 | 4 | /** 5 | Hook: before 6 | Service: post 7 | */ 8 | export default { 9 | all: [ 10 | auth.authenticate(['jwt', 'local']), 11 | ], 12 | find: [], 13 | get: [], 14 | create: [ 15 | setUUID(), 16 | ], 17 | update: [], 18 | patch: [], 19 | remove: [], 20 | }; 21 | -------------------------------------------------------------------------------- /src/shared/stores/ui/SnackBar.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | export default class SnackBar { 4 | 5 | @observable isOpen = false; 6 | @observable duration = 3000; 7 | @observable message = ''; 8 | 9 | @action 10 | open(message) { 11 | this.message = message; 12 | this.isOpen = true; 13 | } 14 | 15 | @action 16 | close() { 17 | this.message = ''; 18 | this.isOpen = false; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/styles/_.modal.js: -------------------------------------------------------------------------------- 1 | /* 2 | react-modal global style 3 | */ 4 | export default { 5 | overlay: { 6 | backgroundColor: 'rgba(255, 255, 255, 0.75)', 7 | }, 8 | content: { 9 | backgroundColor: '#1C2541', 10 | border: 0, 11 | padding: 0, 12 | maxWidth: '450px', 13 | maxHeight: '350px', 14 | marginTop: 'auto', 15 | marginBottom: 'auto', 16 | marginLeft: 'auto', 17 | marginRight: 'auto', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/api/hooks/setUUID.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid'; 2 | 3 | function assignUUID(item) { 4 | item.uuid = uuid.v4(); // eslint-disable-line no-param-reassign 5 | } 6 | 7 | export function setUUID() { 8 | return (hook, next) => { 9 | const data = hook.data; 10 | 11 | if (Array.isArray(data)) { 12 | data.map(item => assignUUID(item)); 13 | return next(); 14 | } 15 | 16 | assignUUID(data); 17 | return next(); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/shared/styles/_.material.js: -------------------------------------------------------------------------------- 1 | import { lightWhite } from 'material-ui/styles/colors'; 2 | 3 | /* 4 | material-ui override styles 5 | */ 6 | export default { 7 | palette: { 8 | primary1Color: '#5BC0BE', 9 | textColor: '#f0f0f0', 10 | }, 11 | textField: { 12 | errorColor: '#F25F5C', 13 | }, 14 | overlay: { 15 | backgroundColor: lightWhite, 16 | }, 17 | drawer: { 18 | color: '#1C2541', // this is the background-color 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/api/services/post/model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | 3 | const PostSchema = new Schema( 4 | { 5 | uuid: { type: String, required: true, unique: true }, 6 | title: { type: String, required: true }, 7 | completed: { type: Boolean, default: false }, 8 | }, 9 | { 10 | timestamps: true, // Will automatically create and update updatedAt and createdAt Fields 11 | }); 12 | 13 | export default mongoose.model('post', PostSchema); 14 | -------------------------------------------------------------------------------- /src/seeds/factories/post.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import faker from 'faker'; 3 | import { service } from '@/shared/app'; 4 | 5 | const items = []; 6 | 7 | export function factory() { 8 | return { 9 | title: faker.name.title(), 10 | completed: faker.random.boolean(), 11 | }; 12 | } 13 | 14 | function pushData() { 15 | items.push(factory()); 16 | } 17 | 18 | export function postSeederDevelopment(n = 15) { 19 | _.times(n, pushData); 20 | return service('post').create(items); 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/forms/_.bindings.js: -------------------------------------------------------------------------------- 1 | /** 2 | Fields Bindings 3 | https://foxhound87.github.io/mobx-react-form/docs/bindings/ 4 | */ 5 | 6 | export default { 7 | 8 | MaterialTextField: { 9 | id: 'id', 10 | name: 'name', 11 | type: 'type', 12 | value: 'value', 13 | label: 'floatingLabelText', 14 | placeholder: 'hintText', 15 | disabled: 'disabled', 16 | error: 'errorText', 17 | onChange: 'onChange', 18 | onFocus: 'onFocus', 19 | onBlur: 'onBlur', 20 | }, 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /config/vendor.js: -------------------------------------------------------------------------------- 1 | /** 2 | Vendor for webpack code splitting 3 | */ 4 | 5 | export default [ 6 | 'animate.css', 7 | 'bluebird', 8 | 'classnames', 9 | 'lodash', 10 | 'mobx', 11 | 'mobx-react', 12 | 'mobx-react-form', 13 | 'mobx-react-matchmedia', 14 | 'material-ui', 15 | 'react', 16 | 'react-dom', 17 | 'react-modal', 18 | 'react-pagify', 19 | 'react-parallax', 20 | 'react-router', 21 | 'react-timeago', 22 | 'rfx-core', 23 | 'validatorjs', 24 | 'tachyons', 25 | 'socket.io-client', 26 | ]; 27 | -------------------------------------------------------------------------------- /src/api/services/user/model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | export default mongoose.model('user', 6 | new Schema({ 7 | uuid: { type: String, required: true, unique: true }, 8 | email: { type: String, required: true, unique: true }, 9 | username: { type: String, required: true, unique: true }, 10 | password: { type: String, required: true }, 11 | createdAt: { type: Date, default: Date.now }, 12 | updatedAt: { type: Date, default: Date.now }, 13 | })); 14 | -------------------------------------------------------------------------------- /src/electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron", 3 | "version": "1.0.0", 4 | "description": "Electron App", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "electron ." 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "electron-debug": "^1.0.0", 15 | "electron-packager": "^7.0.3", 16 | "electron-prebuilt": "^1.2.2", 17 | "electron-rebuild": "^1.1.5" 18 | }, 19 | "dependencies": {} 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb"], 4 | "globals": { 5 | "fetch": true, 6 | "navigator": true, 7 | "document": true, 8 | "window": true 9 | }, 10 | "rules": { 11 | "react/forbid-prop-types": 0, 12 | "react/require-default-props": 0, 13 | "import/extensions": 0, 14 | "import/prefer-default-export": 0, 15 | "import/no-unresolved": [2, { "ignore": ["^[~]", "^[@]"] }], 16 | "class-methods-use-this": 0, 17 | "no-useless-computed-key": 0, 18 | "key-spacing": [0, { "align": "value" }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config/postcss.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | import postcssImport from 'postcss-import'; 3 | import postcssExtend from 'postcss-extend'; 4 | import postcssFocus from 'postcss-focus'; 5 | import postcssUrl from 'postcss-url'; 6 | import autoprefixer from 'autoprefixer'; 7 | import precss from 'precss'; 8 | import cssnano from 'cssnano'; 9 | 10 | export default bundler => [ 11 | postcssImport({ addDependencyTo: bundler }), 12 | postcssUrl('inline'), 13 | postcssExtend(), 14 | postcssFocus(), 15 | autoprefixer(), 16 | precss(), 17 | cssnano(), 18 | ]; 19 | -------------------------------------------------------------------------------- /src/seeds/factories/user.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { service } from '@/shared/app'; 3 | 4 | export function factory() { 5 | return { 6 | email: faker.internet.email(), 7 | username: faker.internet.userName(), 8 | password: faker.internet.password(), 9 | }; 10 | } 11 | 12 | export function userSeederDevelopment() { 13 | return service('user').create({ 14 | email: 'admin@test.tld', 15 | username: 'admin', 16 | password: '12345', 17 | }); 18 | } 19 | 20 | export function userSeederTesting() { 21 | return service('user').create(factory()); 22 | } 23 | -------------------------------------------------------------------------------- /config/feathers/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "db": { 4 | "host": "DB_HOST", 5 | "name": "DB_NAME", 6 | "port": "DB_PORT" 7 | }, 8 | "web": { 9 | "host": "WEB_HOST", 10 | "port": "WEB_PORT" 11 | }, 12 | "api": { 13 | "host": "API_HOST", 14 | "port": "API_PORT" 15 | } 16 | }, 17 | "auth": { 18 | "path": "/authentication", 19 | "header": "Authorization", 20 | "entity": "user", 21 | "service": "user", 22 | "secret": "JNoSF4uRkGB1zhccVkpX3ulB/1KErbj/tuhCTY0dkpVNyYTU8bJiNC4ErCUK4sDSe/YMjuC0kqtlYnWuK8tHsQ==" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/components/form/AuthLogin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import TextField from './inputs/MaterialTextField'; 5 | import FormControls from './controls/FormControls'; 6 | 7 | export default observer(({ form }) => ( 8 |
9 | 10 | 11 | 16 |

{form.error}

17 | 18 | )); 19 | -------------------------------------------------------------------------------- /src/shared/stores.js: -------------------------------------------------------------------------------- 1 | import { store } from 'rfx-core'; 2 | import { useStrict } from 'mobx'; 3 | 4 | import UIStore from './stores/ui'; 5 | import AppStore from './stores/app'; 6 | import AuthStore from './stores/auth'; 7 | import PostStore from './stores/post'; 8 | 9 | /** 10 | Enables MobX strict mode globally. 11 | In strict mode, it is not allowed to 12 | change any state outside of an action 13 | */ 14 | useStrict(true); 15 | 16 | /** 17 | Stores 18 | */ 19 | export default store 20 | .setup({ 21 | ui: UIStore, 22 | app: AppStore, 23 | auth: AuthStore, 24 | post: PostStore, 25 | }); 26 | -------------------------------------------------------------------------------- /src/api/middleware/logger.js: -------------------------------------------------------------------------------- 1 | import { log } from '@/utils/logger'; 2 | 3 | export default function (app) { 4 | // Add a logger to our app object for convenience 5 | app.logger = log; // eslint-disable-line no-param-reassign 6 | 7 | return (err, req, res, next) => { 8 | if (err) { 9 | const { url } = req; 10 | const { code, message } = err; 11 | const msg = `${code ? `(${code})` : ''} Route: ${url} - ${message}`; 12 | 13 | if (err.code === 404) { 14 | log.info(msg); 15 | } else { 16 | log.error(msg); 17 | log.info(err.stack); 18 | } 19 | } 20 | next(err); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/stores/ui/Auth.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | export default class Auth { 4 | 5 | @observable modalIsOpen = false; 6 | 7 | @observable showSection = 'signin'; 8 | 9 | @action toggleModal(flag = null, section = null) { 10 | if (!flag) this.modalIsOpen = !this.modalIsOpen; 11 | if (flag === 'open') this.modalIsOpen = true; 12 | if (flag === 'close') this.modalIsOpen = false; 13 | if (section) this.toggleSection(section); 14 | } 15 | 16 | @action toggleSection(to = 'signin') { 17 | if (to === 'signin') this.showSection = 'signin'; 18 | if (to === 'signup') this.showSection = 'signup'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/styles/Home.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #6FFFE9; 3 | font-weight: 100; 4 | } 5 | 6 | .subTitle { 7 | color: #5BC0BE; 8 | font-weight: 200; 9 | } 10 | 11 | .xsTitle { 12 | margin: 100px auto 15px auto; 13 | font-size: 80px; 14 | } 15 | 16 | .xsSubTitle { 17 | margin: 0 auto 50px auto; 18 | font-size: 15px; 19 | } 20 | 21 | .suTitle { 22 | margin: 250px auto 15px auto; 23 | font-size: 130px; 24 | } 25 | 26 | .suSubTitle { 27 | margin: 0 auto 250px auto; 28 | font-size: 25px; 29 | } 30 | 31 | .features { 32 | color: #5BC0BE; 33 | background: #0B132B; 34 | padding: 50px 0; 35 | font-size: 20px; 36 | } 37 | 38 | .features i { 39 | font-size: 55px; 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/electron/index.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | /* eslint import/no-extraneous-dependencies: 0 */ 3 | const electron = require('electron'); 4 | 5 | const app = electron.app; // Module to control application life. 6 | const BrowserWindow = electron.BrowserWindow; // Module to create native browser window. 7 | let mainWindow = null; 8 | 9 | app.on('window-all-closed', () => { 10 | if (process.platform !== 'darwin') { 11 | app.quit(); 12 | } 13 | }); 14 | 15 | app.on('ready', () => { 16 | mainWindow = new BrowserWindow({ width: 1100, height: 700 }); 17 | mainWindow.loadURL(`file://${__dirname}/index.html`); 18 | 19 | mainWindow.on('closed', () => { 20 | mainWindow = null; 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/web/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Router } from 'react-router'; 3 | import { Provider } from 'mobx-react'; 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 5 | 6 | export default class App extends Component { 7 | 8 | static propTypes = { 9 | store: React.PropTypes.object, 10 | routerProps: React.PropTypes.object, 11 | }; 12 | 13 | static fetchData() {} 14 | 15 | render() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/web/middleware/hot.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | import webpackHotMiddleware from 'webpack-hot-middleware'; 3 | import webpackDevMiddleware from 'webpack-dev-middleware'; 4 | import webpack from 'webpack'; 5 | import isDev from 'isdev'; 6 | import _ from 'lodash'; 7 | 8 | export function hotMiddleware({ wpc, wdmc, whmc }) { 9 | const bundler = webpack(wpc); 10 | 11 | return isDev ? [ 12 | 13 | webpackDevMiddleware(bundler, _.merge(wdmc, { 14 | filename: wpc.output.filename, 15 | publicPath: wpc.output.publicPath, 16 | })), 17 | 18 | webpackHotMiddleware(bundler, _.merge(whmc, { 19 | log: () => {}, 20 | })), 21 | 22 | ] : (req, res, next) => next(); 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/components/form/AuthRegister.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import TextField from './inputs/MaterialTextField'; 5 | import FormControls from './controls/FormControls'; 6 | 7 | export default observer(({ form }) => ( 8 |
9 | 10 | 11 | 12 | 13 | 18 |

{form.error}

19 | 20 | )); 21 | -------------------------------------------------------------------------------- /src/utils/jwt.js: -------------------------------------------------------------------------------- 1 | import decodeJWT from 'jwt-decode'; 2 | 3 | const payloadIsValid = payload => 4 | payload && payload.exp * 1000 > new Date().getTime(); 5 | 6 | export const verifyJWT = (token) => { 7 | if (typeof token !== 'string') { 8 | return Promise.reject(new Error('Token provided to verifyJWT is missing or not a string')); 9 | } 10 | 11 | try { 12 | const payload = decodeJWT(token); 13 | 14 | if (payloadIsValid(payload)) { 15 | // return both payload and token for better promise handling 16 | return Promise.resolve({ payload, token }); 17 | } 18 | 19 | return Promise.reject(new Error('Invalid token: expired')); 20 | } catch (error) { 21 | return Promise.reject(new Error('Cannot decode malformed token.')); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/shared/app.js: -------------------------------------------------------------------------------- 1 | import feathers from 'feathers/client'; 2 | import hooks from 'feathers-hooks'; 3 | import auth from 'feathers-authentication-client'; 4 | import socket from 'feathers-socketio/client'; 5 | import io from 'socket.io-client'; 6 | 7 | let instance = false; 8 | const config = global.CONFIG; 9 | const storage = (global.TYPE === 'CLIENT') ? window.localStorage : null; 10 | const uri = ['http://', config.io.host, ':', config.io.port].join(''); 11 | 12 | export function app() { 13 | if (instance) return instance; 14 | 15 | instance = feathers() 16 | .configure(socket(io(uri))) 17 | .configure(hooks()) 18 | .configure(auth({ storage })); 19 | 20 | return instance; 21 | } 22 | 23 | export function service(name) { 24 | return app().service(name); 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/components/AppNav.jsx: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/no-static-element-interactions: 0 */ 2 | import React from 'react'; 3 | import { observer } from 'mobx-react'; 4 | import { dispatch } from 'rfx-core'; 5 | import cx from 'classnames'; 6 | 7 | // styles 8 | import styles from '@/shared/styles/AppNav.css'; 9 | 10 | // components 11 | import Drawer from 'material-ui/Drawer'; 12 | 13 | const handleOnRequestChange = (open) => { 14 | dispatch('ui.appNav.open', open); 15 | }; 16 | 17 | const handleOnClick = () => { 18 | dispatch('ui.appNav.open', false); 19 | }; 20 | 21 | export default observer(({ children, open, docked }) => ( 22 | 28 |
{children}
29 |
30 | )); 31 | -------------------------------------------------------------------------------- /webpack/config.client.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import getenv from 'getenv'; 3 | import env from '~/config/expose'; 4 | import postcss from '~/config/postcss'; 5 | 6 | export function load() { 7 | return { 8 | target: 'web', 9 | plugins: [ 10 | new webpack.DefinePlugin({ 11 | 'global.DIR': JSON.stringify(global.DIR), 12 | 'global.CONFIG': JSON.stringify(getenv.multi(env)), 13 | 'global.TYPE': JSON.stringify('CLIENT'), 14 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 15 | }), 16 | new webpack.ProvidePlugin({ 17 | Promise: 'bluebird', 18 | }), 19 | new webpack.LoaderOptionsPlugin({ 20 | minimize: false, 21 | debug: true, 22 | options: { 23 | postcss: postcss(webpack), 24 | }, 25 | }), 26 | ], 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/components/MenuLinksSX.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import cx from 'classnames'; 4 | 5 | // components 6 | import { Link } from 'react-router'; 7 | 8 | // stules 9 | const a = cx('db', 'ph3', 'pv3', 'fw4'); 10 | const listBlock = cx('list', 'pl0', 'ml0'); 11 | const listInline = cx('list', 'pa0', 'mv0'); 12 | const liBlock = cx('db'); 13 | const liInline = cx('dib'); 14 | 15 | export default observer(({ inline }) => ( 16 |
    17 |
  • Home
  • 18 |
  • Messages Demo
  • 19 |
  • Packages
  • 20 |
21 | )); 22 | -------------------------------------------------------------------------------- /src/shared/components/PostSearch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { dispatch } from 'rfx-core'; 4 | import $ from '@/shared/styles/_.mixins'; 5 | 6 | const handleSearch = (e) => { 7 | e.preventDefault(); 8 | const val = e.target.value; 9 | dispatch('post.search', val); 10 | }; 11 | 12 | const resetSearch = (e) => { 13 | e.preventDefault(); 14 | dispatch('post.search', null); 15 | }; 16 | 17 | export default observer(({ search }) => ( 18 |
19 | 26 | 30 | 31 | )); 32 | -------------------------------------------------------------------------------- /src/shared/components/AuthModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { dispatch } from 'rfx-core'; 4 | 5 | import _ from 'lodash'; 6 | import modalBaseStyle from '@/shared/styles/_.modal.js'; 7 | 8 | import Modal from 'react-modal'; 9 | import AuthForm from './AuthForm'; 10 | 11 | const styles = _.cloneDeep(modalBaseStyle); 12 | 13 | _.assign(styles.content, { 14 | maxWidth: '450px', 15 | maxHeight: '500px', 16 | }); 17 | 18 | const handleCloseModal = () => 19 | dispatch('ui.auth.toggleModal', 'close'); 20 | 21 | export default observer(({ open, showSection, forms }) => ( 22 | 28 | 32 | 33 | )); 34 | -------------------------------------------------------------------------------- /src/web/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%-head.title%> 8 | <%-head.meta%> 9 | <%-head.link%> 10 | <% if (build) { %> 11 | 12 | <% } %> 13 | 16 | 17 | 18 |
<%-root%>
19 | <% if (build) { %> 20 | 21 | 22 | <% } else { %> 23 | 24 | <% } %> 25 | 26 | 27 | -------------------------------------------------------------------------------- /webpack/config.server.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import getenv from 'getenv'; 3 | import env from '~/config/expose'; 4 | import postcss from '~/config/postcss'; 5 | 6 | export function load() { 7 | return { 8 | target: 'async-node', 9 | node: { 10 | __filename: true, 11 | __dirname: true, 12 | }, 13 | plugins: [ 14 | new webpack.ProvidePlugin({ 15 | Promise: 'bluebird', 16 | }), 17 | new webpack.DefinePlugin({ 18 | 'global.CONFIG': JSON.stringify(getenv.multi(env)), 19 | 'global.TYPE': JSON.stringify('SERVER'), 20 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 21 | }), 22 | new webpack.LoaderOptionsPlugin({ 23 | minimize: true, 24 | debug: false, 25 | options: { 26 | postcss: postcss(webpack), 27 | }, 28 | }), 29 | ], 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/shared/styles/_.global.css: -------------------------------------------------------------------------------- 1 | @import 'font-awesome'; 2 | @import 'animate.css'; 3 | @import 'normalize.css'; 4 | @import "tachyons"; 5 | @import "./_.custom.css"; 6 | 7 | :root { 8 | --button-color: #1C2541; 9 | --button-background-color: #5BC0BE; 10 | } 11 | 12 | 13 | body { 14 | color: #5BC0BE; 15 | background-color: #1C2541; 16 | } 17 | 18 | a { 19 | color: #5BC0BE; 20 | text-decoration: none; 21 | transition: all 0.2s; 22 | } 23 | 24 | a:hover, 25 | a:focus { 26 | border-color: rgba(0,0,0,0) !important; 27 | box-shadow: none !important; 28 | text-decoration: none; 29 | color: #6FFFE9; 30 | } 31 | 32 | a, button { 33 | outline: 0; 34 | } 35 | 36 | h1, h3, h4 { 37 | color: #FFD98E; 38 | } 39 | 40 | img { 41 | max-width: 100%; 42 | } 43 | 44 | input, textarea { 45 | font-size: 1rem; 46 | } 47 | 48 | input::-ms-clear, textarea::-ms-clear { 49 | display: none; 50 | } 51 | -------------------------------------------------------------------------------- /src/shared/forms/auth.js: -------------------------------------------------------------------------------- 1 | import { dispatch } from 'rfx-core'; 2 | import Form from './_.extend'; 3 | 4 | class AuthForm extends Form { 5 | 6 | onSuccess(form) { 7 | return dispatch('auth.login', form.values()) 8 | .then(() => dispatch('ui.auth.toggleModal', 'close')) 9 | .then(() => dispatch('ui.snackBar.open', 'Login Successful.')) 10 | .then(() => form.clear()) 11 | .catch((err) => { 12 | form.invalidate(err.message); 13 | dispatch('ui.snackBar.open', err.message); 14 | }); 15 | } 16 | } 17 | 18 | export default 19 | new AuthForm({ 20 | fields: { 21 | email: { 22 | label: 'Email', 23 | placeholder: 'Insert Email', 24 | rules: 'required|email|string|between:5,50', 25 | }, 26 | password: { 27 | label: 'Password', 28 | placeholder: 'Insert Password', 29 | rules: 'required|between:5,20', 30 | }, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/shared/styles/MenuLinkDX.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | color: #3A506B; 3 | } 4 | 5 | .loginBtn { 6 | border: 1px solid #5BC0BE; 7 | color: #5BC0BE; 8 | } 9 | 10 | .registerBtn { 11 | border: 1px solid #FFD98E; 12 | background: #FFD98E; 13 | color: #1C2541; 14 | } 15 | 16 | .loginBtn:hover, 17 | .registerBtn:hover { 18 | background: #6FFFE9; 19 | color: #1C2541; 20 | } 21 | 22 | .menuAccount ul { 23 | /* min-width:128px; */ 24 | color: #1C2541; 25 | background: #5BC0BE; 26 | } 27 | 28 | .menuAccount ul li a { 29 | color: #1C2541; 30 | } 31 | 32 | .menuAccount ul li a:hover { 33 | background: #6FFFE9; 34 | } 35 | 36 | .menuAccount ul li:first-child, 37 | .menuAccount ul li:first-child a { 38 | border-top-left-radius: 3px; 39 | border-top-right-radius: 3px; 40 | } 41 | 42 | .menuAccount ul li:last-child, 43 | .menuAccount ul li:last-child a { 44 | border-bottom-left-radius: 3px; 45 | border-bottom-right-radius: 3px; 46 | } 47 | -------------------------------------------------------------------------------- /src/web/middleware/routing.js: -------------------------------------------------------------------------------- 1 | import { match } from 'react-router'; 2 | 3 | function handleRouter(req, res, props, ssr) { 4 | console.log('route:', req.url); // eslint-disable-line no-console 5 | if (req.url !== '/favicon.ico') ssr(req, res, props); 6 | } 7 | 8 | function handleRedirect(res, redirect) { 9 | res.redirect(302, redirect.pathname + redirect.search); 10 | } 11 | 12 | function handleNotFound(res) { 13 | res.status(404).send('Not Found'); 14 | } 15 | 16 | function handleError(res, err) { 17 | res.status(500).send(err.message); 18 | } 19 | 20 | export function routingMiddleware(routes, ssr) { 21 | return (req, res) => { 22 | match({ routes, location: req.url }, 23 | (err, redirect, props) => { 24 | if (err) handleError(res, err); 25 | else if (redirect) handleRedirect(res, redirect); 26 | else if (props) handleRouter(req, res, props, ssr); 27 | else handleNotFound(res); 28 | }); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/shared/forms/_.extend.js: -------------------------------------------------------------------------------- 1 | import MobxReactForm from 'mobx-react-form'; 2 | import validatorjs from 'validatorjs'; 3 | import bindings from './_.bindings'; 4 | 5 | /** 6 | What can I do with mobx-react-form ? 7 | 8 | API: https://foxhound87.github.io/mobx-react-form/docs/api-reference/ 9 | FIELDS: https://foxhound87.github.io/mobx-react-form/docs/defining-fields.html 10 | ACTIONS: https://foxhound87.github.io/mobx-react-form/docs/actions/ 11 | EVENTS: https://foxhound87.github.io/mobx-react-form/docs/events/ 12 | VALIDATION: https://foxhound87.github.io/mobx-react-form/docs/validation/ 13 | BINDINGS: https://foxhound87.github.io/mobx-react-form/docs/bindings/ 14 | */ 15 | 16 | export default class Form extends MobxReactForm { 17 | 18 | plugins() { 19 | return { 20 | dvr: validatorjs, 21 | }; 22 | } 23 | 24 | bindings() { 25 | return bindings; 26 | } 27 | 28 | onInit() { 29 | this.each(field => 30 | field.set('bindings', 'MaterialTextField')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/components/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import Paginator from 'react-pagify'; 4 | import $ from '@/shared/styles/_.mixins'; 5 | 6 | export default observer(({ currentPage, onPageChange }) => ( 7 | 12 |
13 | 14 | 18 | 19 | 20 | 24 | 25 |
26 |
27 | )); 28 | -------------------------------------------------------------------------------- /src/shared/styles/_.custom.css: -------------------------------------------------------------------------------- 1 | 2 | .divider { 3 | color: #3A506B; 4 | } 5 | 6 | /* TEXT COLORS */ 7 | 8 | ._c1 { 9 | color: #5BC0BE; 10 | } 11 | 12 | ._c2 { 13 | color: #6FFFE9; 14 | } 15 | 16 | ._c3 { 17 | color: #FFD98E; 18 | } 19 | 20 | ._c4 { 21 | color: #1C2541; 22 | } 23 | 24 | /* BORDER COLORS */ 25 | 26 | ._b1 { 27 | border-color: #5BC0BE; 28 | } 29 | 30 | ._b2 { 31 | border-color: #6FFFE9; 32 | } 33 | 34 | ._b3 { 35 | border-color: #FFD98E; 36 | } 37 | 38 | /* BACKGROUND COLORS */ 39 | 40 | ._bg1 { 41 | background-color: #5BC0BE; 42 | color: #1C2541; 43 | } 44 | 45 | ._bg2 { 46 | background-color: #6FFFE9; 47 | color: #1C2541; 48 | } 49 | 50 | ._bg3 { 51 | background-color: #FFD98E; 52 | color: #1C2541; 53 | } 54 | 55 | /* HOVER COLORS */ 56 | 57 | ._c1:hover { 58 | color: #6FFFE9; 59 | } 60 | 61 | ._c2:hover { 62 | color: #6FFFE9; 63 | } 64 | 65 | ._b1:hover { 66 | border-color: #6FFFE9; 67 | } 68 | 69 | ._b2:hover { 70 | border-color: #6FFFE9; 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/api/auth.js: -------------------------------------------------------------------------------- 1 | import auth from 'feathers-authentication'; 2 | import { setupJWTPayload } from '@/api/hooks/setupJWTPayload'; 3 | 4 | // import { Strategy as FacebookStrategy } from 'passport-facebook'; 5 | // import FacebookTokenStrategy from 'passport-facebook-token'; 6 | // import { Strategy as GithubStrategy } from 'passport-github'; 7 | // import GithubTokenStrategy from 'passport-github-token'; 8 | 9 | export default function () { 10 | const app = this; 11 | 12 | const config = app.get('auth'); 13 | 14 | // config.facebook.strategy = FacebookStrategy; 15 | // config.facebook.tokenStrategy = FacebookTokenStrategy; 16 | // config.github.strategy = GithubStrategy; 17 | // config.github.tokenStrategy = GithubTokenStrategy; 18 | 19 | app.set('auth', config); 20 | app.configure(auth(config)); 21 | 22 | app.service('authentication').hooks({ 23 | before: { 24 | create: [ 25 | auth.hooks.authenticate(['jwt', 'local']), 26 | setupJWTPayload(app), 27 | ], 28 | }, 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/shared/containers/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import Helmet from 'react-helmet'; 4 | 5 | import AuthForm from '@/shared/components/AuthForm'; 6 | 7 | // forms 8 | import authForm from '@/shared/forms/auth'; 9 | import userForm from '@/shared/forms/user'; 10 | 11 | @inject('store') @observer 12 | export default class Auth extends Component { 13 | 14 | static fetchData() {} 15 | 16 | static propTypes = { 17 | store: React.PropTypes.object, 18 | }; 19 | 20 | render() { 21 | const { ui } = this.props.store; 22 | 23 | return ( 24 |
25 | 26 |

Not Authorized

27 |

You are not authorized to access.

28 | 35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/shared/components/PostListHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import cx from 'classnames'; 4 | import { dispatch } from 'rfx-core'; 5 | import $ from '@/shared/styles/_.mixins'; 6 | 7 | const handleAddRandomPost = (e) => { 8 | e.preventDefault(); 9 | dispatch('post.create'); 10 | }; 11 | 12 | const handleCreatePost = (e) => { 13 | e.preventDefault(); 14 | dispatch('ui.postCreateModal.open', true); 15 | }; 16 | 17 | export default observer(() => ( 18 |
19 | 27 | 35 |
36 | )); 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.8.0 (alpha.8) 2 | 3 | * Code Splitting 4 | * Introduced mobx-react Provide 5 | * Introduced tachyons 6 | * Introduced rfx-core 7 | * Updated Dependencies 8 | 9 | # 0.7.0 (alpha.7) 10 | 11 | * Added Electron App 12 | 13 | # 0.6.0 (alpha.6) 14 | 15 | * Form management with mobx-react-form 16 | * Updated Graphic 17 | 18 | # 0.5.0 (alpha.5) 19 | 20 | * Updated to react-hot-loader 3 beta 21 | * Hot-Reloadable MobX Stores 22 | * Improved directory structure and scripts 23 | * Better documentation 24 | 25 | # 0.4.0 (alpha.4) 26 | 27 | * Enabled MobX strict mode 28 | 29 | # 0.3.0 (alpha.3) 30 | 31 | * React Stateless Components implementation 32 | * Action Dispatcher for Stateless React Components 33 | * Helmet w/ server side rendering 34 | * Updated NPM Dependencies 35 | 36 | # 0.2.0 (alpha.2) 37 | 38 | * Browser Sync Integration 39 | * Reactive Media Queries (MatchMedia + MobX) 40 | 41 | # 0.1.0 (alpha.1) 42 | 43 | * Server Side Rendering 44 | * React Transform HMR 45 | * Isomorphic Fetch/Socket 46 | * Modular CSS for React 47 | -------------------------------------------------------------------------------- /src/api/services/user/hooks.before.js: -------------------------------------------------------------------------------- 1 | import { hooks as auth } from 'feathers-authentication'; 2 | import { hooks as local } from 'feathers-authentication-local'; 3 | import { hooks as perms } from 'feathers-permissions'; 4 | import { setUUID } from '@/api/hooks/setUUID'; 5 | 6 | /** 7 | Hook: before 8 | Service: user 9 | */ 10 | export default { 11 | all: [], 12 | find: [ 13 | auth.authenticate(['jwt', 'local']), 14 | perms.checkPermissions({ service: 'user' }), 15 | ], 16 | get: [ 17 | auth.authenticate(['jwt', 'local']), 18 | perms.checkPermissions({ service: 'user' }), 19 | ], 20 | create: [ 21 | setUUID(), 22 | local.hashPassword(), 23 | ], 24 | update: [ 25 | auth.authenticate(['jwt', 'local']), 26 | perms.checkPermissions({ service: 'user' }), 27 | ], 28 | patch: [ 29 | auth.authenticate(['jwt', 'local']), 30 | perms.checkPermissions({ service: 'user' }), 31 | ], 32 | remove: [ 33 | auth.authenticate(['jwt', 'local']), 34 | perms.checkPermissions({ service: 'user' }), 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /webpack.config.client.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | /* eslint import/first: 0 */ 3 | /* eslint import/newline-after-import: 0 */ 4 | /* eslint import/no-extraneous-dependencies: 0 */ 5 | import './run/global'; 6 | import merge from 'webpack-merge'; 7 | import Globals from './webpack/globals'; 8 | import getLoaders from './webpack/loaders'; 9 | 10 | let Config; 11 | let Loader = getLoaders(); 12 | 13 | Config = require('./webpack/config.client').load(); 14 | const ConfigClientDev = require('./webpack/config.client.dev'); 15 | Loader = merge(Loader, ConfigClientDev.loader()); 16 | Config = merge(Config, ConfigClientDev.config('web')); 17 | 18 | // Globals 19 | Config = merge(Config, Globals); 20 | 21 | // Loaders 22 | Config = merge(Config, { 23 | module: { 24 | loaders: [ 25 | Loader.eslint, 26 | Loader.jsx, 27 | Loader.json, 28 | Loader.url, 29 | Loader.file, 30 | Loader.cssGlobal, 31 | Loader.cssModules, 32 | ], 33 | }, 34 | }); 35 | 36 | const WebpackConfig = Config; 37 | export default WebpackConfig; 38 | -------------------------------------------------------------------------------- /src/shared/forms/post.js: -------------------------------------------------------------------------------- 1 | import { dispatch } from 'rfx-core'; 2 | import Form from './_.extend'; 3 | 4 | export class PostForm extends Form { 5 | onSuccess(form) { 6 | const storeAction = form.values().uuid ? 'post.update' : 'post.create'; 7 | 8 | return dispatch(storeAction, form.values()) 9 | .then(() => dispatch('ui.postCreateModal.open', false)) 10 | .then(() => dispatch('ui.snackBar.open', 'Post Saved.')) 11 | .then(() => form.clear()) 12 | .catch((err) => { 13 | form.invalidate(err.message); 14 | dispatch('ui.snackBar.open', err.message); 15 | }); 16 | } 17 | } 18 | 19 | export const fields = { 20 | title: { 21 | label: 'Title', 22 | rules: 'required|string|between:5,50', 23 | }, 24 | completed: { 25 | label: 'Completed', 26 | value: true, 27 | rules: 'boolean', 28 | }, 29 | uuid: { 30 | rules: 'string', 31 | value: null, 32 | }, 33 | }; 34 | 35 | export function init(values = {}) { 36 | return new PostForm({ fields, values }); 37 | } 38 | 39 | export default new PostForm({ fields }); 40 | -------------------------------------------------------------------------------- /src/shared/components/PostFilter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { dispatch } from 'rfx-core'; 4 | import cx from 'classnames'; 5 | import $ from '@/shared/styles/_.mixins'; 6 | 7 | const handleSelect = (e) => { 8 | e.preventDefault(); 9 | const val = e.target.value; 10 | dispatch('post.filterBy', val); 11 | }; 12 | 13 | export default observer(({ filter }) => ( 14 |
15 | 22 | 29 | 36 |
37 | )); 38 | -------------------------------------------------------------------------------- /src/shared/components/PostDetailsHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import { observer } from 'mobx-react'; 4 | import { dispatch } from 'rfx-core'; 5 | import $ from '@/shared/styles/_.mixins'; 6 | 7 | const handleEditPost = (e) => { 8 | e.preventDefault(); 9 | dispatch('ui.postCreateModal.open', true); 10 | }; 11 | 12 | export default observer(({ post }) => ( 13 |
14 |

{post.name || 'Post Details'}

15 | 16 |
17 | 25 |
26 | 27 |
28 | 36 |
37 |
38 | )); 39 | -------------------------------------------------------------------------------- /src/utils/seeder.runner.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | /* eslint import/no-dynamic-require: 0 */ 3 | 4 | import path from 'path'; 5 | import { log } from './logger'; 6 | 7 | function logStart() { 8 | log.info('--- Seeding... ---------------------------'); 9 | log.info('------------------------------------------'); 10 | } 11 | 12 | function logFinish() { 13 | log.info('--- Seed Finish --------------------------'); 14 | log.info('------------------------------------------'); 15 | process.exit(); 16 | } 17 | 18 | function catchError(err) { 19 | log.info('--- Seed Error ---------------------------'); 20 | log.info('------------------------------------------'); 21 | console.error(err); // eslint-disable-line 22 | process.exit(); 23 | } 24 | 25 | export default ($path) => { 26 | const handlerFile = path.resolve($path, 'handlers', process.env.NODE_ENV); 27 | 28 | let handler = require(handlerFile).handle(); 29 | 30 | if (Array.isArray(handler)) handler = Promise.all(handler); 31 | 32 | if (!handler) catchError(); 33 | 34 | handler 35 | .then(logStart) 36 | .then(logFinish) 37 | .catch(catchError); 38 | }; 39 | -------------------------------------------------------------------------------- /webpack/loaders.js: -------------------------------------------------------------------------------- 1 | const Dir = global.DIR; 2 | 3 | export default function getLoaders() { 4 | return { 5 | eslint: { 6 | test: /\.jsx?$/, 7 | enforce: 'pre', 8 | loader: 'eslint-loader', 9 | exclude: /node_modules/, 10 | include: Dir.src, 11 | }, 12 | jsx: { 13 | test: /\.jsx?$/, 14 | loader: 'babel-loader', 15 | exclude: /(node_modules)/, 16 | }, 17 | json: { 18 | test: /\.json$/, 19 | loader: 'json-loader', 20 | }, 21 | url: { 22 | // the "?v=" regex fixes fontawesome issue 23 | test: /\.((woff2?|svg)(\?v=[0-9]\.[0-9]\.[0-9]))|(woff2?|svg|jpe?g|png|gif|ico)$/, 24 | loader: 'url-loader', 25 | }, 26 | file: { 27 | // the "?v=" regex fixes fontawesome issue 28 | test: /\.((ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9]))|(ttf|eot)$/, 29 | loader: 'url-loader', 30 | }, 31 | cssGlobal: { 32 | test: /\.global\.css$/, 33 | loader: 'style-loader!css-loader!postcss-loader', 34 | }, 35 | cssModules: { 36 | test: /^((?!\.global).)*\.css$/, 37 | /* loader: based on target script */ 38 | }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Claudio Savino 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/api/autoloader.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | import { log } from '@/utils/logger'; 3 | 4 | const Dir = global.DIR; 5 | 6 | export function autoloader($service) { 7 | const dir = $service.replace(Dir.api, '.'); // require build fix (".replace()") 8 | const ServiceConfig = require(dir + '/config.js').default; // eslint-disable-line 9 | const ServiceModel = require(dir + '/model.js').default; // eslint-disable-line 10 | 11 | // extend the service object with related model 12 | Object.assign(ServiceConfig.options, { Model: ServiceModel }); 13 | 14 | // Create an instance of the Feather service 15 | const serviceInstance = this.adapter(ServiceConfig.options); 16 | 17 | // Attach the service to the app server 18 | log.info('Service', ServiceConfig.namespace); 19 | this.app.use(ServiceConfig.namespace, serviceInstance); 20 | 21 | // get the service 22 | const service = this.app.service(ServiceConfig.namespace); 23 | 24 | // Setup our HOOKS (before/after) 25 | service.before(require(dir + '/hooks.before.js').default); // eslint-disable-line 26 | service.after(require(dir + '/hooks.after.js').default); // eslint-disable-line 27 | } 28 | -------------------------------------------------------------------------------- /src/shared/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | 4 | // Components 5 | import AppLayout from './containers/AppLayout'; 6 | import NotFound from './containers/NotFound'; 7 | 8 | function $import(location, cb, component) { 9 | return System.import('./containers/' + component) // eslint-disable-line 10 | .then(module => cb(null, module.default)) 11 | .catch(err => console.error('Dynamic page loading failed', err)); // eslint-disable-line 12 | } 13 | 14 | export default ( 15 | 16 | 17 | $import(loc, cb, 'Home')} /> 18 | 19 | $import(loc, cb, 'Auth')} /> 20 | 21 | 22 | $import(loc, cb, 'Messages')} /> 23 | $import(loc, cb, 'Message')} /> 24 | 25 | 26 | $import(loc, cb, 'Packages')} /> 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /src/utils/authorize.hoc.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | /** 5 | Require Auth HOC 6 | */ 7 | export const authorize = ComposedComponent => 8 | observer(class Auth extends Component { 9 | 10 | static propTypes = { 11 | store: React.PropTypes.object, 12 | router: React.PropTypes.object, 13 | location: React.PropTypes.object, 14 | }; 15 | 16 | static fetchData(data) { 17 | if (!data.store.auth.check) { 18 | return new Promise(resolve => resolve()); 19 | } 20 | 21 | return ComposedComponent.fetchData(data); 22 | } 23 | 24 | componentWillMount() { 25 | const { store, location, router } = this.props; 26 | 27 | if (global.TYPE === 'CLIENT') { 28 | if (!store.auth.check) { 29 | const currentPath = location.pathname; 30 | store.auth.redirect = currentPath; 31 | router.push('/auth'); 32 | } 33 | } 34 | } 35 | 36 | render() { 37 | return ( 38 | this.props.store.auth.check && 39 | 40 | ); 41 | } 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /src/shared/styles/_.mixins.js: -------------------------------------------------------------------------------- 1 | import cx from 'classnames'; 2 | 3 | const buttonBase = cx('f6', 'ba', 'ph3', 'pv2', 'mb2', 'dib', 'pointer'); 4 | 5 | const buttonGeneric = cx(buttonBase, 'br2'); 6 | 7 | const buttonPill = cx(buttonBase, 'br-pill', '_c1', '_b1', 'bg-transparent'); 8 | 9 | const buttonPillSearch = cx(buttonBase, 10 | 'fl', 'f6', 'f5-l', 'button-reset', 'pv2', 'tc', 'bn', 'bg-animate', 11 | 'pointer', 'w-25', 'w-20-l', 'br2', 'br--right', 12 | 'br--right-ns', '_c4', '_bg3', 13 | ); 14 | 15 | const buttonGroupBase = cx(buttonBase, '_b1', '_c1', 'bg-transparent'); 16 | 17 | const buttonGroupCenter = cx(buttonGroupBase); 18 | 19 | const buttonGroupLeft = cx(buttonGroupBase, 'br2', 'br--left'); 20 | 21 | const buttonGroupRight = cx(buttonGroupBase, 'br2', 'br--right'); 22 | 23 | const inputSearch = cx( 24 | 'fl', 'f6', 'f5-l', 'input-reset', 'bn', 'black-80', 'bg-white', 25 | 'fl', 'pa2', 'lh-solid', 'w-75', 'w-80-l', 'br2', 'br--left', 26 | ); 27 | 28 | export default { 29 | buttonBase, 30 | buttonGeneric, 31 | buttonPill, 32 | buttonPillSearch, 33 | buttonGroupCenter, 34 | buttonGroupLeft, 35 | buttonGroupRight, 36 | inputSearch, 37 | }; 38 | -------------------------------------------------------------------------------- /src/electron/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 |
21 | 22 | 23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/shared/components/PostListBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { dispatch } from 'rfx-core'; 4 | 5 | import PostSearch from './PostSearch'; 6 | import PostFilter from './PostFilter'; 7 | import PostInfo from './PostInfo'; 8 | import Pagination from './Pagination'; 9 | 10 | const handlePostPageChange = (page) => { 11 | dispatch('post.page', page); 12 | }; 13 | 14 | export default observer(({ post }) => ( 15 |
16 |
17 | 18 |
19 |
20 | 25 |
26 |
27 | 28 |
29 |
30 | 34 |
35 |
36 | )); 37 | 38 | -------------------------------------------------------------------------------- /src/web/client.jsx: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | import '@/shared/stores'; // initialize stores 3 | 4 | import { 5 | rehydrate, 6 | hotRehydrate, 7 | fetchDataOnLocationMatch } from 'rfx-core'; 8 | 9 | import React from 'react'; 10 | import { render } from 'react-dom'; 11 | import { hashHistory, browserHistory, match } from 'react-router'; 12 | import { AppContainer } from 'react-hot-loader'; 13 | import routes from '@/shared/routes'; 14 | import App from './App'; 15 | 16 | const store = rehydrate(); 17 | const history = global.ELECTRON ? hashHistory : browserHistory; 18 | 19 | fetchDataOnLocationMatch(history, routes, match, store); 20 | store.ui.injectTapEventPlugin(); // material-ui fix 21 | 22 | function renderApp(AppComponent) { 23 | match({ history, routes }, 24 | (error, redirect, routerProps) => 25 | render( 26 | 27 | 31 | , 32 | document.getElementById('root'), 33 | )); 34 | } 35 | 36 | renderApp(App); 37 | 38 | if (module.hot) { 39 | module.hot.accept(() => 40 | renderApp(require('./App').default)); 41 | } 42 | -------------------------------------------------------------------------------- /config/dir.js: -------------------------------------------------------------------------------- 1 | /* 2 | Project Directories 3 | */ 4 | export default path => ({ 5 | config : path.resolve(__dirname), 6 | root : path.resolve(__dirname, '..'), 7 | src : path.resolve(__dirname, '..', 'src'), 8 | run : path.resolve(__dirname, '..', 'run'), 9 | modules : path.resolve(__dirname, '..', 'node_modules'), 10 | staticBuild : path.resolve(__dirname, '..', 'public', 'build'), 11 | nodeBuild : path.resolve(__dirname, '..', 'run', 'build'), 12 | public : path.resolve(__dirname, '..', 'public'), 13 | static : path.resolve(__dirname, '..', 'public', 'static'), 14 | shared : path.resolve(__dirname, '..', 'src', 'shared'), 15 | api : path.resolve(__dirname, '..', 'src', 'api'), 16 | web : path.resolve(__dirname, '..', 'src', 'web'), 17 | views : path.resolve(__dirname, '..', 'src', 'web', 'views'), 18 | utils : path.resolve(__dirname, '..', 'src', 'utils'), 19 | hooks : path.resolve(__dirname, '..', 'src', 'api', 'hooks'), 20 | middleware : path.resolve(__dirname, '..', 'src', 'api', 'middleware'), 21 | services : path.resolve(__dirname, '..', 'src', 'api', 'services'), 22 | seeds : path.resolve(__dirname, '..', 'src', 'seeds'), 23 | }); 24 | -------------------------------------------------------------------------------- /src/shared/containers/Messages.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { inject, observer } from 'mobx-react'; 4 | import { authorize } from '@/utils/authorize.hoc'; 5 | 6 | // components 7 | import PostListHeader from '@/shared/components/PostListHeader'; 8 | import PostListBar from '@/shared/components/PostListBar'; 9 | import PostList from '@/shared/components/PostList'; 10 | import PostCreateModal from '@/shared/components/PostCreateModal'; 11 | 12 | // form 13 | import postForm from '@/shared/forms/post'; 14 | 15 | @inject('store') @authorize @observer 16 | export default class Messages extends Component { 17 | 18 | static fetchData({ store }) { 19 | return store.post.find(); 20 | } 21 | 22 | static propTypes = { 23 | store: React.PropTypes.object, 24 | }; 25 | 26 | render() { 27 | const { ui, post } = this.props.store; 28 | 29 | return ( 30 |
31 | 32 | 33 | 34 |
35 | 36 |
37 | 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/services.autoload.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import globule from 'globule'; 3 | import { log } from './logger'; 4 | 5 | class ServicesSetup { 6 | init(props) { 7 | this.dir = props.dir; 8 | this.adapter = props.adapter; 9 | this.connector = props.connector; 10 | this.autoloader = props.autoloader; 11 | } 12 | } 13 | 14 | /* 15 | Feathers Services Autoload 16 | */ 17 | class Services { 18 | 19 | constructor(app) { 20 | this.app = app; 21 | } 22 | 23 | init(props) { 24 | this.dir = path.resolve(props.dir, 'services'); 25 | this.connector = props.connector; 26 | this.adapter = props.adapter; 27 | this.db = this.connector(this.app.get('server').db); 28 | this.autoloader = props.autoloader; 29 | this.loadServices(); 30 | } 31 | 32 | loadServices() { 33 | log.info('------------------------------------------'); 34 | log.info('Loading services...'); 35 | globule 36 | .find(path.join(this.dir, '*')) 37 | .map($service => this.autoloader($service)); 38 | log.info('------------------------------------------'); 39 | } 40 | } 41 | 42 | const servicesSetup = new ServicesSetup(); 43 | 44 | export function setupServices($props) { 45 | servicesSetup.init($props); 46 | } 47 | 48 | export function initServices() { 49 | new Services(this).init(servicesSetup); 50 | } 51 | -------------------------------------------------------------------------------- /src/shared/forms/user.js: -------------------------------------------------------------------------------- 1 | import { dispatch } from 'rfx-core'; 2 | import Form from './_.extend'; 3 | 4 | class UserForm extends Form { 5 | 6 | onSuccess(form) { 7 | return dispatch('auth.register', form.values()) 8 | .then(() => dispatch('ui.auth.toggleSection', 'signin')) 9 | .then(() => dispatch('ui.snackBar.open', 'Register Successful.')) 10 | .then(() => form.clear()) 11 | .catch((err) => { 12 | form.invalidate(err.message); 13 | dispatch('ui.snackBar.open', err.message); 14 | }); 15 | } 16 | } 17 | 18 | export default 19 | new UserForm({ 20 | fields: { 21 | username: { 22 | label: 'Username', 23 | rules: 'required|string|between:5,20', 24 | placeholder: 'Insert Username', 25 | }, 26 | email: { 27 | label: 'Email', 28 | rules: 'required|email|string|between:5,50', 29 | placeholder: 'Insert Email', 30 | }, 31 | password: { 32 | label: 'Password', 33 | rules: 'required|string|between:5,20', 34 | placeholder: 'Insert Password', 35 | related: ['passwordConfirm'], 36 | }, 37 | passwordConfirm: { 38 | label: 'Confirm Password', 39 | rules: 'required|string|between:5,20|same:password', 40 | placeholder: 'Insert Confirmation Password', 41 | }, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /src/web/server.js: -------------------------------------------------------------------------------- 1 | import feathers from 'feathers'; 2 | import compression from 'compression'; 3 | import bodyParser from 'body-parser'; 4 | import cookieParser from 'cookie-parser'; 5 | import ejs from 'ejs'; 6 | 7 | import { setupServer, startServer } from '@/utils/server.start'; 8 | import { logServerConfig } from '@/utils/logger'; 9 | 10 | // webpack configs 11 | import wpc from '~/webpack.config.client.babel'; 12 | import { wdmc, whmc } from '~/config/hot'; 13 | 14 | // routes & ssr 15 | import routes from '@/shared/routes'; 16 | import ssr from './ssr'; 17 | 18 | // middlewares 19 | import { serveStaticMiddleware } from './middleware/serveStatic'; 20 | import { hotMiddleware } from './middleware/hot'; 21 | import { routingMiddleware } from './middleware/routing'; 22 | 23 | const Dir = global.DIR; 24 | 25 | setupServer({ 26 | namespace: 'web', 27 | logger: logServerConfig, 28 | }); 29 | 30 | const app = feathers(); 31 | 32 | app 33 | .use(compression()) 34 | .use(cookieParser()) 35 | .use(bodyParser.json()) 36 | .use(bodyParser.urlencoded({ extended: true })) 37 | .engine('ejs', ejs.renderFile) 38 | .set('view engine', 'ejs') 39 | .set('views', Dir.views) 40 | .configure(serveStaticMiddleware) 41 | .use(hotMiddleware({ wpc, wdmc, whmc })) 42 | .use(routingMiddleware(routes, ssr)) 43 | .configure(startServer); 44 | 45 | export default app; 46 | -------------------------------------------------------------------------------- /src/shared/containers/Message.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | 4 | // Components 5 | import Helmet from 'react-helmet'; 6 | import PostDetailsHeader from '@/shared/components/PostDetailsHeader'; 7 | import PostDetails from '@/shared/components/PostDetails'; 8 | import PostCreateModal from '@/shared/components/PostCreateModal'; 9 | import { authorize } from '@/utils/authorize.hoc'; 10 | 11 | @inject('store') @authorize @observer 12 | export default class Message extends Component { 13 | static postForm; 14 | 15 | static fetchData({ store, params }) { 16 | console.log('Fetching message data for', params.messageId); // eslint-disable-line 17 | return store.post.get(params.messageId); 18 | } 19 | 20 | static propTypes = { 21 | store: React.PropTypes.object, 22 | }; 23 | 24 | componentWillUnmount() { 25 | return this.props.store.post.clear(); 26 | } 27 | 28 | render() { 29 | const { ui, post } = this.props.store; 30 | 31 | return ( 32 |
33 | 34 | 35 |
36 | 37 |
38 | 42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/shared/components/AuthForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { dispatch } from 'rfx-core'; 4 | 5 | import cx from 'classnames'; 6 | import $ from '@/shared/styles/_.mixins'; 7 | 8 | import AuthFormLogin from './form/AuthLogin'; 9 | import AuthFormRegister from './form/AuthRegister'; 10 | 11 | const handleShowSigninSection = () => 12 | dispatch('ui.auth.toggleSection', 'signin'); 13 | 14 | const handleShowSignupSection = () => 15 | dispatch('ui.auth.toggleSection', 'signup'); 16 | 17 | export default observer(({ showSection, forms }) => ( 18 |
19 |
20 | 26 | 32 |
33 | 34 |
35 |

Login

36 | 37 |
38 | 39 |
40 |

Register

41 | 42 |
43 |
44 | )); 45 | -------------------------------------------------------------------------------- /src/shared/components/AppBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cx from 'classnames'; 3 | import { observer } from 'mobx-react'; 4 | import { dispatch } from 'rfx-core'; 5 | 6 | import styles from '@/shared/styles/AppBar.css'; 7 | import MenuLinksSX from './MenuLinksSX'; 8 | import MenuLinksDX from './MenuLinksDX'; 9 | 10 | const openNavBtn = cx('link', 'bn', 'ph3', 'pv3', 'fl', 'bg-transparent', 'pointer', '_c1'); 11 | const appBar = cx('animated', 'fadeIn', 'fixed', 'w-100', 'db', 'dt-l', 'bg-black-30'); 12 | 13 | // events 14 | const handleNavToggle = (e) => { 15 | e.preventDefault(); 16 | dispatch('ui.appNav.open'); 17 | }; 18 | 19 | export default observer(({ 20 | authCheck, 21 | user, 22 | accountMenuIsOpen, 23 | layoutIsShifted, 24 | }) => ( 25 |
30 |
31 | 37 |
38 | 39 |
40 |
41 |
42 | 48 |
49 |
50 | )); 51 | 52 | -------------------------------------------------------------------------------- /src/shared/components/form/controls/FormControls.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | import cx from 'classnames'; 5 | import $ from '@/shared/styles/_.mixins'; 6 | 7 | const errorMessage = cx('red', 'm2', 'pt4'); 8 | const button = cx($.buttonPill, '_c1', '_b1', 'b'); 9 | 10 | export default observer(({ form, controls = null, labels = null }) => ( 11 |
12 |
13 | 14 | {(!controls || controls.onSubmit) && 15 | } 25 | 26 | {(!controls || controls.onClear) && 27 | } 30 | 31 | {(!controls || controls.onReset) && 32 | } 35 | 36 |
37 | 38 | {((!controls || controls.error) && form.hasError) && 39 |

{form.error}

} 40 | 41 |
42 | )); 43 | -------------------------------------------------------------------------------- /src/shared/components/PostDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toJS } from 'mobx'; 3 | import { observer } from 'mobx-react'; 4 | import _ from 'lodash'; 5 | 6 | // components 7 | import TimeAgo from 'react-timeago'; 8 | 9 | // styles 10 | import styles from '@/shared/styles/PostList.css'; 11 | 12 | const NotLoaded = observer(({ item }) => ( 13 |
14 |
15 |

Loading ... {!item}

16 |
17 | )); 18 | 19 | const ItemDetail = observer(({ item }) => ( 20 |
21 |
22 |
23 | { item.completed 24 | ? 25 | : } 26 | {' '} 27 | { item.title } 28 |
29 |
ID: { item.uuid }
30 |
31 |
32 |

Created at:

33 |

Updated at:

34 |
35 |
36 | )); 37 | 38 | export default observer(({ item }) => { 39 | console.log('Rendering Post Details for item: %o', toJS(item)); // eslint-disable-line 40 | 41 | return ( 42 |
43 | {_.isEmpty(item) 44 | ? 45 | : } 46 |
47 | ); 48 | }); 49 | -------------------------------------------------------------------------------- /src/shared/components/PostList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | 4 | // components 5 | import { Link } from 'react-router'; 6 | import TimeAgo from 'react-timeago'; 7 | 8 | // styles 9 | import styles from '@/shared/styles/PostList.css'; 10 | 11 | const ItemsNotFound = () => ( 12 |
13 |
14 |

NO ITEMS FOUND

15 |
16 | ); 17 | 18 | const ItemsList = observer(({ items }) => ( 19 |
    20 | {items.map(item => 21 |
  • 22 |
    23 |
    24 | {item.completed 25 | ? 26 | : 27 | } {item.title} 28 |
    29 |
    30 | ID: {item.uuid} 31 |
    32 |
    33 |
    34 |

    Created at:

    35 |

    Updated at:

    36 |
    37 |
  • )} 38 |
39 | )); 40 | 41 | export default observer(({ items }) => ( 42 |
43 | {items.length 44 | ? 45 | : } 46 |
47 | )); 48 | -------------------------------------------------------------------------------- /src/web/ssr.js: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-filename-extension: [1, { "extensions": [".js", ".jsx"] }] */ 2 | import isDev from 'isdev'; 3 | import React from 'react'; 4 | import Helmet from 'react-helmet'; 5 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 6 | import { renderToString } from 'react-dom/server'; 7 | import { RouterContext } from 'react-router'; 8 | import { Provider } from 'mobx-react'; 9 | import { setMatchMediaConfig } from 'mobx-react-matchmedia'; 10 | import { fetchData, dehydrate } from 'rfx-core'; 11 | import stores from '@/shared/stores'; 12 | import bootstrap from './bootstrap'; 13 | 14 | export default (req, res, props) => { 15 | const cookieName = 'ssrToken'; 16 | 17 | const store = stores.inject({ 18 | app: { ssrLocation: req.url }, 19 | auth: { jwt: req.cookies[cookieName], cookieName }, 20 | ui: { mui: { userAgent: req.headers['user-agent'] } }, 21 | }); 22 | 23 | Promise.all(bootstrap(store)) 24 | .then(() => fetchData(store, props) 25 | .then(() => setMatchMediaConfig(req)) 26 | .then(() => renderToString( 27 | 28 | 29 | 30 | 31 | , 32 | )) 33 | .then(html => res 34 | .status(200) 35 | .render('index', { 36 | build: isDev ? null : '/build', 37 | head: Helmet.rewind(), 38 | state: dehydrate(), 39 | root: html, 40 | }))); 41 | }; 42 | -------------------------------------------------------------------------------- /src/utils/server.start.js: -------------------------------------------------------------------------------- 1 | import getenv from 'getenv'; 2 | 3 | class ServerSetup { 4 | init(props) { 5 | this.config = props.config; 6 | this.namespace = props.namespace; 7 | this.logger = props.logger; 8 | } 9 | } 10 | 11 | class ServerStart { 12 | 13 | constructor(app) { 14 | this.app = app; 15 | this.fixUA(); 16 | } 17 | 18 | init(props) { 19 | const key = props.namespace || 'api'; 20 | const configkey = this.configkey || 'server'; 21 | const logger = props.logger || null; 22 | const config = this.getFeathersConfig(configkey) || this.getEnvConfig(key); 23 | this.start(config, key, logger); 24 | } 25 | 26 | start(config, key, logger) { 27 | this.app 28 | .listen( 29 | config[key.toLowerCase()].port, 30 | config[key.toLowerCase()].host, 31 | ) 32 | .on('listening', () => logger && logger(key)); 33 | } 34 | 35 | getFeathersConfig(configkey) { 36 | return this.app.get(configkey); 37 | } 38 | 39 | getEnvConfig(key) { 40 | return { 41 | [key.toLowerCase()]: { 42 | host: getenv([key.toUpperCase(), 'HOST'].join('_')), 43 | port: getenv([key.toUpperCase(), 'PORT'].join('_')), 44 | }, 45 | }; 46 | } 47 | 48 | fixUA() { 49 | // Tell any CSS tooling (such as Material UI) to use 50 | // "all" vendor prefixes if the user agent is not known. 51 | global.navigator = global.navigator || {}; 52 | global.navigator.userAgent = global.navigator.userAgent || 'all'; 53 | } 54 | } 55 | 56 | const serverSetup = new ServerSetup(); 57 | 58 | export function setupServer($props) { 59 | serverSetup.init($props); 60 | } 61 | 62 | export function startServer() { 63 | new ServerStart(this).init(serverSetup); 64 | } 65 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | import log from 'winston'; 2 | import getenv from 'getenv'; 3 | 4 | // set log as cli mode 5 | log.cli(); 6 | 7 | export const webhost = key => ['http://', 8 | getenv([key.toUpperCase(), 'HOST'].join('_')), ':', 9 | getenv([key.toUpperCase(), 'PORT'].join('_')), 10 | ].join(''); 11 | 12 | const logInit = () => { 13 | log.info('------------------------------------------'); 14 | log.info('--------------- RFX STACK ----------------'); 15 | log.info('------------------------------------------'); 16 | }; 17 | 18 | const logServerAPI = (url) => { 19 | log.info('API Listening at:', url); 20 | log.info('Environment:', getenv('NODE_ENV')); 21 | log.info('------------------------------------------'); 22 | log.info('Database Host:', getenv('DB_HOST')); 23 | log.info('Database Name:', getenv('DB_NAME')); 24 | log.info('Database Port:', getenv('DB_PORT')); 25 | log.info('------------------------------------------'); 26 | }; 27 | 28 | const logServerWEB = (url) => { 29 | log.info('API Listening at:', url); 30 | log.info('Environment:', getenv('NODE_ENV')); 31 | log.info('------------------------------------------'); 32 | log.info('IO Host:', getenv('IO_HOST')); 33 | log.info('IO Port:', getenv('IO_PORT')); 34 | log.info('------------------------------------------'); 35 | }; 36 | 37 | export const logServerConfigWebpack = url => ([ 38 | 'RFX STACK', 39 | `WEB Listening at: ${webhost(url)}`, 40 | `Environment: ${getenv('NODE_ENV')}`, 41 | `IO Host: ${getenv('IO_HOST')}`, 42 | `IO Port: ${getenv('IO_PORT')}`, 43 | ]); 44 | 45 | export const logServerConfig = (key = null) => { 46 | logInit(); 47 | const url = webhost(key); 48 | return (key.toUpperCase() === 'API') 49 | ? logServerAPI(url) 50 | : logServerWEB(url); 51 | }; 52 | 53 | export { log }; 54 | -------------------------------------------------------------------------------- /src/api/server.js: -------------------------------------------------------------------------------- 1 | import feathers from 'feathers'; 2 | import configuration from 'feathers-configuration'; 3 | import local from 'feathers-authentication-local'; 4 | import jwt from 'feathers-authentication-jwt'; 5 | import hooks from 'feathers-hooks'; 6 | import rest from 'feathers-rest'; 7 | import socketio from 'feathers-socketio'; 8 | import adapter from 'feathers-mongoose'; 9 | import errorHandler from 'feathers-errors/handler'; 10 | 11 | import compression from 'compression'; 12 | import bodyParser from 'body-parser'; 13 | import cookieParser from 'cookie-parser'; 14 | import cors from 'cors'; 15 | 16 | import { setupServices, initServices } from '@/utils/services.autoload'; 17 | import { setupServer, startServer } from '@/utils/server.start'; 18 | import { logServerConfig } from '@/utils/logger'; 19 | 20 | import auth from './auth'; 21 | import { connector } from './connector'; 22 | import { autoloader } from './autoloader'; 23 | 24 | import loggerMiddleware from './middleware/logger'; 25 | import notFoundMiddleware from './middleware/notFound'; 26 | 27 | setupServer({ 28 | namespace: 'api', 29 | logger: logServerConfig, 30 | }); 31 | 32 | setupServices({ 33 | dir: __dirname, 34 | adapter, 35 | connector, 36 | autoloader, 37 | }); 38 | 39 | const app = feathers(); 40 | 41 | app 42 | .configure(configuration()) 43 | .use(compression()) 44 | .options('*', cors()) 45 | .use(cors({ origin: true })) 46 | .configure(rest()) 47 | .configure(socketio(io => io.set('origins', '*:*'))) 48 | .configure(hooks()) 49 | .use(cookieParser()) 50 | .use(bodyParser.json()) 51 | .use(bodyParser.urlencoded({ extended: true })) 52 | .configure(auth) 53 | .configure(local()) 54 | .configure(jwt()) 55 | .configure(initServices) 56 | .use(notFoundMiddleware()) 57 | .use(loggerMiddleware(app)) 58 | .use(errorHandler({ html: false })) 59 | .configure(startServer); 60 | -------------------------------------------------------------------------------- /webpack/config.server.build.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import ProgressBarPlugin from 'progress-bar-webpack-plugin'; 4 | import nodeExternalModules from 'webpack-node-externals'; 5 | import path from 'path'; 6 | 7 | const Dir = global.DIR; 8 | 9 | export function loader() { 10 | return { 11 | jsx: { 12 | query: { 13 | cacheDirectory: true, 14 | presets: [['es2015', { modules: false }], 'stage-0', 'react'], 15 | plugins: [ 16 | 'system-import-transformer', 17 | 'transform-decorators-legacy', 18 | 'transform-runtime', 19 | 'transform-class-properties', 20 | 'babel-root-import', 21 | ], 22 | }, 23 | }, 24 | cssModules: { 25 | loader: ExtractTextPlugin.extract({ 26 | fallbackLoader: 'isomorphic-style-loader', 27 | loader: ['css-loader?modules', 28 | 'importLoaders=1', 29 | 'localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'] 30 | .join('&'), 31 | }), 32 | }, 33 | cssGlobal: { 34 | loader: ExtractTextPlugin.extract({ 35 | fallbackLoader: 'isomorphic-style-loader', 36 | loader: 'css-loader!postcss-loader', 37 | }), 38 | }, 39 | }; 40 | } 41 | 42 | export function config(entry) { 43 | return { 44 | devtool: 'source-map', 45 | entry: [ 46 | 'babel-polyfill', 47 | 'isomorphic-fetch', 48 | 'whatwg-fetch', 49 | path.join(Dir.run, entry), 50 | ], 51 | output: { 52 | path: Dir.nodeBuild, 53 | filename: [entry, 'bundle', 'js'].join('.'), 54 | }, 55 | externals: [nodeExternalModules()], 56 | plugins: [ 57 | new ProgressBarPlugin(), 58 | new ExtractTextPlugin({ 59 | disable: true, 60 | }), 61 | new webpack.optimize.UglifyJsPlugin({ 62 | comments: false, 63 | sourceMap: true, 64 | compress: { 65 | screw_ie8: true, 66 | warnings: false, 67 | }, 68 | }), 69 | ], 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: 0 */ 2 | /* eslint import/first: 0 */ 3 | /* eslint import/newline-after-import: 0 */ 4 | /* eslint import/no-extraneous-dependencies: 0 */ 5 | import './run/global'; 6 | import { match } from 'rfx-core'; 7 | import merge from 'webpack-merge'; 8 | import Globals from './webpack/globals'; 9 | import getLoaders from './webpack/loaders'; 10 | 11 | let Config; 12 | let Loader = getLoaders(); 13 | 14 | if (match.script('web:dev', 'development')) { 15 | Config = require('./webpack/config.server').load(); 16 | const ConfigServerDev = require('./webpack/config.server.dev'); 17 | Loader = merge(Loader, ConfigServerDev.loader()); 18 | Config = merge(Config, ConfigServerDev.config('start.web')); 19 | } 20 | 21 | if (match.script('build:client:web', 'production')) { 22 | Config = require('./webpack/config.client').load(); 23 | const ConfigClientBuild = require('./webpack/config.client.build'); 24 | Loader = merge(Loader, ConfigClientBuild.loader()); 25 | Config = merge(Config, ConfigClientBuild.config('web')); 26 | } 27 | 28 | if (match.script('build:server:web', 'production')) { 29 | Config = require('./webpack/config.server').load(); 30 | const ConfigServerBuild = require('./webpack/config.server.build'); 31 | Loader = merge(Loader, ConfigServerBuild.loader()); 32 | Config = merge(Config, ConfigServerBuild.config('start.web')); 33 | } 34 | 35 | if (match.script('build:server:api', 'production')) { 36 | Config = require('./webpack/config.server').load(); 37 | const ConfigServerBuild = require('./webpack/config.server.build'); 38 | Loader = merge(Loader, ConfigServerBuild.loader()); 39 | Config = merge(Config, ConfigServerBuild.config('start.api')); 40 | } 41 | 42 | // Globals 43 | Config = merge(Config, Globals); 44 | 45 | // Loaders 46 | Config = merge(Config, { 47 | module: { 48 | loaders: [ 49 | Loader.eslint, 50 | Loader.jsx, 51 | Loader.json, 52 | Loader.url, 53 | Loader.file, 54 | Loader.cssGlobal, 55 | Loader.cssModules, 56 | ], 57 | }, 58 | }); 59 | 60 | const WebpackConfig = Config; 61 | export default WebpackConfig; 62 | -------------------------------------------------------------------------------- /webpack/config.client.dev.js: -------------------------------------------------------------------------------- 1 | import BrowserSyncPlugin from 'browser-sync-webpack-plugin'; 2 | import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'; 3 | import webpack from 'webpack'; 4 | import getenv from 'getenv'; 5 | import path from 'path'; 6 | 7 | import { logServerConfigWebpack, webhost } from '@/utils/logger'; 8 | 9 | const Dir = global.DIR; 10 | 11 | export function loader() { 12 | return { 13 | jsx: { 14 | query: { 15 | presets: [['es2015', { modules: false }], 'stage-0', 'react'], 16 | plugins: [ 17 | 'transform-decorators-legacy', 18 | 'transform-class-properties', 19 | 'transform-runtime', 20 | 'babel-root-import', 21 | 'react-hot-loader/babel', 22 | ], 23 | }, 24 | }, 25 | cssModules: { 26 | loaders: [ 27 | 'style-loader', 28 | ['css-loader?modules', 29 | 'importLoaders=1', 30 | 'localIdentName=[name]__[local]___[hash:base64:5]'] 31 | .join('&'), 32 | 'postcss-loader', 33 | ], 34 | }, 35 | }; 36 | } 37 | 38 | export function config(entry) { 39 | return { 40 | devtool: 'cheap-module-eval-source-map', 41 | entry: { 42 | app: [ 43 | 'babel-polyfill', 44 | 'isomorphic-fetch', 45 | 'whatwg-fetch', 46 | 'react-hot-loader/patch', 47 | 'webpack-hot-middleware/client', 48 | // ['webpack-hot-middleware/client', webhost].join('?'), 49 | path.join(Dir.src, entry, 'client'), 50 | ], 51 | }, 52 | output: { 53 | path: '/', 54 | publicPath: '/', 55 | filename: 'bundle.js', 56 | }, 57 | plugins: [ 58 | new FriendlyErrorsWebpackPlugin({ 59 | clearConsole: true, 60 | compilationSuccessInfo: { 61 | messages: logServerConfigWebpack(entry), 62 | }, 63 | }), 64 | new BrowserSyncPlugin({ 65 | host: getenv('BROWSERSYNC_HOST'), 66 | port: getenv('BROWSERSYNC_PORT'), 67 | proxy: webhost(entry), 68 | }, { reload: false }), 69 | new webpack.HotModuleReplacementPlugin(), 70 | new webpack.NoErrorsPlugin(), 71 | ], 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /webpack/config.server.dev.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import StartServerPlugin from 'start-server-webpack-plugin'; 4 | import nodeExternalModules from 'webpack-node-externals'; 5 | import path from 'path'; 6 | 7 | const Dir = global.DIR; 8 | 9 | export function loader() { 10 | return { 11 | jsx: { 12 | query: { 13 | cacheDirectory: true, 14 | presets: [['es2015', { modules: false }], 'stage-0', 'react'], 15 | plugins: [ 16 | 'system-import-transformer', 17 | 'transform-decorators-legacy', 18 | 'transform-runtime', 19 | 'transform-class-properties', 20 | 'babel-root-import', 21 | ], 22 | }, 23 | }, 24 | cssModules: { 25 | loader: ExtractTextPlugin.extract({ 26 | fallbackLoader: 'isomorphic-style-loader', 27 | loader: ['css-loader?modules', 28 | 'importLoaders=1', 29 | 'localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'] 30 | .join('&'), 31 | }), 32 | }, 33 | cssGlobal: { 34 | loader: ExtractTextPlugin.extract({ 35 | fallbackLoader: 'isomorphic-style-loader', 36 | loader: 'css-loader!postcss-loader', 37 | }), 38 | }, 39 | }; 40 | } 41 | 42 | export function config(entry) { 43 | return { 44 | devtool: 'cheap-module-eval-source-map', 45 | entry: [ 46 | 'babel-polyfill', 47 | 'isomorphic-fetch', 48 | 'whatwg-fetch', 49 | // 'webpack/hot/poll?1000', 50 | path.join(Dir.run, entry), 51 | ], 52 | output: { 53 | path: Dir.nodeBuild, 54 | filename: [entry, 'dev', 'bundle', 'js'].join('.'), 55 | chunkFilename: '[id].[hash:5]-[chunkhash:7].js', 56 | devtoolModuleFilenameTemplate: '[absolute-resource-path]', 57 | libraryTarget: 'commonjs2', 58 | }, 59 | externals: [nodeExternalModules()], 60 | // externals: [nodeExternalModules({ 61 | // whitelist: ['webpack/hot/poll?1000'], 62 | // })], 63 | plugins: [ 64 | new ExtractTextPlugin({ 65 | disable: true, 66 | }), 67 | new StartServerPlugin(), 68 | new webpack.HotModuleReplacementPlugin(), 69 | new webpack.NamedModulesPlugin(), 70 | new webpack.NoErrorsPlugin(), 71 | ], 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /webpack/config.client.build.js: -------------------------------------------------------------------------------- 1 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 2 | import ProgressBarPlugin from 'progress-bar-webpack-plugin'; 3 | import webpack from 'webpack'; 4 | import path from 'path'; 5 | 6 | import vendor from '~/config/vendor'; 7 | 8 | const Dir = global.DIR; 9 | 10 | export function loader() { 11 | return { 12 | jsx: { 13 | query: { 14 | presets: [['es2015', { modules: false }], 'stage-0', 'react'], 15 | plugins: [ 16 | 'transform-decorators-legacy', 17 | 'transform-class-properties', 18 | 'transform-runtime', 19 | 'babel-root-import', 20 | ], 21 | }, 22 | }, 23 | cssModules: { 24 | loader: ExtractTextPlugin.extract({ 25 | fallbackLoader: 'style-loader', 26 | loader: [ 27 | 'css-loader?modules', 28 | 'importLoaders=1', 29 | 'localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader', 30 | ].join('&'), 31 | }), 32 | }, 33 | cssGlobal: { 34 | loader: ExtractTextPlugin.extract({ 35 | fallbackLoader: 'style-loader', 36 | loader: 'css-loader!postcss-loader', 37 | }), 38 | }, 39 | }; 40 | } 41 | 42 | export function config(entry) { 43 | return { 44 | bail: true, 45 | devtool: 'source-map', 46 | entry: { 47 | vendor, 48 | app: [ 49 | 'babel-polyfill', 50 | 'isomorphic-fetch', 51 | 'whatwg-fetch', 52 | path.join(Dir.src, entry, 'client'), 53 | ], 54 | }, 55 | output: { 56 | path: path.join(Dir.public, 'build'), 57 | publicPath: '/build/', 58 | filename: [entry, 'app', 'bundle', 'js'].join('.'), 59 | }, 60 | plugins: [ 61 | new ProgressBarPlugin(), 62 | new webpack.optimize.UglifyJsPlugin({ 63 | comments: false, 64 | sourceMap: true, 65 | compress: { 66 | screw_ie8: true, 67 | warnings: false, 68 | }, 69 | }), 70 | new webpack.optimize.CommonsChunkPlugin({ 71 | name: 'vendor', 72 | minChunks: Infinity, 73 | filename: [entry, 'vendor', 'bundle', 'js'].join('.'), 74 | }), 75 | new ExtractTextPlugin({ 76 | filename: [entry, 'style', 'css'].join('.'), 77 | allChunks: true, 78 | }), 79 | ], 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /src/shared/components/PostCreateModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { observer } from 'mobx-react'; 3 | import { dispatch } from 'rfx-core'; 4 | 5 | import Modal from 'react-modal'; 6 | import TextField from 'material-ui/TextField'; 7 | import Toggle from 'material-ui/Toggle'; 8 | 9 | import _ from 'lodash'; 10 | import cx from 'classnames'; 11 | import $ from '@/shared/styles/_.mixins'; 12 | import modalBaseStyle from '@/shared/styles/_.modal.js'; 13 | 14 | const styles = _.cloneDeep(modalBaseStyle); 15 | const errorMessage = cx('red', 'm1'); 16 | const button = cx($.buttonPill, '_c1', '_b1', 'b'); 17 | 18 | _.assign(styles.content, { 19 | maxWidth: '450px', 20 | maxHeight: '300px', 21 | }); 22 | 23 | // events 24 | const handleCloseModal = () => 25 | dispatch('ui.postCreateModal.open', false); 26 | 27 | export default observer(({ open, form }) => ( 28 | 34 | {!(form && form.$) ? null : ( 35 |
36 |

{form.$('uuid').value ? 'Edit' : 'Create'} Post

37 |
38 |
39 | 47 | 55 |
56 |
57 | 62 |
63 |
68 | {form.genericErrorMessage} 69 |
70 |
71 |
72 | )} 73 |
74 | )); 75 | -------------------------------------------------------------------------------- /src/shared/containers/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { inject, observer } from 'mobx-react'; 4 | import cx from 'classnames'; 5 | import { Parallax } from 'react-parallax'; 6 | import styles from '@/shared/styles/Home.css'; 7 | 8 | @inject('store') @observer 9 | export default class Home extends Component { 10 | 11 | static fetchData() {} 12 | 13 | static propTypes = { 14 | store: React.PropTypes.object, 15 | }; 16 | 17 | render() { 18 | const bp = this.props.store.ui.breakpoints; 19 | return ( 20 |
21 | 22 |
23 | 24 |

RFX STACK

32 |

Universal App featuring: React + Feathers + MobX 40 |

41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 |
MobX Reactive State Management 49 |
50 |
51 | 52 |
Blazing fast Real Time by Feathers 53 |
54 |
55 | 56 |
React HOC for Responsive Media Queries 57 |
58 |
59 |
60 |
61 | 62 |
Isomorphic Fetch/Socket 63 |
64 |
65 | 66 |
Microservices Ready 67 |
68 |
69 | 70 |
React Hot Loader 3 71 |
72 |
73 |
74 |
75 |
76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/shared/stores/ui.js: -------------------------------------------------------------------------------- 1 | /* eslint no-confusing-arrow: 0 */ 2 | import _ from 'lodash'; 3 | import { observable, autorun } from 'mobx'; 4 | import { extend, toggle } from 'rfx-core'; 5 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 6 | import materialBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme'; 7 | import injectTapEventPlugin from 'react-tap-event-plugin'; 8 | import materialOverrideStyles from '@/shared/styles/_.material.js'; 9 | 10 | // ui classes 11 | import auth from './ui/Auth'; 12 | import appBar from './ui/AppBar'; 13 | import appNav from './ui/AppNav'; 14 | import snackBar from './ui/SnackBar'; 15 | import postCreateModal from './ui/PostCreateModal'; 16 | 17 | @extend({ 18 | auth, 19 | appBar, 20 | appNav, 21 | snackBar, 22 | postCreateModal, 23 | }) 24 | @toggle('shiftLayout', 'layoutIsShifted') 25 | export default class UIStore { 26 | 27 | mui = {}; 28 | 29 | @observable layoutIsShifted = false; 30 | 31 | @observable breakpoints = { 32 | xs: '(max-width: 767px)', 33 | su: '(min-width: 768px)', 34 | sm: '(min-width: 768px) and (max-width: 991px)', 35 | md: '(min-width: 992px) and (max-width: 1199px)', 36 | mu: '(min-width: 992px)', 37 | lg: '(min-width: 1200px)', 38 | }; 39 | 40 | init() { 41 | // shift the layout on "su" breakpoint when appnav is open 42 | autorun(() => this.breakpoints.su && this.appNav.isOpen 43 | ? this.shiftLayout(true) 44 | : this.shiftLayout(false), 45 | ); 46 | 47 | // undock the navbar if the modal is open 48 | autorun(() => this.auth.modalIsOpen 49 | ? this.appNav.open(false) 50 | : () => this.breakpoints.mu && this.appNav.open(true), 51 | ); 52 | 53 | /** 54 | The following autoruns demonstartes how to keep 55 | the navbar open from the startup and how to close it 56 | automatically when the browser window is resized 57 | */ 58 | 59 | // // open and close the nav automatically 60 | // // when the "xs" breakpoint changes 61 | // autorun(() => this.breakpoints.xs 62 | // ? this.appNav.open(false) 63 | // : this.appNav.open(true), 64 | // ); 65 | 66 | // // dock/undock the nav automatically 67 | // // when the "su" breakpoint changes 68 | // autorun(() => this.breakpoints.su 69 | // ? this.appNav.dock(true) 70 | // : this.appNav.dock(false), 71 | // ); 72 | } 73 | 74 | getMui() { 75 | const mui = (global.TYPE === 'CLIENT') 76 | ? { userAgent: navigator.userAgent } 77 | : {}; 78 | 79 | return getMuiTheme(this.mui, _.merge( 80 | mui, 81 | materialBaseTheme, 82 | materialOverrideStyles, 83 | )); 84 | } 85 | 86 | injectTapEventPlugin() { 87 | if (process.env.NODE_ENV === 'development') { 88 | return console.warn([ // eslint-disable-line no-console 89 | 'The react-tap-event-plugin is enabled only in production, ', 90 | 'due to a issue with Hot-Reloadable MobX Stores.', 91 | ].join('')); 92 | } 93 | // Material-UI components use react-tap-event-plugin to listen for touch events 94 | // This dependency is temporary and will go away once react v1.0 95 | return injectTapEventPlugin(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/shared/containers/AppLayout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { inject, observer } from 'mobx-react'; 4 | import cx from 'classnames'; 5 | 6 | // dev tools 7 | import isDev from 'isdev'; 8 | import DevTools from 'mobx-react-devtools'; 9 | import MobxReactFormDevTools from 'mobx-react-form-devtools'; 10 | 11 | // components 12 | import { MatchMediaProvider } from 'mobx-react-matchmedia'; 13 | import Snackbar from 'material-ui/Snackbar'; 14 | import AppBar from '@/shared/components/AppBar'; 15 | import AppNav from '@/shared/components/AppNav'; 16 | import AuthModal from '@/shared/components/AuthModal'; 17 | import MenuLinksSX from '@/shared/components/MenuLinksSX'; 18 | import MenuLinksDX from '@/shared/components/MenuLinksDX'; 19 | 20 | // forms 21 | import authForm from '@/shared/forms/auth'; 22 | import userForm from '@/shared/forms/user'; 23 | 24 | // styles 25 | import '@/shared/styles/_.global.css'; 26 | import styles from '@/shared/styles/AppLayout.css'; 27 | 28 | if (isDev) { 29 | MobxReactFormDevTools.register({ 30 | authForm, 31 | userForm, 32 | }); 33 | 34 | MobxReactFormDevTools.select('authForm'); 35 | MobxReactFormDevTools.open(false); 36 | } 37 | 38 | @inject('store') @observer 39 | export default class AppLayout extends Component { 40 | 41 | static fetchData() { } 42 | 43 | static propTypes = { 44 | children: React.PropTypes.node, 45 | store: React.PropTypes.object, 46 | }; 47 | 48 | render() { 49 | const { ui, auth } = this.props.store; 50 | 51 | return ( 52 | 53 |
54 | {isDev && } 55 | {isDev && } 56 | 61 | 67 | 72 | 73 | 74 |
75 | 81 |
82 | {this.props.children} 83 |
84 |
85 | ui.snackBar.close()} 90 | /> 91 | 99 |
100 |
101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/shared/stores/auth.js: -------------------------------------------------------------------------------- 1 | import { observable, computed, action, reaction } from 'mobx'; 2 | import { app, service } from '@/shared/app'; 3 | import { verifyJWT } from '@/utils/jwt'; 4 | import { browserHistory } from 'react-router'; 5 | import cookie from 'js-cookie'; 6 | import _ from 'lodash'; 7 | 8 | export default class AuthStore { 9 | 10 | cookieName = 'ssrToken'; 11 | 12 | redirect = '/'; 13 | 14 | jwt = null; 15 | 16 | @observable user = {}; 17 | 18 | init() { 19 | this.authenticate(); 20 | this.loadAuthPageOnLogout(); 21 | } 22 | 23 | authenticate() { 24 | if (global.TYPE === 'CLIENT') return this.authOnClient(); 25 | if (global.TYPE === 'SERVER') return this.authOnServer(); 26 | return null; 27 | } 28 | 29 | /** 30 | Authorize on Server Side 31 | authenticate on bootstrap 32 | */ 33 | authOnServer() { 34 | // authorize apis on server side 35 | if (this.jwt) return this.jwtAuth({ token: this.jwt }); 36 | // force logout on api server 37 | return this.logout(); 38 | } 39 | 40 | /** 41 | Authorize on Client Side 42 | authenticate on init 43 | */ 44 | authOnClient() { 45 | // run cookie auth only on client side 46 | const token = cookie.get(this.cookieName); 47 | // force logout if token not present 48 | if (!token) return this.logout(); 49 | // token present - authenticate 50 | return this.jwtAuth({ token }); 51 | } 52 | 53 | /** 54 | Check Auth (if user is logged) 55 | */ 56 | @computed get check() { 57 | return !_.isEmpty(this.user); 58 | } 59 | 60 | @action 61 | updateUser(data = null) { 62 | this.user = data || {}; 63 | } 64 | 65 | sessionAuth() { 66 | return app() 67 | .authenticate() 68 | .then(response => verifyJWT(response.accessToken)) 69 | .then(data => this.setCookie(data)) 70 | .then(payload => service('user').get(payload.userId)) 71 | .then(user => this.updateUser(user)); 72 | } 73 | 74 | jwtAuth({ token }) { 75 | return app() 76 | .authenticate({ strategy: 'jwt', accessToken: token }) 77 | .then(response => verifyJWT(response.accessToken)) 78 | .then(data => this.setCookie(data)) 79 | .then(payload => service('user').get(payload.userId)) 80 | .then(user => this.updateUser(user)); 81 | } 82 | 83 | @action 84 | login({ email, password }) { 85 | return app() 86 | .authenticate({ strategy: 'local', email, password }) 87 | .then(response => verifyJWT(response.accessToken)) 88 | .then(data => this.setCookie(data)) 89 | .then(payload => service('user').get(payload.userId)) 90 | .then(user => this.updateUser(user)) 91 | .then(user => this.redirectAfterLogin(user)); 92 | } 93 | 94 | @action 95 | register({ email, password, username }) { 96 | return service('user') 97 | .create({ email, password, username }); 98 | } 99 | 100 | setCookie({ token, payload }) { 101 | this.jwt = token; 102 | cookie.set(this.cookieName, token); 103 | return payload; 104 | } 105 | 106 | unsetCookie() { 107 | this.jwt = null; 108 | cookie.remove(this.cookieName); 109 | } 110 | 111 | @action 112 | logout() { 113 | return new Promise((resolve) => { 114 | app().logout(); 115 | this.unsetCookie(); 116 | this.updateUser({}); 117 | resolve(); 118 | }); 119 | } 120 | 121 | loadAuthPageOnLogout() { 122 | reaction(() => this.check, 123 | check => !check && browserHistory.push('/auth')); 124 | } 125 | 126 | redirectAfterLogin() { 127 | browserHistory.push(this.redirect); 128 | this.redirect = '/'; // reset default redirect 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/shared/components/MenuLinksDX.jsx: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/no-static-element-interactions: 0 */ 2 | import React from 'react'; 3 | import { observer } from 'mobx-react'; 4 | import { dispatch } from 'rfx-core'; 5 | import cx from 'classnames'; 6 | import $ from '@/shared/styles/_.mixins'; 7 | import styles from '@/shared/styles/MenuLinkDX.css'; 8 | 9 | const list = cx('list', 'br2', 'tl', 'pa0'); 10 | const inlineList = cx('mt1', 'mr3'); 11 | const blockList = cx(); 12 | const menuAccount = cx('absolute', 'right-0'); 13 | const btnBlock = cx('db', 'ph3', 'pv3', 'tc'); 14 | const btnInline = cx('dib', 'ph3', 'pv3'); 15 | const baseBtn = cx('pointer', 'fw4'); 16 | const authInlineBtn = cx($.buttonGeneric, 'mh2', 'mv1', 'mb2'); 17 | 18 | const handleMenuAccountToggle = (e) => { 19 | e.preventDefault(); 20 | dispatch('ui.appBar.toggleAccountMenu'); 21 | }; 22 | 23 | const handleAuthModalSignin = (e) => { 24 | e.preventDefault(); 25 | dispatch('ui.auth.toggleModal', 'open', 'signin'); 26 | }; 27 | 28 | const handleAuthModalSignup = (e) => { 29 | e.preventDefault(); 30 | dispatch('ui.auth.toggleModal', 'open', 'signup'); 31 | }; 32 | 33 | const handleLogout = (e) => { 34 | e.preventDefault(); 35 | dispatch('auth.logout'); 36 | }; 37 | 38 | const Avatar = observer(() => ( 39 |
40 | 45 |
46 | )); 47 | 48 | const UserSubMenu = observer(({ inline }) => ( 49 | 66 | )); 67 | 68 | const BlockSubMenu = observer(({ inline }) => ( 69 |
70 |
71 | 72 |
73 | )); 74 | 75 | const InlineSubMenu = observer(({ inline, accountMenuIsOpen }) => ( 76 |
82 | 83 |
84 | )); 85 | 86 | const UserMenu = observer(({ inline, user, accountMenuIsOpen }) => ( 87 | 88 | 92 | {user.email} {inline && } 93 | 94 | {inline ? 95 | : } 99 | 100 | )); 101 | 102 | const GuestMenu = observer(({ inline }) => ( 103 | 104 | 113 | Login 114 | 115 | 124 | Register 125 | 126 | 127 | )); 128 | 129 | export default observer(({ user, inline, authCheck, accountMenuIsOpen }) => ( 130 | 131 | {(authCheck && !inline) && } 132 | 133 | {authCheck ? 134 | : } 139 | 140 |
141 | 142 | )); 143 | -------------------------------------------------------------------------------- /src/shared/containers/Breakpoints.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { inject, observer } from 'mobx-react'; 4 | import cx from 'classnames'; 5 | import $ from '@/shared/styles/_.mixins'; 6 | 7 | // styles 8 | const button = cx($.buttonPill, '_c1', '_b1'); 9 | 10 | @inject('store') @observer 11 | export default class MatchMedia extends Component { 12 | 13 | static fetchData() {} 14 | 15 | static propTypes = { 16 | store: React.PropTypes.object, 17 | }; 18 | 19 | render() { 20 | const bp = this.props.store.ui.breakpoints; 21 | return ( 22 |
23 | 24 |
25 |
26 |
27 |

MobX React MatchMedia

28 |

Resize your browser window to see the breakpoints changing automatically.

29 |

The breakpoints are customizables in /src/shared/stores/ui.js

30 |
31 |

32 | 38 |

39 |

40 | 46 |

47 |
48 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 92 | 98 | 99 | 100 | 101 | 102 | 108 | 109 | 110 | 111 |
xs 53 | {bp.xs 54 | ? 55 | : 56 | } 57 | Extra small devices
su 63 | {bp.su 64 | ? 65 | : 66 | } 67 | Small devices and UP
sm 73 | {bp.sm 74 | ? 75 | : 76 | } 77 | Small devices
md 83 | {bp.md 84 | ? 85 | : 86 | } 87 | Medium devices
mu 93 | {bp.mu 94 | ? 95 | : 96 | } 97 | Medium devices and UP
lg 103 | {bp.lg 104 | ? 105 | : 106 | } 107 | Large devices and UP
112 |
113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RFX Stack 2 | 3 | #### Universal App featuring: 4 | ### React + Feathers + MobX 5 | --- 6 | 7 | [![GitHub license](https://img.shields.io/github/license/foxhound87/rfx-stack.svg)]() 8 | [![node](https://img.shields.io/badge/node-5.0%2B-blue.svg)]() 9 | [![npm](https://img.shields.io/badge/npm-3.3%2B-blue.svg)]() 10 | 11 | ## Changelog & Documentation 12 | See the [Changelog](https://github.com/foxhound87/rfx-stack/blob/master/CHANGELOG.md) or the [Documentation](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md) for all the details. 13 | 14 | --- 15 | 16 | ## Main Features 17 | - Hot-Reloadable MobX Stores 18 | - Action Dispatcher for Stateless Components 19 | - Server Side Rendering (SSR) 20 | - Reactive UI & Media Queries 21 | - React Hot Loader 3 22 | - React Stateless Components 23 | - Isomorphic Fetch/Socket 24 | - Multi Platform Ready 25 | - Real Time Ready 26 | - Microservices Ready 27 | - Functional & Modular CSS 28 | - Webpack 2 w/ code-splitting 29 | 30 | ## Main Libs 31 | 32 | | Name | Description | | | 33 | |---|---|---|---| 34 | | **react** | View Layer | [GitHub ➜](https://github.com/facebook/react) | [NPM ➜](https://www.npmjs.com/package/react) | 35 | | **react-router** | Routing | [GitHub ➜](https://github.com/reactjs/react-router) | [NPM ➜](https://www.npmjs.com/package/react-router) | 36 | | **mobx** | State Management | [GitHub ➜](https://github.com/mobxjs/mobx) | [NPM ➜](https://www.npmjs.com/package/mobx) | 37 | | **feathers** | Server, CRUD & Data Transport | [GitHub ➜](https://github.com/feathersjs/feathers) | [NPM ➜](https://www.npmjs.com/package/feathers) | 38 | | **postcss** | Styling | [GitHub ➜](https://github.com/postcss/postcss) | [NPM ➜](https://www.npmjs.com/package/postcss) | 39 | | **browser-sync** | Live Browser Syncing | [GitHub ➜](https://github.com/browsersync/browser-sync) | [NPM ➜](https://www.npmjs.com/package/browser-sync) | 40 | | **mobx-react-form** | Forms Management | [GitHub ➜](https://github.com/foxhound87/mobx-react-form) | [NPM ➜](https://www.npmjs.com/package/mobx-react-form) | 41 | | **babel** | Javascript Transpiler | [GitHub ➜](https://github.com/babel/babel) | [NPM ➜](https://www.npmjs.com/package/babel) | 42 | | **webpack 2** | Javascript Bundler | [GitHub ➜](https://github.com/webpack/webpack) | [NPM ➜](https://www.npmjs.com/package/webpack) | 43 | | **eslint** | Code Linter | [GitHub ➜](https://github.com/eslint/eslint) | [NPM ➜](https://www.npmjs.com/package/eslint) | 44 | | **eslint-config-airbnb** | Code Style Guide & Rules | [GitHub ➜](https://github.com/airbnb/javascript) | [NPM ➜](https://www.npmjs.com/package/eslint-config-airbnb) | 45 | | **electron** | Cross platform desktop app | [GitHub ➜](https://github.com/electron/electron) | [Website ➜](http://electron.atom.io/) | | 46 | 47 | 48 | --- 49 | 50 | # Quick Setup 51 | 52 | > Run a local MongoDB instance (port 27017) before start the server. 53 | [Install MongoDB](https://docs.mongodb.org/manual/administration/install-community/) 54 | 55 | #### ENV: Development 56 | 57 | `npm install` 58 | 59 | > Run each script in different terminals. 60 | 61 | `npm run api:dev` 62 | 63 | `npm run web:dev` 64 | 65 | > Run the **seed** app or the **web** app after the **api** app is running. 66 | 67 | `npm run seed:dev` 68 | 69 | #### ENV: Production 70 | 71 | `npm install` 72 | 73 | > Build 74 | 75 | `npm run build:client:web` 76 | 77 | `npm run build:server:web` 78 | 79 | `npm run build:server:api` 80 | 81 | > Run 82 | 83 | `npm run api:prod` 84 | 85 | `npm run web:prod` 86 | 87 | #### Electron App 88 | 89 | [Click here to see how to setup the electron app](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#electron) 90 | 91 | --- 92 | 93 | ## Getting started with RFX Stack 94 | 95 | - [Ten minute introduction to MobX and React](https://mobxjs.github.io/mobx/getting-started.html) 96 | - [State Management and Hydration with MobX for SSR](https://medium.com/@foxhound87/state-management-hydration-with-mobx-we-must-react-ep-05-1922a72453c6) 97 | - [Functional CSS - The Good, The Bad, and Some Protips for React.js Users](https://github.com/chibicode/react-functional-css-protips) 98 | - [Feathers API service composition with hooks](https://blog.feathersjs.com/api-service-composition-with-hooks-47af13aa6c01) 99 | 100 | 101 | ## Credits 102 | 103 | Thanks to [Eric John Juta](https://github.com/rej156) for his contribution about the **Hot-Reloadable MobX Stores** implementation. 104 | 105 | ## Contributing 106 | 107 | If you like this stack, don't forget to star the repo! 108 | 109 | If you want to contribute to the development, do not hesitate to fork the repo and send pull requests. 110 | -------------------------------------------------------------------------------- /src/shared/containers/Packages.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { inject, observer } from 'mobx-react'; 4 | import cx from 'classnames'; 5 | import $ from '@/shared/styles/_.mixins'; 6 | 7 | @inject('store') @observer 8 | export default class MatchMedia extends Component { 9 | 10 | static fetchData() {} 11 | 12 | static propTypes = { 13 | store: React.PropTypes.object, 14 | }; 15 | 16 | render() { 17 | const bp = this.props.store.ui.breakpoints; 18 | return ( 19 |
20 | 21 |
22 |
23 |
24 |

MobX React Form

25 |

Automagically manage React forms state and automatic validation with MobX.

26 |

27 | See the Documentation for all the details. 28 |

29 |
30 |

31 | 35 | foxhound87/mobx-react-form 36 | 37 |

38 |

39 | 43 | package/mobx-react-form 44 | 45 |

46 |
47 |
48 |
49 |
50 |

MobX React MatchMedia

51 |

Resize your browser window to see the breakpoints changing automatically.

52 |

The breakpoints are customizables in /src/shared/stores/ui.js

53 |
54 |

55 | 59 | foxhound87/mobx-react-matchmedia 60 | 61 |

62 |

63 | 67 | package/mobx-react-matchmedia 68 | 69 |

70 |
71 | 72 | 73 | 74 | 75 | 81 | 82 | 83 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 | 104 | 105 | 111 | 112 | 113 | 114 | 115 | 121 | 122 | 123 | 124 | 125 | 131 | 132 | 133 | 134 |
xs 76 | {bp.xs 77 | ? 78 | : 79 | } 80 | Extra small devices
su 86 | {bp.su 87 | ? 88 | : 89 | } 90 | Small devices and UP
sm 96 | {bp.sm 97 | ? 98 | : 99 | } 100 | Small devices
md 106 | {bp.md 107 | ? 108 | : 109 | } 110 | Medium devices
mu 116 | {bp.mu 117 | ? 118 | : 119 | } 120 | Medium devices and UP
lg 126 | {bp.lg 127 | ? 128 | : 129 | } 130 | Large devices and UP
135 |
136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfx-stack", 3 | "version": "0.8.0-alpha.8", 4 | "license": "MIT", 5 | "description": "RFX Stack - Universal App featuring: React + Feathers + MobX", 6 | "author": "Claudio Savino (https://twitter.com/foxhound87)", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/foxhound87/rfx-stack.git" 10 | }, 11 | "scripts": { 12 | "lint": "eslint . --ext .jsx,.js --ignore-path .gitignore", 13 | "clean:build": "rm -rf ./public/build && rm -rf ./run/build", 14 | "clean:modules": "rm -rf ./node_modules && npm cache clean", 15 | "build:client:web": "cross-env NODE_ENV=production webpack", 16 | "build:server:web": "cross-env NODE_ENV=production webpack", 17 | "build:server:api": "cross-env NODE_ENV=production webpack", 18 | "web:dev": "cross-env NODE_ENV=development webpack --watch ./run/start.web.js", 19 | "web:prod": "cross-env NODE_ENV=production node ./run/build/start.web.bundle.js", 20 | "api:dev": "cross-env NODE_ENV=development nodemon ./run/start.api.js", 21 | "api:prod": "cross-env NODE_ENV=production node ./run/build/start.api.bundle.js", 22 | "seed:dev": "cross-env NODE_ENV=development node ./run/start.seeder.js", 23 | "seed:prod": "cross-env NODE_ENV=production node ./run/start.seeder.js" 24 | }, 25 | "dependencies": { 26 | "animate.css": "3.5.2", 27 | "autoprefixer": "6.5.3", 28 | "babel-register": "6.18.0", 29 | "body-parser": "1.15.2", 30 | "classnames": "2.2.5", 31 | "compression": "1.6.2", 32 | "cookie-parser": "1.4.3", 33 | "cors": "2.8.1", 34 | "cross-env": "3.1.3", 35 | "css-modules-require-hook": "4.0.5", 36 | "dotenv": "2.0.0", 37 | "ejs": "2.5.5", 38 | "faker": "3.1.0", 39 | "feathers": "2.0.3", 40 | "feathers-authentication": "1.0.2", 41 | "feathers-authentication-client": "0.1.6", 42 | "feathers-authentication-jwt": "0.3.1", 43 | "feathers-authentication-local": "0.3.2", 44 | "feathers-authentication-oauth2": "0.2.3", 45 | "feathers-configuration": "0.4.1", 46 | "feathers-errors": "2.5.0", 47 | "feathers-hooks": "1.7.1", 48 | "feathers-hooks-common": "2.0.3", 49 | "feathers-mongoose": "3.6.1", 50 | "feathers-permissions": "0.1.1", 51 | "feathers-rest": "1.6.0", 52 | "feathers-socketio": "1.4.2", 53 | "font-awesome": "4.7.0", 54 | "getenv": "0.7.0", 55 | "globule": "1.1.0", 56 | "isdev": "1.0.1", 57 | "isomorphic-fetch": "2.2.1", 58 | "js-cookie": "2.1.3", 59 | "jwt-decode": "2.1.0", 60 | "lodash": "4.17.2", 61 | "material-ui": "0.16.4", 62 | "mobx": "^3.0.2", 63 | "mobx-react": "^4.1.0", 64 | "mobx-react-devtools": "^4.2.11", 65 | "mobx-react-form": "~1.31.0", 66 | "mobx-react-form-devtools": "^1.6.0", 67 | "mobx-react-matchmedia": "^1.3.1", 68 | "moment": "2.17.1", 69 | "mongoose": "4.7.1", 70 | "morgan": "1.7.0", 71 | "normalize.css": "5.0.0", 72 | "react": "15.4.2", 73 | "react-dom": "15.4.2", 74 | "react-helmet": "3.3.0", 75 | "react-hot-loader": "3.0.0-beta.6", 76 | "react-modal": "1.6.5", 77 | "react-pagify": "2.1.1", 78 | "react-parallax": "1.2.2", 79 | "react-router": "3.0.2", 80 | "react-tap-event-plugin": "2.0.1", 81 | "react-timeago": "3.1.3", 82 | "rfx-core": "^1.5.3", 83 | "serve-static": "1.11.1", 84 | "socket.io-client": "1.7.2", 85 | "tachyons": "4.6.1", 86 | "uuid": "3.0.1", 87 | "validatorjs": "3.11.0", 88 | "winston": "2.3.0" 89 | }, 90 | "peerDependencies": { 91 | "react": "15.4.2", 92 | "react-dom": "15.4.2" 93 | }, 94 | "devDependencies": { 95 | "babel-core": "6.21.0", 96 | "babel-eslint": "7.1.1", 97 | "babel-loader": "6.2.10", 98 | "babel-plugin-system-import-transformer": "2.4.0", 99 | "babel-plugin-transform-class-properties": "6.19.0", 100 | "babel-plugin-transform-decorators-legacy": "1.3.4", 101 | "babel-plugin-transform-runtime": "6.15.0", 102 | "babel-polyfill": "6.20.0", 103 | "babel-preset-es2015": "6.18.0", 104 | "babel-preset-react": "6.16.0", 105 | "babel-preset-stage-0": "6.16.0", 106 | "babel-root-import": "4.1.5", 107 | "bluebird": "3.4.6", 108 | "browser-sync": "2.18.2", 109 | "browser-sync-webpack-plugin": "1.1.3", 110 | "css-loader": "0.26.1", 111 | "cssnano": "3.8.1", 112 | "eslint": "3.15.0", 113 | "eslint-config-airbnb": "14.0.0", 114 | "eslint-loader": "1.6.1", 115 | "eslint-plugin-import": "2.2.0", 116 | "eslint-plugin-jsx-a11y": "3.0.2", 117 | "eslint-plugin-react": "6.9.0", 118 | "extract-text-webpack-plugin": "2.0.0-beta.4", 119 | "file-loader": "0.9.0", 120 | "friendly-errors-webpack-plugin": "1.1.2", 121 | "imports-loader": "0.6.5", 122 | "isomorphic-style-loader": "1.1.0", 123 | "json-loader": "0.5.4", 124 | "nodemon": "1.11.0", 125 | "postcss": "5.2.6", 126 | "postcss-extend": "1.0.5", 127 | "postcss-focus": "1.0.0", 128 | "postcss-import": "9.0.0", 129 | "postcss-loader": "1.2.0", 130 | "postcss-url": "5.1.2", 131 | "precss": "1.4.0", 132 | "progress-bar-webpack-plugin": "1.9.0", 133 | "start-server-webpack-plugin": "2.1.1", 134 | "style-loader": "0.13.1", 135 | "url-loader": "0.5.7", 136 | "webpack": "2.1.0-beta.27", 137 | "webpack-dev-middleware": "1.9.0", 138 | "webpack-hot-middleware": "2.15.0", 139 | "webpack-merge": "2.3.1", 140 | "webpack-node-externals": "1.5.4", 141 | "whatwg-fetch": "2.0.1" 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/shared/stores/post.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { extendObservable, observable, action, computed } from 'mobx'; 3 | 4 | import { service } from '@/shared/app'; 5 | import { factory } from '@/seeds/factories/post'; // just for test 6 | 7 | import postForm, { init as initPostForm } from '@/shared/forms/post'; 8 | 9 | export default class PostStore { 10 | 11 | static post = { 12 | uuid: null, 13 | title: null, 14 | completed: null, 15 | createdAt: null, 16 | updatedAt: null, 17 | }; 18 | 19 | query = {}; 20 | 21 | @observable searchValue = ''; 22 | 23 | @observable filter = 'all'; 24 | 25 | @observable list = []; 26 | 27 | @observable selected = _.clone(PostStore.post); 28 | 29 | @observable editForm = postForm; 30 | 31 | /* 32 | "total": "", 33 | "limit": "", 34 | "skip": "", 35 | "current": "" 36 | "pages": "" 37 | */ 38 | @observable $pagination = {}; 39 | 40 | init() { 41 | // run events on client side-only 42 | if (global.TYPE === 'CLIENT') this.initEvents(); 43 | } 44 | 45 | initEvents() { 46 | service('post').on('created', action(this.onCreated)); // onCreated = (data, params) => {} 47 | service('post').on('updated', action(this.onUpdated)); // onUpdated = (data) => {} 48 | service('post').on('patched', action(this.onPatched)); // onPatched = (data) => {} 49 | // service('post').on('removed', action(this.onRemoved)); // onRemoved = (id, params) => {} 50 | } 51 | 52 | 53 | @action 54 | setSelected(json = {}) { 55 | if (_.isEmpty(json)) { 56 | return this.clearSelected(); 57 | } 58 | 59 | this.editForm = initPostForm(json); 60 | 61 | console.log('Setting Selected Post: %o', json); //eslint-disable-line 62 | extendObservable(this.selected, json); 63 | 64 | return this.selected; 65 | } 66 | 67 | @action 68 | clearSelected() { 69 | extendObservable(this.selected, PostStore.post); 70 | console.assert(!this.selected.uuid, 'Selected Object UUID must be null'); // eslint-disable-line 71 | 72 | this.editForm = postForm; 73 | 74 | return this.selected; 75 | } 76 | 77 | @action 78 | updateList(json) { 79 | this.list = json.data; 80 | this.$pagination = _.omit(json, 'data'); 81 | } 82 | 83 | @computed 84 | get pagination() { 85 | const { total, limit, skip } = this.$pagination; 86 | return _.extend(this.$pagination, { 87 | pages : Math.ceil(total / limit), 88 | current : Math.ceil((skip - 1) / limit) + 1, 89 | }); 90 | } 91 | 92 | @action 93 | emptyList() { 94 | this.list = []; 95 | } 96 | 97 | addItem(item) { 98 | if (this.list.length >= this.$pagination.limit) this.list.pop(); 99 | this.list.unshift(item); 100 | this.$pagination.total += 1; 101 | } 102 | 103 | create(data = null) { 104 | // we use factory() just for test 105 | return service('post') 106 | .create(data || factory()) 107 | .catch(err => console.error(err)); // eslint-disable-line no-console 108 | } 109 | 110 | update(data = {}, id = data.uuid) { 111 | return service('post') 112 | .patch(id, data) 113 | .catch(err => console.error(err)); // eslint-disable-line no-console 114 | } 115 | 116 | get(id) { 117 | if (_.isEmpty(id)) { 118 | return Promise.reject('Must Specify Message ID'); 119 | } 120 | 121 | return service('post') 122 | .get(id) 123 | .then(post => this.setSelected(post)) 124 | .catch(err => console.error(err)); // eslint-disable-line no-console 125 | } 126 | 127 | clear() { 128 | return this.clearSelected(); 129 | } 130 | 131 | find(query = {}) { 132 | _.merge(this.query, query); 133 | return service('post') 134 | .find(this.query) 135 | .then(json => this.updateList(json)); 136 | } 137 | 138 | /* EVENTS */ 139 | 140 | onCreated = item => this.addItem(item); 141 | 142 | @action 143 | onUpdated = (data) => { 144 | console.log('Received Post Update: %O', data); // eslint-disable-line 145 | 146 | const existing = _.find(this.list, { uuid: data.uuid }); 147 | if (existing) { 148 | _.extend(existing, data); 149 | } 150 | 151 | if (this.selected.uuid === data.uuid) { 152 | _.extend(this.selected, data); 153 | } 154 | }; 155 | 156 | onPatched = this.onUpdated; 157 | 158 | // onRemoved = (id, params) => {}; 159 | 160 | /* ACTIONS */ 161 | 162 | @action 163 | page(page = 1) { 164 | const skipPage = this.$pagination.limit * (page - 1); 165 | const { pages } = this.$pagination; 166 | if (skipPage < 0 || page > pages) return null; 167 | return this.find({ query: { $skip: skipPage } }); 168 | } 169 | 170 | @action 171 | search(title = null) { 172 | this.searchValue = title || ''; 173 | return this.find({ 174 | query: { 175 | $skip: 0, 176 | title: { 177 | $regex: `.*${this.searchValue}.*`, 178 | $options: 'i', 179 | }, 180 | }, 181 | }); 182 | } 183 | 184 | @action 185 | filterBy(filter) { 186 | this.filter = filter; 187 | let completed; 188 | 189 | switch (this.filter) { 190 | case 'all': this.query.query.completed = undefined; break; 191 | case 'todo': completed = false; break; 192 | case 'done': completed = true; break; 193 | default: completed = 'all'; 194 | } 195 | 196 | if (filter === 'all') return this.find(); 197 | return this.find({ query: { completed } }); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Index 2 | 3 | - [Introduction](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#introduction) 4 | - [Requirements](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#requirements) 5 | - [Scripts](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#scripts) 6 | - [Utils](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#utils) 7 | - [Builders](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#builders) 8 | - [Runners](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#runners) 9 | - [Electron](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#electron) 10 | - [Setup Stores](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#setup-stores) 11 | - [Server Side Rendering](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#server-side-rendering) 12 | - [Connect / Observer](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#connect--observer) 13 | - [Dispatch / Actions](https://github.com/foxhound87/rfx-stack/blob/master/DOCUMENTATION.md#dispatch--actions) 14 | 15 | 16 | # Introduction 17 | With the RFX Stack you can build and run different pieces of the app independently. 18 | 19 | You are also free to change some of its parts as you need. 20 | 21 | For this purpose the code is divided into differents compartments: 22 | 23 | - api 24 | - electron 25 | - seeds 26 | - shared 27 | - utils 28 | - web 29 | 30 | This structure does not force you to separate the server-side code from the client-side, as with React and its server side rendering features, these two concepts are more coupled than ever. The **web** directory contains both the server and client code specifically needed for in-browser rendering. The **shared** directory contains code that can be shared, for example, with React Native or Electron in the same project. That's the main goal: provide flexibility and extensibility. 31 | 32 | --- 33 | 34 | # Requirements 35 | 36 | - node@^5.x.x 37 | - npm@^3.3.x 38 | 39 | --- 40 | 41 | # Scripts 42 | 43 | ## Utils 44 | 45 | | Command | Description | 46 | |---|---| 47 | | **lint** | Code linting & syntax cheking. | 48 | | **clean:build** | Delete all the generated bundles. | 49 | | **clean:modules** | Delete `node_modules` and cache | 50 | 51 | 52 | ## Builders 53 | 54 | | Command | Type | Output Dir | Description | 55 | |---|---|---|---| 56 | | **build:client:web** | client | `/public/build` | Build the browser client-side code of the **web** app. | 57 | | **build:server:web** | server | `/run/build` | Build the node server-side code of the **web** app. | 58 | | **build:server:api** | server | `/run/build` | Build the node server-side code of the **api** app. | 59 | 60 | ## Runners 61 | 62 | ##### ENV: Development 63 | | Command | Env | Description | 64 | |---|---|---| 65 | | **web:dev** | development | Run only the **web** app. | 66 | | **api:dev** | development | Run only the **api** app. | 67 | | **seed:dev** | development | Run only the **seed** app. | 68 | 69 | ##### ENV: Production 70 | | Command | Env | Description | 71 | |---|---|---| 72 | | **web:prod** | production | Run only the **web** app. | 73 | | **api:prod** | production | Run only the **api** app. | 74 | | **seed:prod** | production | Run only the **seed** app. | 75 | 76 | --- 77 | 78 | # Electron 79 | 80 | Go to `/src/electron` and run `npm install`. 81 | 82 | The `electron` app depends directly from the `web` app, so the client side bundles must be availables running the web app in dev mode or building the client-side code for prod mode. 83 | 84 | If you want develop with the hot loader enabled you have to make sure that the global `global.HOT` is defined in `/src/electron/src/globals.js`. 85 | 86 | When you want to go in production, just set it to false or comment it. 87 | 88 | So, in case you disabled it, you have to build the client-side code. 89 | 90 | Then to start the app, run in sequence: 91 | 92 | > in the project root: 93 | 94 | `npm run api:dev` 95 | 96 | `npm run build:client:web` // only if **global.HOT** is NOT defined 97 | 98 | `npm run web:dev` // only if **global.HOT** is defined 99 | 100 | > in the electron root: 101 | 102 | `npm start` 103 | 104 | 105 | # Setup Stores 106 | 107 | Create your stores files as Classes with `export default class` in `/src/shared/stores/*` and then assigns them a key in the `store.setup() method` in the `/src/shared/stores.js` file. 108 | 109 | ```javascript 110 | import { store } from 'rfx-core'; 111 | 112 | import UIStore from './stores/ui'; 113 | 114 | /** 115 | Stores 116 | */ 117 | export default store 118 | .setup({ 119 | ui: UIStore, 120 | }); 121 | 122 | ``` 123 | 124 | The mapped Stores are called by the **Store Initalizer** of the `rfx-core` that will automatically inject the **inital state** in themselves. It is also be used as a getter of the Stores. 125 | 126 | Now we can use the `mobx-react` **Provide** Component on both client and server: 127 | 128 | ```javascript 129 | import { Provider } from 'mobx-react'; 130 | 131 | 132 | ... 133 | 134 | ``` 135 | 136 | On the **server**-side: `/src/web/ssr.js`; 137 | 138 | On the **client**-side: `/src/web/App.js`; 139 | 140 | # Server Side Rendering 141 | 142 | Define the inital state of the Stores in `/src/web/ssr.js` injecting it using the `inject` method of the Store Initalizer. 143 | 144 | ```javascript 145 | import stores from '~/src/shared/stores'; 146 | ... 147 | 148 | const store = stores.inject({ 149 | app: { ssrLocation: req.url }, 150 | // put here the inital state of other stores... 151 | }); 152 | ``` 153 | 154 | The inital state can be dynamically updated using **fetchData**: 155 | 156 | For fetching specific data on specific pages (rendered both on the server and client), we use a `static fetchData({ store, params, query })` inside our react containers in`/src/shared/containers/*`. It passes the stores, and react-router params and query for the current location. 157 | 158 | ```javascript 159 | class Home extends Component { 160 | 161 | static fetchData({ store }) { 162 | return store.post.find(); 163 | } 164 | 165 | ... 166 | ``` 167 | 168 | **static fetchData()** will be automatically called when React Router reaches that component. 169 | 170 | 171 | # Connect / Observer 172 | 173 | Use the `mobx-react` **@observer** decorator to pass the `stores` props to the **Containers**. 174 | 175 | 176 | in `/src/shared/containers/*`: 177 | 178 | ```javascript 179 | import { observer } from 'mobx-react'; 180 | ... 181 | 182 | @observer(['store']) 183 | export default class Home extends Component { 184 | 185 | static propTypes = { 186 | store: React.PropTypes.object, 187 | }; 188 | 189 | render() { 190 | const items = this.props.store.post.list; 191 | return ( 192 | ... 193 | ); 194 | } 195 | } 196 | ``` 197 | 198 | The **@connect** decorator also wraps the component with the MobX **observer** making it reactive. 199 | 200 | You can use it also on the Stateless Components to make it reactive, but you cannot access the provided stores from there, you must pass the store as props from a parent component (a container) instead. 201 | 202 | # Dispatch / Actions 203 | 204 | The **dispatch()** function is handy to call an **action** when handle component events. It can also be called from another Store too. 205 | 206 | Use the dot notation to select a store key (defined in **Setup Stores** previously) and the name of the method/action: 207 | 208 | ```javascript 209 | import { dispatch } from 'rfx-core'; 210 | 211 | ... 212 | 213 | const handleOnSubmitFormRegister = (e) => { 214 | e.preventDefault(); 215 | dispatch('auth.login', { email, password }).then( ... ); 216 | }; 217 | ``` 218 | 219 | Also params can be passed if needed. 220 | --------------------------------------------------------------------------------