├── .babelrc
├── .eslintrc
├── .gitignore
├── README.md
├── circle.yml
├── index.js
├── package.json
├── run-webpack-server.js
├── src
├── client
│ ├── app
│ │ ├── App.js
│ │ ├── app.scss
│ │ └── react-logo.png
│ ├── auth
│ │ ├── LoginApp.js
│ │ ├── LoginComponent.js
│ │ ├── RegisterApp.js
│ │ └── RegisterComponent.js
│ ├── counter
│ │ └── Counter.js
│ ├── createRoutes.js
│ ├── devTools.js
│ ├── event
│ │ ├── CreateEvent.js
│ │ └── Event.js
│ └── index.js
├── common
│ ├── app
│ │ └── reducers.js
│ ├── auth
│ │ ├── actions.js
│ │ └── reducers.js
│ ├── components
│ │ └── RouterHandler.js
│ ├── configureStore.js
│ ├── counter
│ │ ├── actions.js
│ │ ├── api.js
│ │ ├── counter.spec.js
│ │ └── reducers.js
│ ├── event
│ │ ├── actions.js
│ │ └── reducers.js
│ ├── fetchComponentData.js
│ └── translations.js
└── server
│ ├── index.js
│ └── server.js
├── translate
├── index.js
└── translate.js
├── webpack-dev-server.js
├── webpack-isomorphic-tools-configuration.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"],
3 | "plugins": [
4 | "transform-class-properties",
5 | "transform-object-rest-spread",
6 | "syntax-async-functions",
7 | "transform-async-to-generator",
8 | ["react-intl", {
9 | "messagesDir": "./build/messages/"
10 | }]
11 | ]
12 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser" : "babel-eslint",
3 | "plugins": [
4 | "import"
5 | ],
6 | "extends" : ["airbnb"],
7 | "rules": {
8 | "react/prop-types": 0,
9 | "react/jsx-no-bind": 0
10 | },
11 | "globals": {
12 | "require": false,
13 | "it": false,
14 | "describe": false,
15 | },
16 | "settings": {
17 | "import/ignore": [
18 | "node_modules",
19 | "\\.json$"
20 | ],
21 | "import/parser": "babel-eslint",
22 | "import/resolve": {
23 | "extensions": [
24 | ".js",
25 | ".jsx",
26 | ".json"
27 | ]
28 | }
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | build/
3 | node_modules/
4 | npm-debug.log
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## General
2 |
3 | This project is a work in progress. It's a project that serves as a minimal starting point for my React apps.
4 |
5 | [](https://circleci.com/gh/jvorcak/universal-react-kit)
6 | [](https://david-dm.org/jvorcak/universal-react-kit)
7 |
8 | ## Most important features
9 | - Store management with Redux
10 | - Server rendering
11 | - Tests
12 | - Routing
13 | - i18n support
14 | - Redux dev tools
15 | - Checking for Airbnb JavaScript Style Guide using eslint
16 | - Firebase integration
17 | - ~~E2E tests~~ (coming soon)
18 | - ~~React native support~~ (coming soon)
19 |
20 | ## It currently includes/supports:
21 | - React
22 | - React-router
23 | - Babel 6
24 | - Redux
25 | - Express with server rendering support
26 | - Immutable.JS (mostly used for Redux store)
27 | - Webpack
28 | - Redux devtools
29 | - Hot reload
30 | - Chai
31 | - React Helmet
32 | - Mocha
33 | - React-intl 2
34 | - Eslint
35 | - Firebase
36 |
37 | As written in a headline, it's used to understand how things fit toghether, so there are probably a lot of things that should be done differently. If you see any of those, please feel free to comment.
38 |
39 | # Recommended setup/Prerequisities
40 |
41 | Recommended setup is to use Node 5 with npm 3. It hasn't been properly tested with previous versions.
42 |
43 | # Instalation
44 |
45 | ```
46 | npm install
47 | npm start
48 | ```
49 |
50 | # Runtime
51 |
52 | `http://localhost:3000/` main page
53 | `http://localhost:3000/counter` shows a simple counter example with server rendering
54 | `http://localhost:3000/event` this page does nothing usefull :) it just serves to test a different module in the app.
55 |
56 | # Commands
57 |
58 | - Run `npm run tests` to execute tests.
59 | - Run `npm run eslint` to check a quality of a source code.
60 | - Run `npm run translate` to combine translated messages into one flat object.
61 |
62 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 5.3.0
4 | post:
5 | - npm install -g npm@3
6 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('./src/client');
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-universal-example",
3 | "version": "0.0.0",
4 | "description": "An example of a universally-rendered Redux application",
5 | "scripts": {
6 | "start": "concurrent --kill-others \"npm run start-dev-server\" \"npm run start-server\"",
7 | "start-dev-server": "node ./run-webpack-server.js",
8 | "start-server": "node src/server/index.js",
9 | "translate": "node translate",
10 | "test": "mocha --compilers js:babel-core/register ./src/**/*.spec.js",
11 | "eslint": "eslint src"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/rackt/redux.git"
16 | },
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/rackt/redux/issues"
20 | },
21 | "homepage": "http://rackt.github.io/redux",
22 | "dependencies": {
23 | "css-loader": "^0.23.1",
24 | "express": "^4.13.3",
25 | "firebase": "^2.4.0",
26 | "firebase-promisified": "0.0.1",
27 | "immutable": "^3.7.6",
28 | "react": "^0.14.0",
29 | "react-dom": "^0.14.0",
30 | "react-helmet": "^2.3.1",
31 | "react-intl": "^2.0.0-beta-2",
32 | "react-pure-render": "^1.0.2",
33 | "react-redux": "^4.0.0",
34 | "react-router": "^2.0.0",
35 | "redux": "^3.0.0",
36 | "redux-form": "^4.1.16",
37 | "redux-logger": "^2.3.1",
38 | "redux-promise-middleware": "^2.3.3",
39 | "rx": "^4.0.7",
40 | "serve-static": "^1.10.0",
41 | "shortid": "^2.2.4",
42 | "url-loader": "^0.5.7",
43 | "webpack-isomorphic-tools": "^2.2.26"
44 | },
45 | "devDependencies": {
46 | "babel-core": "^6.4.5",
47 | "babel-eslint": "^4.1.8",
48 | "babel-loader": "^6.2.1",
49 | "babel-plugin-react-intl": "^2.1.1",
50 | "babel-plugin-react-transform": "^2.0.0",
51 | "babel-plugin-syntax-async-functions": "^6.3.13",
52 | "babel-plugin-transform-async-to-generator": "^6.4.6",
53 | "babel-plugin-transform-class-properties": "^6.4.0",
54 | "babel-plugin-transform-object-rest-spread": "^6.3.13",
55 | "babel-polyfill": "^6.3.14",
56 | "babel-preset-es2015": "^6.3.13",
57 | "babel-preset-react": "^6.3.13",
58 | "babel-preset-stage-2": "^6.3.13",
59 | "babel-runtime": "^6.3.19",
60 | "chai": "^3.5.0",
61 | "chai-immutable": "^1.5.3",
62 | "concurrently": "^1.0.0",
63 | "css-loader": "^0.23.1",
64 | "eslint": "^1.10.3",
65 | "eslint-config-airbnb": "^5.0.0",
66 | "eslint-plugin-import": "^0.12.1",
67 | "eslint-plugin-react": "^3.16.1",
68 | "glob": "^6.0.4",
69 | "json-loader": "^0.5.4",
70 | "mkdirp": "^0.5.1",
71 | "mocha": "^2.4.5",
72 | "node-sass": "^3.4.2",
73 | "react-hot-loader": "^1.3.0",
74 | "react-transform-hmr": "^1.0.0",
75 | "redux-devtools": "^3.0.1",
76 | "redux-devtools-dock-monitor": "^1.0.1",
77 | "redux-devtools-log-monitor": "^1.0.1",
78 | "sass-loader": "^3.1.2",
79 | "style-loader": "^0.13.0",
80 | "webpack": "^1.11.0",
81 | "webpack-dev-middleware": "^1.2.0",
82 | "webpack-hot-middleware": "^2.2.0"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/run-webpack-server.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('./webpack-dev-server');
3 |
--------------------------------------------------------------------------------
/src/client/app/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router';
3 | import { bindActionCreators } from 'redux';
4 | import { connect } from 'react-redux';
5 | import { Map } from 'immutable';
6 | import DevTools from '../devTools';
7 | import * as counterActions from '../../common/counter/actions';
8 | import * as eventActions from '../../common/event/actions';
9 | import * as authActions from '../../common/auth/actions';
10 | import RouterHandler from '../../common/components/RouterHandler';
11 |
12 | import styles from './app.scss';
13 |
14 | function mapStateToProps(state) {
15 | return {
16 | ...state,
17 | };
18 | }
19 |
20 | const actions = [
21 | counterActions,
22 | eventActions,
23 | authActions,
24 | ];
25 |
26 | function mapDispatchToProps(dispatch) {
27 | const creators = new Map()
28 | .merge(...actions)
29 | .filter(value => typeof value === 'function')
30 | .toObject();
31 |
32 | return {
33 | actions: bindActionCreators(creators, dispatch),
34 | };
35 | }
36 |
37 | class App extends Component {
38 |
39 | componentWillMount() {
40 | const { actions: { checkAuth } } = this.props;
41 | checkAuth();
42 | }
43 |
44 | render() {
45 | // to demonstrate webpack-isomorphic-tools
46 | const imagePath = require('./react-logo.png');
47 |
48 | const { auth, actions: {logOut} } = this.props;
49 |
50 | const avatarURL = auth.getIn(["loggedIn", "password", "profileImageURL"]);
51 |
52 | return (
53 |
54 | universal-react-kit
55 |
56 |
64 |
65 |
66 |
67 |
68 |
);
69 | }
70 |
71 | }
72 |
73 | export default connect(mapStateToProps, mapDispatchToProps)(App);
74 |
--------------------------------------------------------------------------------
/src/client/app/app.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background: #61DAFB;
3 | }
4 |
5 | :local(.app) {
6 | $color: white;
7 | padding: 10px;
8 | background-color: $color;
9 | }
10 |
--------------------------------------------------------------------------------
/src/client/app/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jvorcak/universal-react-kit/c0af3dff1d95716e14b3a2342b7981754ca1caa1/src/client/app/react-logo.png
--------------------------------------------------------------------------------
/src/client/auth/LoginApp.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { LoginFormWrapper } from './LoginComponent';
4 |
5 | class LoginApp extends Component {
6 |
7 | componentWillMount() {
8 | //this.context.router.push('/register');
9 | }
10 |
11 | render() {
12 | const { auth: loggedIn } = this.props;
13 |
14 | return (
15 |
16 |
Login
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | LoginApp.contextTypes = {
24 | router: React.PropTypes.object.isRequired
25 | };
26 |
27 | export default LoginApp;
28 |
--------------------------------------------------------------------------------
/src/client/auth/LoginComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm } from 'redux-form';
3 |
4 | const fields = ['email', 'password'];
5 |
6 | class LoginForm extends Component {
7 |
8 | handleSubmit(e) {
9 | e.preventDefault();
10 | const { actions: { login }, fields: { email, password } } = this.props;
11 | login(email.value, password.value);
12 | }
13 |
14 | render() {
15 | const { actions: {
16 | loginWithFacebook,
17 | loginWithTwitter,
18 | }, fields: { email, password } } = this.props;
19 |
20 | return (
21 |
22 |
23 |
24 |
39 |
40 | );
41 | }
42 | }
43 |
44 | export const LoginFormWrapper = reduxForm({
45 | form: 'login',
46 | fields,
47 | })(LoginForm);
48 |
--------------------------------------------------------------------------------
/src/client/auth/RegisterApp.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import { RegisterFormWrapper } from './RegisterComponent';
4 |
5 | export default class RegisterApp extends Component {
6 |
7 | render() {
8 | return (
9 |
10 |
Register
11 |
12 |
13 | );
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/client/auth/RegisterComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { reduxForm } from 'redux-form';
3 |
4 | const fields = ['email', 'password'];
5 |
6 | class RegisterForm extends Component {
7 |
8 | handleSubmit(e) {
9 | e.preventDefault();
10 | const { actions: { register }, fields: { email, password } } = this.props;
11 | register(email.value, password.value);
12 | }
13 |
14 | render() {
15 | const { fields: { email, password } } = this.props;
16 |
17 | return (
18 |
35 | );
36 | }
37 | }
38 |
39 | export const RegisterFormWrapper = reduxForm({
40 | form: 'registration',
41 | fields,
42 | })(RegisterForm);
43 |
--------------------------------------------------------------------------------
/src/client/counter/Counter.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import Helmet from 'react-helmet';
3 | import { FormattedMessage, defineMessages, injectIntl, intlShape } from 'react-intl';
4 |
5 | import { incrementAsync as asyncAction } from '../../common/counter/actions';
6 |
7 | const messages = defineMessages({
8 | counterTitle: {
9 | id: 'counterTitle',
10 | defaultMessage: 'Counter title',
11 | },
12 | });
13 |
14 | class Counter extends Component {
15 |
16 | static propTypes = {
17 | actions: React.PropTypes.shape({
18 | increment: PropTypes.func.isRequired,
19 | incrementIfOdd: PropTypes.func.isRequired,
20 | incrementAsync: PropTypes.func.isRequired,
21 | decrement: PropTypes.func.isRequired,
22 | }),
23 | counter: React.PropTypes.shape({
24 | counter: PropTypes.number.isRequired,
25 | }),
26 | intl: intlShape.isRequired,
27 | };
28 |
29 | render() {
30 | const {
31 | actions: { increment, incrementIfOdd, incrementAsync, decrement },
32 | counter: {
33 | counter,
34 | },
35 | } = this.props;
36 |
37 | const { formatMessage } = this.props.intl;
38 |
39 | return (
40 |
41 |
42 | : {counter}
47 |
51 | {' '}
52 |
53 | {' '}
54 |
55 | {' '}
56 |
57 | {' '}
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | const wrappedCounter = injectIntl(Counter);
65 | /**
66 | * Here we define all async actions that needs
67 | * to be completed before this class is rendered
68 | * on a server
69 | */
70 | wrappedCounter.needs = [
71 | asyncAction,
72 | ];
73 | export default wrappedCounter;
74 |
--------------------------------------------------------------------------------
/src/client/createRoutes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import App from './app/App';
3 | import Counter from './counter/Counter';
4 | import EventsApp from './event/Event';
5 | import { EventsCreateApp } from './event/CreateEvent';
6 | import { Route } from 'react-router';
7 | import RegisterApp from './auth/RegisterApp';
8 | import LoginApp from './auth/LoginApp';
9 |
10 | // simple NotFound component
11 | const NotFound = () => Not found
;
12 |
13 | export default function createRoutes() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/src/client/devTools.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // Exported from redux-devtools
4 | import { createDevTools } from 'redux-devtools';
5 |
6 | // Monitors are separate packages, and you can make a custom one
7 | import LogMonitor from 'redux-devtools-log-monitor';
8 | import DockMonitor from 'redux-devtools-dock-monitor';
9 |
10 | // createDevTools takes a monitor and produces a DevTools component
11 | const DevTools = createDevTools(
12 | // Monitors are individually adjustable with props.
13 | // Consult their repositories to learn about those props.
14 | // Here, we put LogMonitor inside a DockMonitor.
15 |
16 |
17 |
18 | );
19 |
20 | export default DevTools;
21 |
--------------------------------------------------------------------------------
/src/client/event/CreateEvent.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { reduxForm } from 'redux-form';
3 | import { injectIntl } from 'react-intl';
4 |
5 | export const fields = ['name', 'location'];
6 |
7 | class CreateEvent extends Component {
8 |
9 | static propTypes = {
10 | fields: PropTypes.object.isRequired,
11 | resetForm: PropTypes.func.isRequired,
12 | submitting: PropTypes.bool.isRequired,
13 | };
14 |
15 | saveEventHandler = (e) => {
16 | e.preventDefault();
17 | const { resetForm, actions: { saveEvent }, fields } = this.props;
18 | saveEvent(fields);
19 | resetForm();
20 | };
21 |
22 | render() {
23 | const { fields: { name, location } } = this.props;
24 |
25 | return (
26 |
27 |
Create event
28 |
43 |
44 | );
45 | }
46 | }
47 |
48 | const CreateEventWrappedIntl = injectIntl(CreateEvent);
49 | const CreateEventWrappedReduxForm = reduxForm({
50 | form: 'newEvent',
51 | fields,
52 | })(CreateEventWrappedIntl);
53 |
54 | export class EventsCreateApp extends Component {
55 | render() {
56 | return ;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/client/event/Event.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { injectIntl } from 'react-intl';
3 | import { Link } from 'react-router';
4 |
5 | import { getAllEvents as asyncAction } from '../../common/event/actions';
6 |
7 | class EventsApp extends Component {
8 |
9 | static propTypes = {
10 | event: React.PropTypes.shape({
11 | events: PropTypes.object.isRequired,
12 | }),
13 | actions: React.PropTypes.shape({
14 | getAllEvents: PropTypes.func.isRequired,
15 | }),
16 | };
17 |
18 | componentDidMount() {
19 | const { actions: { getAllEvents }, event: { events } } = this.props;
20 |
21 | // if no events are in the component, let's load them
22 | if (!events) {
23 | getAllEvents();
24 | }
25 | }
26 |
27 | render() {
28 | const { event } = this.props;
29 |
30 | const events = event.get('events').toList().toJS();
31 |
32 | return (
33 |
34 |
Create event
35 | Events
36 |
37 | {events.map(event =>
38 | - {event.name}
39 | )}
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | const wrappedEventsApp = injectIntl(EventsApp);
47 | /**
48 | * Here we define all async actions that needs
49 | * to be completed before this class is rendered
50 | * on a server
51 | */
52 | wrappedEventsApp.needs = [
53 | asyncAction,
54 | ];
55 | export default wrappedEventsApp;
56 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Router } from 'react-router';
3 | import { render } from 'react-dom';
4 | import { Provider } from 'react-redux';
5 | import configureStore from '../common/configureStore';
6 | import createRoutes from './createRoutes';
7 | import { browserHistory } from 'react-router';
8 | import { IntlProvider } from 'react-intl';
9 |
10 | const initialState = window.__INITIAL_STATE__;
11 | const store = configureStore(initialState);
12 | const rootElement = document.getElementById('app');
13 | const routes = createRoutes();
14 |
15 | const { locale, messages } = window.__I18N__;
16 |
17 | render(
18 |
19 |
20 |
21 | {routes}
22 |
23 |
24 | , rootElement
25 | );
26 |
--------------------------------------------------------------------------------
/src/common/app/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import counter from '../counter/reducers';
3 | import event from '../event/reducers';
4 | import auth from '../auth/reducers';
5 | import { reducer as formReducer } from 'redux-form';
6 |
7 | const rootReducer = combineReducers({
8 | counter,
9 | event,
10 | auth,
11 | form: formReducer,
12 | });
13 |
14 | export default rootReducer;
15 |
--------------------------------------------------------------------------------
/src/common/auth/actions.js:
--------------------------------------------------------------------------------
1 | export const REGISTER = 'REGISTER';
2 | export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
3 | export const REGISTER_ERROR = 'REGISTER_ERROR';
4 |
5 | export const LOGOUT = 'LOGOUT';
6 |
7 | export const LOGIN = 'LOGIN';
8 | export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
9 | export const LOGIN_ERROR = 'LOGIN_ERROR';
10 |
11 | export const LOGIN_WITH_FACEBOOK = 'LOGIN_WITH_FACEBOOK';
12 | export const LOGIN_WITH_FACEBOOK_SUCCESS = 'LOGIN_WITH_FACEBOOK_SUCCESS';
13 | export const LOGIN_WITH_FACEBOOK_ERROR = 'LOGIN_WITH_FACEBOOK_ERROR';
14 |
15 | export const LOGIN_WITH_TWITTER = 'LOGIN_WITH_TWITTER';
16 | export const LOGIN_WITH_TWITTER_SUCCESS = 'LOGIN_WITH_TWITTER_SUCCESS';
17 | export const LOGIN_WITH_TWITTER_ERROR = 'LOGIN_WITH_TWITTER_ERROR';
18 |
19 | export function register(email, password) {
20 | return ({ firebase }) => Object({
21 | type: REGISTER,
22 | payload: {
23 | promise: firebase
24 | .createUser({
25 | email,
26 | password,
27 | }),
28 | },
29 | });
30 | }
31 |
32 | export function checkAuth() {
33 | return ({firebase}) => Object({
34 | type: LOGIN,
35 | payload: {
36 | promise: new Promise((resolve, reject) =>
37 | firebase.onAuth(data => data === null ? reject() : resolve(data))
38 | )
39 | }
40 | });
41 | }
42 |
43 | export function logOut() {
44 | return ({firebase}) => {
45 | firebase.unauth();
46 | return {
47 | type: LOGOUT
48 | };
49 | };
50 | }
51 |
52 | export function login(email, password) {
53 |
54 | // find a suitable name based on the meta info given by each provider
55 | function getName(authData) {
56 | switch (authData.provider) {
57 | case 'password':
58 | return authData.password.email.replace(/@.*/, '');
59 | case 'twitter':
60 | return authData.twitter.displayName;
61 | case 'facebook':
62 | return authData.facebook.displayName;
63 | }
64 | }
65 |
66 | return ({ firebase }) => Object({
67 | type: LOGIN,
68 | payload: {
69 | promise: firebase
70 | .authWithPassword({
71 | email,
72 | password,
73 | })
74 | .then(authData => {
75 | if (authData) {
76 | // save the user's profile into the database so we can list users,
77 | // use them in Security and Firebase Rules, and show profiles
78 | firebase.child("users").child(authData.uid).set({
79 | provider: authData.provider,
80 | name: getName(authData)
81 | });
82 | }
83 | return authData;
84 | })
85 | ,
86 | },
87 | });
88 | }
89 |
90 | export function loginWithFacebook() {
91 | return ({ firebase }) => Object({
92 | type: LOGIN_WITH_TWITTER,
93 | payload: {
94 | promise: firebase
95 | .authWithOAuthPopup('facebook'),
96 | },
97 | });
98 | }
99 |
100 |
101 | export function loginWithTwitter() {
102 | return ({ firebase }) => Object({
103 | type: LOGIN_WITH_TWITTER,
104 | payload: {
105 | promise: firebase
106 | .authWithOAuthPopup('twitter'),
107 | },
108 | });
109 | }
110 |
--------------------------------------------------------------------------------
/src/common/auth/reducers.js:
--------------------------------------------------------------------------------
1 | import Immutable, { Record } from 'immutable';
2 | import { REGISTER_SUCCESS, LOGIN_SUCCESS, LOGOUT } from './actions';
3 |
4 | export const InitialState = new Record({
5 | loggedIn: 0,
6 | });
7 | const initialState = new InitialState();
8 |
9 | const revive = ({ loggedIn }) => initialState.merge({
10 | loggedIn,
11 | });
12 |
13 | export default function authReducer(state = initialState, action) {
14 | if (!(state instanceof InitialState)) return revive(state);
15 |
16 | switch (action.type) {
17 | case LOGIN_SUCCESS:
18 | return state.set('loggedIn', Immutable.fromJS(action.payload));
19 | case LOGOUT:
20 | return state.set('loggedIn', null);
21 | default:
22 | return state;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/common/components/RouterHandler.js:
--------------------------------------------------------------------------------
1 | import Component from 'react-pure-render/component';
2 | import React, { PropTypes } from 'react';
3 |
4 | // RouterHandler is back since suggested solution via React.cloneElement sucks.
5 | // https://github.com/rackt/react-router/blob/master/UPGRADE_GUIDE.md#routehandler
6 | // This is just syntax sugar for react-router 1.0.0 filtering children in props.
7 | // https://github.com/este/este/issues/535
8 | // Note React does not validate propTypes that are specified via cloneElement.
9 | // It is recommended to make such propTypes optional.
10 | // https://github.com/facebook/react/issues/4494#issuecomment-125068868
11 | export default class RouterHandler extends Component {
12 |
13 | static propTypes = {
14 | children: PropTypes.object,
15 | };
16 |
17 | render() {
18 | const { children } = this.props;
19 | // No children means nothing to render.
20 | if (!children) return null;
21 |
22 | // That makes nested routes working.
23 | const propsWithoutChildren = { ...this.props };
24 | delete propsWithoutChildren.children;
25 |
26 | return React.cloneElement(children, propsWithoutChildren);
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/common/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import createLogger from 'redux-logger';
3 | import rootReducer from './app/reducers';
4 | import DevTools from '../client/devTools';
5 | import promiseMiddleware from 'redux-promise-middleware';
6 | import Firebase from 'firebase';
7 | import shortid from 'shortid';
8 |
9 | // adds Rx and Promises to the Firebase prototype
10 |
11 | export default function configureStore(initialState) {
12 |
13 | const firebase = new Firebase('https://fiery-inferno-4599.firebaseio.com/');
14 |
15 | // Inspired by https://github.com/este/este
16 | // TODO Maybe I misunderstood, but it fails if an actions returns undefined.
17 | const injectMiddleware = deps => store => next => action =>
18 | next(typeof action === 'function'
19 | ? action({...deps, store})
20 | : action
21 | );
22 | const logger = createLogger({ logger: console });
23 |
24 | const store = compose(
25 | applyMiddleware(
26 | injectMiddleware({
27 | firebase,
28 | getUid: () => shortid.generate(),
29 | }),
30 | logger,
31 | promiseMiddleware({
32 | promiseTypeSuffixes: ['START', 'SUCCESS', 'ERROR'],
33 | })
34 | ),
35 | DevTools.instrument()
36 | )(createStore)(rootReducer, initialState);
37 |
38 | if (module.hot) {
39 | // Enable Webpack hot module replacement for reducers
40 | module.hot.accept('./app/reducers', () => {
41 | const nextRootReducer = require('./app/reducers');
42 | store.replaceReducer(nextRootReducer);
43 | });
44 | }
45 |
46 | return store;
47 | }
48 |
--------------------------------------------------------------------------------
/src/common/counter/actions.js:
--------------------------------------------------------------------------------
1 | export const SET_COUNTER = 'SET_COUNTER';
2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER';
3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER';
4 | export const INCREMENT_COUNTER_SUCCESS = 'INCREMENT_COUNTER_SUCCESS';
5 |
6 | export function set(value) {
7 | return {
8 | type: SET_COUNTER,
9 | payload: value,
10 | };
11 | }
12 |
13 | export function increment() {
14 | return {
15 | type: INCREMENT_COUNTER,
16 | };
17 | }
18 |
19 | export function decrement() {
20 | return {
21 | type: DECREMENT_COUNTER,
22 | };
23 | }
24 |
25 | export function incrementIfOdd() {
26 | return ({store: { getState, dispatch }}) => {
27 | const { counter: { counter } } = getState();
28 |
29 | // TODO - write a better middleware so that one
30 | // doesn't need to return anything from an action
31 | if (counter % 2 === 0) {
32 | return {
33 | type: ''
34 | };
35 | }
36 |
37 | return {
38 | type: INCREMENT_COUNTER
39 | }
40 | };
41 | }
42 |
43 | export function incrementAsync(delay = 1000) {
44 | return {
45 | type: INCREMENT_COUNTER,
46 | payload: {
47 | promise: new Promise(resolve => {
48 | setTimeout(() => {
49 | function getRandomInt(min, max) {
50 | return Math.floor(Math.random() * (max - min)) + min;
51 | }
52 | resolve(getRandomInt(1, 100));
53 | }, delay);
54 | }),
55 | },
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/common/counter/api.js:
--------------------------------------------------------------------------------
1 | function getRandomInt(min, max) {
2 | return Math.floor(Math.random() * (max - min)) + min;
3 | }
4 |
5 | export const fetchCounter = async () =>
6 | new Promise(resolve => {
7 | setTimeout(() => {
8 | resolve(getRandomInt(1, 100));
9 | }, 500);
10 | });
11 |
--------------------------------------------------------------------------------
/src/common/counter/counter.spec.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai';
2 | import chaiImmutable from 'chai-immutable';
3 | import * as actions from './actions';
4 | import counterReducer, { InitialState } from './reducers';
5 |
6 | const initialState = new InitialState();
7 | chai.use(chaiImmutable);
8 |
9 | describe('counter reducer', () => {
10 | it('should return the initial state', () => {
11 | expect(
12 | counterReducer(undefined, {})
13 | ).to.equal(initialState);
14 | });
15 |
16 | it('should set a counter', () => {
17 | const afterState = initialState.set('counter', 4);
18 |
19 | expect(
20 | counterReducer(initialState, {
21 | type: actions.SET_COUNTER,
22 | payload: 4,
23 | })).to.equal(afterState);
24 | });
25 |
26 | it('should increment a counter', () => {
27 | const initialStateLocal = initialState.set('counter', 4);
28 | const afterState = initialState.set('counter', 5);
29 |
30 | expect(
31 | counterReducer(initialStateLocal, {
32 | type: actions.INCREMENT_COUNTER,
33 | })).to.equal(afterState);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/common/counter/reducers.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable';
2 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER,
3 | INCREMENT_COUNTER_SUCCESS } from './actions';
4 |
5 | export const InitialState = new Record({
6 | counter: 0,
7 | });
8 | const initialState = new InitialState();
9 |
10 | const revive = ({ counter }) => initialState.merge({
11 | counter,
12 | });
13 |
14 | export default function counterReducer(state = initialState, action) {
15 | if (!(state instanceof InitialState)) return revive(state);
16 |
17 | switch (action.type) {
18 | case SET_COUNTER:
19 | return state.set('counter', action.payload);
20 | case INCREMENT_COUNTER:
21 | return state.set('counter', state.get('counter') + 1);
22 | case DECREMENT_COUNTER:
23 | return state.set('counter', state.get('counter') - 1);
24 | case INCREMENT_COUNTER_SUCCESS:
25 | return state.set('counter', state.get('counter') + action.payload);
26 | default:
27 | return state;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/common/event/actions.js:
--------------------------------------------------------------------------------
1 | export const GET_ALL_EVENTS = 'GET_ALL_EVENTS';
2 | export const GET_ALL_EVENTS_SUCCESS = 'GET_ALL_EVENTS_SUCCESS';
3 | export const GET_ALL_EVENTS_ERROR = 'GET_ALL_EVENTS_ERROR';
4 |
5 | export const SAVE_EVENT = 'SAVE_EVENT';
6 | export const SAVE_EVENT_SUCCESS = 'SAVE_EVENT_SUCCESS';
7 | export const SAVE_EVENT_ERROR = 'SAVE_EVENT_ERROR';
8 |
9 | export function saveEvent({name, location}) {
10 | return ({firebase, getUid}) => {
11 | return {
12 | type: SAVE_EVENT,
13 | payload: {
14 | promise: firebase
15 | .child('events')
16 | .child(getUid())
17 | .set({
18 | name: name.value,
19 | location: location.value,
20 | })
21 | }
22 | }
23 | };
24 | }
25 |
26 | export function getAllEvents() {
27 | return ({ firebase }) => Object({
28 | type: GET_ALL_EVENTS,
29 | payload: {
30 | promise: firebase
31 | .child('events')
32 | .once('value')
33 | .then(x => x.val()),
34 | },
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/common/event/reducers.js:
--------------------------------------------------------------------------------
1 | import Immutable, { Record } from 'immutable';
2 | import { GET_ALL_EVENTS_SUCCESS, SAVE_EVENT } from './actions';
3 |
4 | export const InitialState = new Record({
5 | events: {},
6 | });
7 | const initialState = new InitialState();
8 |
9 | const revive = ({ events }) => initialState.merge({
10 | events,
11 | });
12 |
13 | export default function counterReducer(state = initialState, action) {
14 | if (!(state instanceof InitialState)) return revive(state);
15 |
16 | switch (action.type) {
17 | case GET_ALL_EVENTS_SUCCESS:
18 | return state.set('events', Immutable.fromJS(action.payload));
19 | case SAVE_EVENT:
20 | return state.setIn(['events', 'saving'], true);
21 | default:
22 | return state;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/common/fetchComponentData.js:
--------------------------------------------------------------------------------
1 | export default function fetchComponentData(dispatch, components, params) {
2 | const needs = components.reduce((prev, current) =>
3 | (current.needs || [])
4 | .concat((current.WrappedComponent ? current.WrappedComponent.needs : []) || [])
5 | .concat(prev)
6 | , []);
7 |
8 | const promises = needs
9 | .map(need => dispatch(need(params)))
10 | .map(action => action.payload.promise);
11 | return Promise.all(promises);
12 | }
13 |
--------------------------------------------------------------------------------
/src/common/translations.js:
--------------------------------------------------------------------------------
1 | import { sync as globSync } from 'glob';
2 | import * as path from 'path';
3 | import { readFileSync } from 'fs';
4 |
5 | const translations = globSync('./build/lang/*.json')
6 | .map((filename) => [
7 | path.basename(filename, '.json'),
8 | readFileSync(filename, 'utf8'),
9 | ])
10 | .map(([locale, file]) => [locale, JSON.parse(file)])
11 | .reduce((collection, [locale, messages]) => {
12 | const retCollection = {};
13 | retCollection[locale] = messages;
14 | return retCollection;
15 | }, {});
16 |
17 |
18 | export default translations;
19 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 |
3 | const WebpackIsomorphicTools = require('webpack-isomorphic-tools');
4 |
5 | // this must be equal to your Webpack configuration "context" parameter
6 | const projectBasePath = require('path').resolve(__dirname, '../..');
7 |
8 | // this global variable will be used later in express middleware
9 | global.webpack_isomorphic_tools = new WebpackIsomorphicTools(
10 | require('../../webpack-isomorphic-tools-configuration'))
11 | // enter development mode if needed
12 | // (you may also prefer to use a Webpack DefinePlugin variable)
13 | // .development(process.env.NODE_ENV === 'development')
14 | .development()
15 | // initializes a server-side instance of webpack-isomorphic-tools
16 | // (the first parameter is the base path for your project
17 | // and is equal to the "context" parameter of you Webpack configuration)
18 | // (if you prefer Promises over callbacks
19 | // you can omit the callback parameter
20 | // and then it will return a Promise instead)
21 | .server(projectBasePath)
22 | .then(() => {
23 | require('./server');
24 | })
25 | .catch(e => console.log(e));
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/server/server.js:
--------------------------------------------------------------------------------
1 | import Express from 'express';
2 | import React from 'react';
3 | import { renderToString } from 'react-dom/server';
4 | import { Provider } from 'react-redux';
5 | import { IntlProvider } from 'react-intl';
6 | import { match, RouterContext } from 'react-router';
7 | import Helmet from 'react-helmet';
8 |
9 | import configureStore from '../common/configureStore';
10 | import createRoutes from '../client/createRoutes';
11 | import translations from '../common/translations';
12 |
13 | import fetchComponentData from '../common/fetchComponentData';
14 |
15 | const app = new Express();
16 | const port = 3000;
17 | const routes = createRoutes();
18 |
19 | function renderFullPage(html, initialState, head, locale, messages) {
20 |
21 | return `
22 |
23 |
24 |
25 | ${head.title.toString()}
26 |
27 |
28 | ${html}
29 |
33 |
34 |
35 |
36 | `;
37 | }
38 |
39 | function handleRender(req, res) {
40 | return match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
41 | if (error) {
42 | res.status(500).end(error.message);
43 | } else if (redirectLocation) {
44 | res.redirect(302, redirectLocation.pathname + redirectLocation.search);
45 | } else if (renderProps) {
46 | const locale = req.query.locale || 'en-US';
47 | const messages = translations[locale];
48 | const store = configureStore();
49 |
50 | fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
51 | .then(() => {
52 | const html = renderToString(
53 |
54 |
55 |
56 |
57 |
58 | );
59 |
60 | const head = Helmet.rewind();
61 |
62 | // Grab the initial state from our Redux store
63 | const finalState = store.getState();
64 |
65 | // Send the rendered page back to the client
66 | res.end(renderFullPage(html, finalState, head, locale, messages));
67 | });
68 | } else {
69 | res.status(404).send('Not found.');
70 | }
71 | });
72 | }
73 |
74 | app.use(handleRender);
75 | app.listen(port, (error) => {
76 | /* eslint-disable no-console */
77 | if (error) {
78 | console.error(error);
79 | } else {
80 | console.info(`Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`);
81 | }
82 | });
83 |
--------------------------------------------------------------------------------
/translate/index.js:
--------------------------------------------------------------------------------
1 | require("babel-core/register");
2 | require("./translate");
--------------------------------------------------------------------------------
/translate/translate.js:
--------------------------------------------------------------------------------
1 | require("babel-core/register");
2 | import * as fs from 'fs';
3 | import {sync as globSync} from 'glob';
4 | import {sync as mkdirpSync} from 'mkdirp';
5 |
6 | const MESSAGES_PATTERN = './build/messages/**/*.json';
7 | const LANG_DIR = './build/lang/';
8 |
9 | // Aggregates the default messages that were extracted from the example app's
10 | // React components via the React Intl Babel plugin. An error will be thrown if
11 | // there are messages in different components that use the same `id`. The result
12 | // is a flat collection of `id: message` pairs for the app's default locale.
13 | let defaultMessages = globSync(MESSAGES_PATTERN)
14 | .map((filename) => fs.readFileSync(filename, 'utf8'))
15 | .map((file) => JSON.parse(file))
16 | .reduce((collection, descriptors) => {
17 | descriptors.forEach(({id, defaultMessage}) => {
18 | if (collection.hasOwnProperty(id)) {
19 | throw new Error(`Duplicate message id: ${id}`);
20 | }
21 |
22 | collection[id] = defaultMessage;
23 | });
24 |
25 | return collection;
26 | }, {});
27 |
28 |
29 | mkdirpSync(LANG_DIR);
30 | fs.writeFileSync(LANG_DIR + 'en-US.json', JSON.stringify(defaultMessages, null, 2));
31 |
--------------------------------------------------------------------------------
/webpack-dev-server.js:
--------------------------------------------------------------------------------
1 | import Express from 'express';
2 | import webpack from 'webpack';
3 | import webpackDevMiddleware from 'webpack-dev-middleware';
4 | import webpackHotMiddleware from 'webpack-hot-middleware';
5 | import webpackConfig from './webpack.config';
6 |
7 | const app = new Express();
8 | const port = 3001;
9 |
10 | // Use this middleware to set up hot module reloading via webpack.
11 | const compiler = webpack(webpackConfig);
12 |
13 | app.use(webpackDevMiddleware(compiler,
14 | {
15 | noInfo: true,
16 | publicPath: webpackConfig.output.publicPath,
17 | }
18 | ));
19 | app.use(webpackHotMiddleware(compiler));
20 |
21 | app.listen(port, (error) => {
22 | /* eslint-disable no-console */
23 | if (error) {
24 | console.error(error);
25 | } else {
26 | console.info(`Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`);
27 | }
28 | });
29 |
--------------------------------------------------------------------------------
/webpack-isomorphic-tools-configuration.js:
--------------------------------------------------------------------------------
1 | import plugin from 'webpack-isomorphic-tools/plugin';
2 |
3 | module.exports = {
4 | assets: {
5 | images: {
6 | extensions: ['jpeg', 'jpg', 'png', 'gif'],
7 | parser: plugin.url_loader_parser
8 | },
9 | fonts: {
10 | extensions: ['woff', 'woff2', 'ttf', 'eot'],
11 | parser: plugin.url_loader_parser
12 | },
13 | svg: {
14 | extension: 'svg',
15 | parser: plugin.url_loader_parser
16 | },
17 | styles: {
18 | extensions: ['css', 'sass', 'scss'],
19 | filter(module, regex, options, log) {
20 | return options.development
21 | ? plugin.style_loader_filter(module, regex, options, log)
22 | : regex.test(module.name);
23 | },
24 | path(module, options, log) {
25 | return options.development
26 | ? plugin.style_loader_path_extractor(module, options, log)
27 | : module.name;
28 | },
29 | parser(module, options, log) {
30 | return options.development
31 | ? plugin.css_modules_loader_parser(module, options, log)
32 | : module.source;
33 | }
34 | }
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var Webpack_isomorphic_tools_plugin = require('webpack-isomorphic-tools/plugin');
5 |
6 | var webpack_isomorphic_tools_plugin =
7 | // webpack-isomorphic-tools settings reside in a separate .js file
8 | // (because they will be used in the web server code too).
9 | new Webpack_isomorphic_tools_plugin(require('./webpack-isomorphic-tools-configuration'))
10 | // also enter development mode since it's a development webpack configuration
11 | // (see below for explanation)
12 | .development();
13 |
14 | module.exports = {
15 | devtool: 'inline-source-map',
16 | entry: [
17 | 'webpack-hot-middleware/client',
18 | './src/client/index.js'
19 | ],
20 | output: {
21 | path: path.join(__dirname, 'dist'),
22 | filename: 'bundle.js',
23 | publicPath: '/static/'
24 | },
25 | module: {
26 | loaders: [
27 | {
28 | test: /\.jsx?$/,
29 | loaders: ['react-hot', 'babel'],
30 | exclude: /node_modules/,
31 | include: __dirname
32 | },
33 | {
34 | test: /\.json$/,
35 | loader: 'json',
36 | exclude: /node_modules/,
37 | include: __dirname
38 | },
39 | {
40 | test: webpack_isomorphic_tools_plugin.regular_expression('images'),
41 | loader: 'url-loader?limit=10240', // any image below or equal to 10K will be converted to inline base64 instead
42 | },
43 | {
44 | test: /\.scss$/,
45 | loader: 'style!css?localIdentName=[name]__[local]___[hash:base64:5]!sass',
46 | exclude: /node_modules/,
47 | include: __dirname,
48 | },
49 | ]
50 | },
51 | plugins: [
52 | webpack_isomorphic_tools_plugin,
53 | new webpack.NoErrorsPlugin(),
54 | new webpack.optimize.OccurenceOrderPlugin(),
55 | new webpack.HotModuleReplacementPlugin(),
56 | ],
57 | };
58 |
59 |
--------------------------------------------------------------------------------