├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
38 |
39 |
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 |
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 |
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 | | xs |
52 |
53 | {bp.xs
54 | ?
55 | :
56 | }
57 | |
58 | Extra small devices |
59 |
60 |
61 | | su |
62 |
63 | {bp.su
64 | ?
65 | :
66 | }
67 | |
68 | Small devices and UP |
69 |
70 |
71 | | sm |
72 |
73 | {bp.sm
74 | ?
75 | :
76 | }
77 | |
78 | Small devices |
79 |
80 |
81 | | md |
82 |
83 | {bp.md
84 | ?
85 | :
86 | }
87 | |
88 | Medium devices |
89 |
90 |
91 | | mu |
92 |
93 | {bp.mu
94 | ?
95 | :
96 | }
97 | |
98 | Medium devices and UP |
99 |
100 |
101 | | lg |
102 |
103 | {bp.lg
104 | ?
105 | :
106 | }
107 | |
108 | Large devices and UP |
109 |
110 |
111 |
112 |
113 | );
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RFX Stack
2 |
3 | #### Universal App featuring:
4 | ### React + Feathers + MobX
5 | ---
6 |
7 | []()
8 | []()
9 | []()
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 |
47 |
71 |
72 |
73 |
74 | | xs |
75 |
76 | {bp.xs
77 | ?
78 | :
79 | }
80 | |
81 | Extra small devices |
82 |
83 |
84 | | su |
85 |
86 | {bp.su
87 | ?
88 | :
89 | }
90 | |
91 | Small devices and UP |
92 |
93 |
94 | | sm |
95 |
96 | {bp.sm
97 | ?
98 | :
99 | }
100 | |
101 | Small devices |
102 |
103 |
104 | | md |
105 |
106 | {bp.md
107 | ?
108 | :
109 | }
110 | |
111 | Medium devices |
112 |
113 |
114 | | mu |
115 |
116 | {bp.mu
117 | ?
118 | :
119 | }
120 | |
121 | Medium devices and UP |
122 |
123 |
124 | | lg |
125 |
126 | {bp.lg
127 | ?
128 | :
129 | }
130 | |
131 | Large devices and UP |
132 |
133 |
134 |
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 |
--------------------------------------------------------------------------------