├── Procfile ├── .babelrc ├── .gitignore ├── logo.png ├── server ├── fav.ico ├── start.js ├── views │ └── index.ejs └── server.js ├── client ├── style │ ├── font │ │ └── SofiaProLight.otf │ ├── icon │ │ ├── fonts │ │ │ ├── hummingbird.eot │ │ │ ├── hummingbird.ttf │ │ │ ├── hummingbird.woff │ │ │ └── hummingbird.svg │ │ ├── styles.css │ │ └── icons-reference.html │ ├── components │ │ ├── slideout.styl │ │ ├── modal.styl │ │ ├── spinner.styl │ │ ├── button.styl │ │ ├── input.styl │ │ └── typography.styl │ ├── ui-kit.json │ └── app.styl ├── components │ ├── login │ │ ├── assets │ │ │ └── close.png │ │ └── login.jsx │ ├── checkbox │ │ └── checkbox.jsx │ ├── spinner │ │ ├── spinner.jsx │ │ └── path.js │ ├── button-velocity.jsx │ ├── side-menu │ │ ├── item.jsx │ │ └── side-menu.jsx │ ├── button-motion.jsx │ ├── modal-container │ │ └── modal-container.jsx │ ├── route-transition.jsx │ ├── toaster │ │ └── toaster.jsx │ └── header │ │ └── header.jsx ├── actions │ ├── viewport.js │ ├── side-menu.js │ ├── modals.js │ ├── toaster.js │ ├── fetch.js │ └── auth │ │ └── login.js ├── containers │ ├── home │ │ ├── home.module.js │ │ └── components │ │ │ └── home.jsx │ ├── nested-example │ │ ├── containers │ │ │ ├── nested-index │ │ │ │ ├── components │ │ │ │ │ └── nested-index.jsx │ │ │ │ └── nested-index.module.js │ │ │ └── nested-route │ │ │ │ ├── components │ │ │ │ └── nested-route.jsx │ │ │ │ └── nested-route.module.js │ │ ├── components │ │ │ └── nested-example.jsx │ │ └── nested-example.module.js │ ├── about │ │ ├── about.module.js │ │ └── components │ │ │ └── about.jsx │ ├── not-found │ │ ├── not-found.module.js │ │ └── components │ │ │ └── not-found.jsx │ ├── containers.module.js │ ├── fetch-example │ │ ├── fetch-example.module.js │ │ └── components │ │ │ └── fetch-example.jsx │ └── index │ │ └── index.jsx ├── globals │ ├── slideout.js │ └── style.js ├── store │ ├── firebase-conf.js │ └── configureStore.js ├── reducers │ ├── route-reducer.js │ ├── modals │ │ ├── modals.js │ │ └── modals.spec.js │ ├── toaster │ │ ├── toaster.js │ │ └── toaster.spec.js │ ├── fetched-data │ │ ├── fetched-data.js │ │ └── fetched-data.spec.js │ ├── viewport │ │ ├── viewport.js │ │ └── viewport.spec.js │ ├── reducers.js │ ├── side-menu │ │ ├── side-menu.js │ │ └── side-menu.spec.js │ └── login │ │ ├── login.js │ │ └── login.spec.js └── app.jsx ├── tests ├── tests-config.js └── karma-conf.js ├── webpack.development.config.js ├── webpack.production.config.js ├── webpack.config.js ├── package.json ├── .eslintrc └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: node server/server.js 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | 'presets': ['es2015', 'react'] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBRT/reactogo/HEAD/logo.png -------------------------------------------------------------------------------- /server/fav.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBRT/reactogo/HEAD/server/fav.ico -------------------------------------------------------------------------------- /client/style/font/SofiaProLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBRT/reactogo/HEAD/client/style/font/SofiaProLight.otf -------------------------------------------------------------------------------- /client/style/icon/fonts/hummingbird.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBRT/reactogo/HEAD/client/style/icon/fonts/hummingbird.eot -------------------------------------------------------------------------------- /client/style/icon/fonts/hummingbird.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBRT/reactogo/HEAD/client/style/icon/fonts/hummingbird.ttf -------------------------------------------------------------------------------- /client/components/login/assets/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBRT/reactogo/HEAD/client/components/login/assets/close.png -------------------------------------------------------------------------------- /client/style/icon/fonts/hummingbird.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PBRT/reactogo/HEAD/client/style/icon/fonts/hummingbird.woff -------------------------------------------------------------------------------- /client/actions/viewport.js: -------------------------------------------------------------------------------- 1 | export const SET_VIEWPORT = 'SET_VIEWPORT'; 2 | 3 | export const setViewport = width => ({type: SET_VIEWPORT, width}); 4 | 5 | -------------------------------------------------------------------------------- /client/containers/home/home.module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getComponent(location, cb) { 3 | require.ensure([], (require) => { 4 | cb(null, require('./components/home.jsx')); 5 | }); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /client/containers/nested-example/containers/nested-index/components/nested-index.jsx: -------------------------------------------------------------------------------- 1 | let NestedIndex = () =>
2 |

Nested Index Route

3 |
; 4 | 5 | export default NestedIndex; 6 | -------------------------------------------------------------------------------- /client/containers/nested-example/containers/nested-route/components/nested-route.jsx: -------------------------------------------------------------------------------- 1 | let NestedRoute = () =>
2 |

One of many nested routes

3 |
; 4 | 5 | export default NestedRoute; 6 | -------------------------------------------------------------------------------- /client/containers/about/about.module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'about', 3 | getComponent(location, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/about.jsx')); 6 | }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/containers/not-found/not-found.module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: '*', 3 | getComponent(location, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/not-found.jsx')); 6 | }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tests/tests-config.js: -------------------------------------------------------------------------------- 1 | // Content available at http://localhost:3001/webpack-dev-server/tests/tests.html 2 | var context = require.context('../client/', true, /.+\.spec\.js?$/); 3 | context.keys().forEach(context); 4 | 5 | module.exports = context; 6 | -------------------------------------------------------------------------------- /client/actions/side-menu.js: -------------------------------------------------------------------------------- 1 | export const OPEN_SIDE_MENU = 'OPEN_SIDE_MENU'; 2 | export const CLOSE_SIDE_MENU = 'CLOSE_SIDE_MENU'; 3 | 4 | export const openSideMenu = () => ({type: OPEN_SIDE_MENU}); 5 | export const closeSideMenu = () => ({type: CLOSE_SIDE_MENU}); 6 | -------------------------------------------------------------------------------- /client/containers/nested-example/containers/nested-index/nested-index.module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getComponent(location, cb) { 3 | require.ensure([], (require) => { 4 | cb(null, require('./components/nested-index.jsx')); 5 | }); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /client/actions/modals.js: -------------------------------------------------------------------------------- 1 | export const OPEN_MODAL_LOGIN = 'OPEN_MODAL_LOGIN'; 2 | export const CLOSE_MODAL_LOGIN = 'CLOSE_MODAL_LOGIN'; 3 | 4 | export const openModal = () => ({type: OPEN_MODAL_LOGIN}); 5 | export const closeModal = () => ({type: CLOSE_MODAL_LOGIN}); 6 | -------------------------------------------------------------------------------- /client/containers/containers.module.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | require('./about/about.module.js'), 3 | require('./fetch-example/fetch-example.module.js'), 4 | require('./nested-example/nested-example.module.js'), 5 | require('./not-found/not-found.module.js'), 6 | ]; 7 | -------------------------------------------------------------------------------- /client/containers/fetch-example/fetch-example.module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: '/fetch-example', 3 | getComponent(location, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/fetch-example.jsx')); 6 | }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/containers/nested-example/containers/nested-route/nested-route.module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'nested', 3 | getComponent(location, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('./components/nested-route.jsx')); 6 | }); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /client/globals/slideout.js: -------------------------------------------------------------------------------- 1 | import Slideout from 'slideout'; 2 | 3 | export const slideoutInst = () => new Slideout({ 4 | 'panel': document.getElementById('panel'), 5 | 'menu': document.getElementById('menu'), 6 | 'padding': UI.sideMenuWidth, 7 | 'tolerance': 70, 8 | 'side': 'right', 9 | }); 10 | -------------------------------------------------------------------------------- /client/store/firebase-conf.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | 3 | const config = { 4 | apiKey: process.env.FIREBASE_API_KEY, 5 | authDomain: process.env.FIREBASE_AUTH_DOMAIN, 6 | databaseURL: process.env.FIREBASE_DATABASE_URL, 7 | }; 8 | 9 | export const initializeApp = () => firebase.initializeApp(config); 10 | -------------------------------------------------------------------------------- /client/reducers/route-reducer.js: -------------------------------------------------------------------------------- 1 | import { LOCATION_CHANGE } from 'react-router-redux'; 2 | 3 | const initialState = Immutable.fromJS({ 4 | locationBeforeTransitions: null 5 | }); 6 | 7 | export default (state = initialState, action) => { 8 | if (action.type === LOCATION_CHANGE) { 9 | return state.merge({locationBeforeTransitions: action.payload}); 10 | } 11 | 12 | return state; 13 | }; 14 | -------------------------------------------------------------------------------- /webpack.development.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var devConfiguration = Object.assign({}, require('./webpack.config.js')); 3 | 4 | devConfiguration.module.loaders 5 | .filter(item => item.id === 'jsx') 6 | .map(item => item.loaders.unshift('react-hot')); 7 | 8 | devConfiguration.plugins.push( 9 | new webpack.HotModuleReplacementPlugin() 10 | ); 11 | 12 | module.exports = devConfiguration; 13 | -------------------------------------------------------------------------------- /client/containers/nested-example/components/nested-example.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router'; 2 | 3 | let NestedExample = (props) =>
4 |

Nested Example

5 | SUB NESTED ROUTE 6 | INDEX ROUTE 7 | {props.children} 8 |
; 9 | 10 | export default NestedExample; 11 | -------------------------------------------------------------------------------- /client/containers/not-found/components/not-found.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { connect } from 'react-redux'; 3 | 4 | let s = getStyle(); 5 | 6 | let NotFound = () => 7 | (
8 | Page not found 9 |
); 10 | 11 | 12 | function getStyle() { 13 | return { 14 | container: { 15 | }, 16 | }; 17 | } 18 | NotFound.displayName = 'NotFound'; 19 | 20 | export default connect((state) => ({viewport: state.get('viewport')}))(NotFound); 21 | -------------------------------------------------------------------------------- /client/reducers/modals/modals.js: -------------------------------------------------------------------------------- 1 | import { OPEN_MODAL_LOGIN, CLOSE_MODAL_LOGIN } from 'modals.js'; 2 | 3 | export const initialState = Immutable.Map({ 4 | isLoginModalOpen: false, 5 | }); 6 | 7 | export const modalsReducer = (state = initialState, action) => { 8 | switch(action.type) { 9 | case (OPEN_MODAL_LOGIN): return state.set('isLoginModalOpen', true); 10 | case (CLOSE_MODAL_LOGIN): return state.set('isLoginModalOpen', false); 11 | default: return state; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /client/style/components/slideout.styl: -------------------------------------------------------------------------------- 1 | .slideout-menu 2 | position: fixed 3 | right: 0 4 | top: 0 5 | bottom: 0 6 | right: 0 7 | z-index: 0 8 | width: sideMenuWidth px 9 | overflow-y: auto 10 | -webkit-overflow-scrolling: touch 11 | display: none 12 | 13 | .slideout-panel 14 | position:relative 15 | z-index: 1 16 | will-change: transform 17 | 18 | .slideout-open, 19 | .slideout-open body, 20 | .slideout-open .slideout-panel 21 | overflow: hidden 22 | 23 | .slideout-open .slideout-menu 24 | display: block 25 | -------------------------------------------------------------------------------- /client/reducers/toaster/toaster.js: -------------------------------------------------------------------------------- 1 | import { ADD_MESSAGE, REMOVE_MESSAGE} from 'toaster.js'; 2 | 3 | export const initialState = Immutable.List(); 4 | 5 | const getElementIndex = (tab, id) => 6 | tab.map(item => item.id).indexOf(id); 7 | 8 | export const toastersReducer = (state = initialState, action) => { 9 | switch(action.type) { 10 | case (ADD_MESSAGE): return state.push(action.message); 11 | case (REMOVE_MESSAGE): return state.delete(getElementIndex(state.toJS(), action.id)); 12 | default: return state; 13 | } 14 | }; 15 | 16 | -------------------------------------------------------------------------------- /client/containers/nested-example/nested-example.module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | path: 'nested-example', 3 | indexRoute: require('./containers/nested-index/nested-index.module.js'), 4 | getChildRoutes(location, cb) { 5 | require.ensure([], (require) => { 6 | cb(null, [ 7 | require('./containers/nested-route/nested-route.module.js'), 8 | ]); 9 | }); 10 | }, 11 | 12 | getComponent(location, cb) { 13 | require.ensure([], (require) => { 14 | cb(null, require('./components/nested-example.jsx')); 15 | }); 16 | } 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /client/actions/toaster.js: -------------------------------------------------------------------------------- 1 | export const ADD_MESSAGE = 'ADD_MESSAGE'; 2 | export const REMOVE_MESSAGE = 'REMOVE_MESSAGE'; 3 | 4 | const addMessage = (message, id, type) => ({ 5 | type: ADD_MESSAGE, 6 | message: { 7 | text: message, 8 | id: id, 9 | type: type ? type : 'success', 10 | }, 11 | }); 12 | 13 | export const removeMessage = id => ({type: REMOVE_MESSAGE, id: id}); 14 | 15 | export const pushMessage = (message, type) => dispatch => { 16 | const id = new Date().getTime(); 17 | dispatch(addMessage(message, id, type)); 18 | setTimeout(() => dispatch(removeMessage(id)), 6000); 19 | }; 20 | -------------------------------------------------------------------------------- /client/containers/about/components/about.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { connect } from 'react-redux'; 3 | 4 | let s = getStyle(); 5 | 6 | let About = () => 7 | (
8 | Build by PBRT, check it on  9 | Github 10 |
); 11 | 12 | 13 | function getStyle() { 14 | return { 15 | container: { 16 | textAlign: 'center', 17 | marginTop: 60, 18 | }, 19 | }; 20 | } 21 | About.displayName = 'About'; 22 | 23 | export default connect((state) => ({viewport: state.get('viewport')}))(About); 24 | 25 | -------------------------------------------------------------------------------- /client/style/ui-kit.json: -------------------------------------------------------------------------------- 1 | { 2 | "wait": "100", 3 | "breakpointM": "500", 4 | "breakpointT": "956", 5 | "breakpointD": "1280", 6 | "fontXS": "10", 7 | "fontSM": "12", 8 | "fontMD": "14", 9 | "fontLG": "18", 10 | "fontXL": "22", 11 | "fontXXL": "30", 12 | "headerHeight": "60", 13 | "sideMenuWidth": "170", 14 | "lightWhite": "#FFFFFF", 15 | "lightDark": "#2c3e50", 16 | "lightBlue": "#2980b9", 17 | "darkBlue": "#2980b9", 18 | "lightRed": "#e74c3c", 19 | "darkRed": "#c0392b", 20 | "lightGreen": "#2ecc71", 21 | "darkGreen": "#27ae60", 22 | "lightYellow": "#f1c40f", 23 | "darkYellow": "#f39c12" 24 | } 25 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | // Libraires 2 | var webpack = require('webpack'); 3 | 4 | // Configuration 5 | var config = Object.assign({}, require('./webpack.config.js'), { 6 | devtool: 'cheap-module-source-map', 7 | }); 8 | 9 | config.plugins = Array.prototype.concat( 10 | config.plugins, [ 11 | new webpack.optimize.DedupePlugin(), 12 | new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}), 13 | new webpack.DefinePlugin({ 14 | 'process.env': {NODE_ENV: '"production"'} 15 | }), 16 | ] 17 | ); 18 | 19 | // Block the build if lint errors 20 | config.eslint = {failOnError: true}; 21 | 22 | module.exports = config; 23 | 24 | -------------------------------------------------------------------------------- /client/actions/fetch.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_DATA = 'REQUEST_DATA'; 2 | export const RECEIVE_DATA = 'RECEIVE_DATA'; 3 | export const FAIL_DATA = 'FAIL_DATA'; 4 | 5 | const requestData = () => ({type: REQUEST_DATA}); 6 | const failData = err => ({type: FAIL_DATA, data: err}); 7 | const receiveData = data => ({type: RECEIVE_DATA, data: data}); 8 | 9 | // A delay of 3seconds is voluntary for seeing the spinner, again a simulation as an example ;) 10 | export const fetchData = () => dispatch => { 11 | dispatch(requestData()); 12 | return fetch('https://randomuser.me/api/', {method: 'get'}) 13 | .then(res => res.json()) 14 | .then(res => setTimeout(() => dispatch(receiveData(res)), 3000)) 15 | .catch(err => dispatch(failData(err))); 16 | }; 17 | -------------------------------------------------------------------------------- /client/reducers/fetched-data/fetched-data.js: -------------------------------------------------------------------------------- 1 | import { REQUEST_DATA, RECEIVE_DATA, FAIL_DATA } from 'fetch.js'; 2 | 3 | export const initialState = Immutable.Map({ 4 | isLoading: false, 5 | data: Immutable.Map(), 6 | error: '', 7 | }); 8 | 9 | export const fetchedDataReducer = (state = initialState, action) => { 10 | switch(action.type) { 11 | case (REQUEST_DATA): return state.set('isLoading', true); 12 | case (FAIL_DATA): return Immutable.fromJS({ 13 | isLoading: false, 14 | error: action.data, 15 | data: {}, 16 | }); 17 | case (RECEIVE_DATA): return Immutable.fromJS({ 18 | isLoading: false, 19 | data: action.data, 20 | error: '', 21 | }); 22 | default: return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /client/reducers/viewport/viewport.js: -------------------------------------------------------------------------------- 1 | import { SET_VIEWPORT } from 'viewport.js'; 2 | 3 | export const initialState = Immutable.Map({ 4 | isMobile: false, 5 | isTablet: false, 6 | isTouchDevice: 'ontouchstart' in window || 'onmsgesturechange' in window, 7 | isDesktop: true, 8 | }); 9 | 10 | // viewport handler 11 | export const viewportReducer = (state = initialState, action) => { 12 | switch(action.type) { 13 | case (SET_VIEWPORT): 14 | return state.merge(Immutable.Map({ 15 | isTouchDevice: 'ontouchstart' in window || 'onmsgesturechange' in window, 16 | isMobile: action.width < 768, 17 | isTablet: action.width >= 768 && action.width < 1024, 18 | isDesktop: action.width >= 1024 19 | })); 20 | default: return state; 21 | } 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /client/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | 3 | // Reducers 4 | import { loginReducer } from './login/login.js'; 5 | import { modalsReducer } from './modals/modals.js'; 6 | import { viewportReducer } from './viewport/viewport.js'; 7 | import { fetchedDataReducer } from './fetched-data/fetched-data.js'; 8 | import { toastersReducer } from './toaster/toaster.js'; 9 | import { sideMenuReducer } from './side-menu/side-menu.js'; 10 | import routeReducer from './route-reducer.js'; 11 | 12 | const app = combineReducers({ 13 | viewport: viewportReducer, 14 | routing: routeReducer, 15 | modals: modalsReducer, 16 | session: loginReducer, 17 | toasters: toastersReducer, 18 | sideMenu: sideMenuReducer, 19 | fetchedData: fetchedDataReducer, 20 | }); 21 | 22 | export default app; 23 | -------------------------------------------------------------------------------- /client/style/app.styl: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family Sofia 3 | font-style normal 4 | src url(font/SofiaProLight.otf) 5 | 6 | body, html 7 | font-family Sofia 8 | padding: 0 9 | margin: 0 10 | 11 | input 12 | -webkit-appearance: none 13 | -moz-appearance: none 14 | appearance: none 15 | outline: none 16 | border: none 17 | 18 | textarea 19 | -webkit-appearance: none 20 | -moz-appearance: none 21 | appearance: none 22 | outline: none 23 | border: none 24 | 25 | 26 | a:hover, a:visited, a:link, a:active 27 | text-decoration: none 28 | color: inherit 29 | 30 | .no-margin 31 | margin: 0px 32 | 33 | .no-padding 34 | padding: 0px 35 | 36 | // Load UI Kit 37 | json('ui-kit.json') 38 | 39 | @require "components/modal" 40 | @require "components/input" 41 | @require "components/typography" 42 | @require "components/button" 43 | @require "components/spinner" 44 | @require "components/slideout" 45 | 46 | -------------------------------------------------------------------------------- /client/components/checkbox/checkbox.jsx: -------------------------------------------------------------------------------- 1 | // CheckboxWithLabel.js 2 | 'use strict'; 3 | 4 | import React from 'react'; 5 | 6 | export default class CheckboxWithLabel extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = {isChecked: false}; 11 | 12 | // bind manually because React class components don't auto-bind 13 | // http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding 14 | this.onChange = this.onChange.bind(this); 15 | } 16 | 17 | onChange() { 18 | this.setState({isChecked: !this.state.isChecked}); 19 | } 20 | 21 | render() { 22 | return ( 23 | 31 | ); 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /client/reducers/side-menu/side-menu.js: -------------------------------------------------------------------------------- 1 | import { OPEN_SIDE_MENU, CLOSE_SIDE_MENU } from 'side-menu.js'; 2 | import { LOCATION_CHANGE } from 'react-router-redux'; 3 | import { SET_VIEWPORT } from 'viewport.js'; 4 | import { SUCCESS_LOGIN, SUCCESS_LOGOUT } from 'auth/login.js'; 5 | 6 | export const initialState = Immutable.Map({ 7 | isSideMenuOpen: false, 8 | }); 9 | 10 | export const sideMenuReducer = (state = initialState, action) => { 11 | switch(action.type) { 12 | case (OPEN_SIDE_MENU): return state.set('isSideMenuOpen', true); 13 | case (CLOSE_SIDE_MENU): return state.set('isSideMenuOpen', false); 14 | case (SET_VIEWPORT): return state.set('isSideMenuOpen', false); 15 | case (LOCATION_CHANGE): return state.set('isSideMenuOpen', false); 16 | case (SUCCESS_LOGOUT): return state.set('isSideMenuOpen', false); 17 | case (SUCCESS_LOGIN): return state.set('isSideMenuOpen', false); 18 | default: return state; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /client/reducers/login/login.js: -------------------------------------------------------------------------------- 1 | import { SUCCESS_LOGIN, FAIL_LOGIN, SUCCESS_LOGOUT } from 'auth/login.js'; 2 | 3 | export const initialState = Immutable.fromJS({ 4 | token: '', 5 | uid: '', 6 | provider: '', 7 | user: {}, 8 | isLoggedIn: false, 9 | }); 10 | 11 | export const loginReducer = (state = initialState, action) => { 12 | switch(action.type) { 13 | case (SUCCESS_LOGIN): return Immutable.fromJS({ 14 | isLoggedIn: true, 15 | uid: action.payload.user.uid, 16 | token: action.payload.credential.accessToken, 17 | provider: action.payload.credential.provider, 18 | user: Object.assign({}, state.user, { 19 | displayName: action.payload.user.displayName, 20 | email: action.payload.user.email, 21 | id: action.payload.user.uid, 22 | profileImageURL: action.payload.user.photoURL, 23 | }), 24 | }); 25 | case (FAIL_LOGIN): return initialState; 26 | case (SUCCESS_LOGOUT): return initialState; 27 | default: return state; 28 | } 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /client/store/configureStore.js: -------------------------------------------------------------------------------- 1 | // Redux 2 | import thunk from 'redux-thunk'; 3 | import promise from 'redux-promise'; 4 | import createLogger from 'redux-logger'; 5 | import { createStore, applyMiddleware } from 'redux'; 6 | 7 | // Reducers 8 | import appReducer from '../reducers/reducers.js'; 9 | 10 | // Actions 11 | import { setViewport } from 'viewport.js'; 12 | 13 | 14 | const stateTransformer = (state) => state.toJS(); 15 | 16 | let store; 17 | 18 | export function initializeStore() { 19 | 20 | const logger = createLogger({stateTransformer}); 21 | 22 | let middleWares = [thunk, promise]; 23 | 24 | if (process.env.NODE_ENV !== 'production') { 25 | middleWares = middleWares.concat([logger]); 26 | } 27 | 28 | const createStoreWithMiddleware = applyMiddleware.apply(null, middleWares)(createStore); 29 | 30 | store = createStoreWithMiddleware(appReducer); 31 | store.dispatch(setViewport(window.innerWidth)); 32 | 33 | return store; 34 | 35 | }; 36 | 37 | export function getStore() { 38 | return store; 39 | }; 40 | -------------------------------------------------------------------------------- /client/globals/style.js: -------------------------------------------------------------------------------- 1 | import { getStore } from 'configureStore.js'; 2 | 3 | export function getMobileStyle(styleObject) { 4 | // Mobile first approach 5 | // Style is overrided if tablet/desktop object present 6 | var mobileObject = {}; 7 | 8 | // Build mobile style object 9 | for (var key in styleObject) { 10 | if (!_.includes(['tablet', 'desktop'], key)) { 11 | mobileObject[key] = styleObject[key]; 12 | } 13 | } 14 | 15 | return mobileObject; 16 | }; 17 | 18 | 19 | export function handleStyle(style) { 20 | const state = getStore().getState().get('viewport'); 21 | 22 | let mobile = getMobileStyle(style); 23 | let {tablet, desktop} = style; 24 | let responsiveStyle; 25 | 26 | if (state.get('isMobile')) { 27 | responsiveStyle = _.clone(mobile); 28 | } else if (state.get('isTablet')) { 29 | responsiveStyle = _.extend(_.clone(mobile), tablet); 30 | } else if (state.get('isDesktop')) { 31 | responsiveStyle = _.extend(_.clone(mobile), _.clone(tablet), desktop); 32 | } 33 | 34 | return responsiveStyle; 35 | }; 36 | -------------------------------------------------------------------------------- /client/style/components/modal.styl: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay 2 | -webkit-perspective 600 3 | perspective 600 4 | opacity 0 5 | overflow-x hidden 6 | overflow-y auto 7 | background-color rgba(0, 0, 0, 0.5) 8 | z-index: 1000 9 | display: flex 10 | align-items: center 11 | 12 | 13 | .ReactModal__Overlay--after-open 14 | opacity 1 15 | transition opacity 150ms ease-out 16 | 17 | 18 | .ReactModal__Content 19 | -webkit-transform scale(0.5) rotateX(-30deg) 20 | max-width: 800px 21 | margin: auto 22 | 23 | 24 | .ReactModal__Content--after-open 25 | -webkit-transform scale(1) rotateX(0deg) 26 | transition all 150ms ease-in 27 | 28 | 29 | .ReactModal__Overlay--before-close 30 | opacity 0 31 | 32 | 33 | .ReactModal__Content--before-close 34 | -webkit-transform scale(0.5) rotateX(30deg) 35 | transition all 150ms ease-in 36 | 37 | 38 | .ReactModal__Content.modal-dialog 39 | border none 40 | background-color transparent 41 | 42 | .ReactModal__Body--open 43 | overflow: hidden 44 | 45 | .ReactModal__Body--close 46 | overflow: visible 47 | -------------------------------------------------------------------------------- /client/style/components/spinner.styl: -------------------------------------------------------------------------------- 1 | .spinner-path 2 | stroke-dasharray: 509 3 | stroke-dashoffset: 509 4 | animation: dash-mobile 10s linear alternate infinite 5 | 6 | @media (min-width: breakpointT px) 7 | .spinner-path 8 | stroke-dasharray: 2116 9 | stroke-dashoffset: 2116 10 | animation: dash 10s linear alternate infinite 11 | 12 | @keyframes dash 13 | 0% { 14 | stroke-dashoffset: 2116; 15 | fill: #ffffff; 16 | } 17 | 18 | 5% { 19 | stroke-dashoffset: 2116; 20 | fill: #ffffff; 21 | } 22 | 23 | 90% { 24 | stroke-dashoffset: 1500; 25 | fill: lightGreen; 26 | } 27 | 28 | 100% { 29 | stroke-dashoffset: 1500; 30 | fill: lightGreen; 31 | } 32 | 33 | @keyframes dash-mobile 34 | 0% { 35 | stroke-dashoffset: 509; 36 | fill: #ffffff; 37 | } 38 | 39 | 5% { 40 | stroke-dashoffset: 509; 41 | fill: #ffffff; 42 | } 43 | 44 | 90% { 45 | stroke-dashoffset: 100; 46 | fill: lightGreen; 47 | } 48 | 49 | 100% { 50 | stroke-dashoffset: 100; 51 | fill: lightGreen; 52 | } 53 | -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | var colors = require('colors'); 3 | var webpack = require('webpack'); 4 | var config = require('../webpack.development.config.js'); 5 | var WebpackDevServer = require('webpack-dev-server'); 6 | 7 | // handle the code updates client side 8 | config.entry.unshift( 9 | 'webpack-dev-server/client?http://0.0.0.0:3000', 10 | 'webpack/hot/only-dev-server' 11 | ); 12 | 13 | var compiler = webpack(config); 14 | 15 | // launch hermes express server 16 | var server = require('./server.js'); 17 | 18 | var devServer = new WebpackDevServer(compiler, { 19 | proxy: { 20 | '*': 'http://localhost:' + (process.env.PORT ? process.env.PORT : '9000'), 21 | }, 22 | quiet: false, 23 | noInfo: false, 24 | stats: { colors: true }, 25 | hot: true, 26 | }); 27 | 28 | // launch webpack dev server TODO: maybe use an env var for the webpack-dev-server port 29 | devServer.listen(3000, function() { 30 | console.log('[webpack-dev-server]'.bold.green + ' listening on port 3000'.bold); 31 | console.log('[webpack-dev-server]'.bold.green + ' building the app...'.bold); 32 | }); 33 | -------------------------------------------------------------------------------- /server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | ReacToGo 13 | 18 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /client/components/spinner/spinner.jsx: -------------------------------------------------------------------------------- 1 | import { path, pathMobile } from './path.js'; 2 | import { connect } from 'react-redux'; 3 | 4 | let Spinner = (props) => { 5 | return ( 6 |
7 | {props.viewport.get('isMobile') ? 8 | 9 | 17 | : 18 | 19 | 27 | } 28 |
29 | ); 30 | }; 31 | 32 | Spinner.displayName = 'Spinner'; 33 | 34 | 35 | export default connect((state) => ({ 36 | viewport: state.get('viewport'), 37 | }))(Spinner); 38 | -------------------------------------------------------------------------------- /tests/karma-conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack.config.js'); 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | basePath: '../', 6 | frameworks: ['mocha'], 7 | files: [ 8 | 'node_modules/phantomjs-polyfill/bind-polyfill.js', 9 | './node_modules/phantomjs-polyfill-object-assign/object-assign-polyfill.js', 10 | './tests/tests-config.js', 11 | ], 12 | preprocessors: {'./tests/tests-config.js': ['webpack']}, 13 | webpack: { 14 | module: webpackConfig.module, 15 | resolve: webpackConfig.resolve, 16 | plugins: webpackConfig.plugins, 17 | }, 18 | webpackMiddleware: { 19 | stats: { 20 | colors: true 21 | } 22 | }, 23 | reporters: ['spec'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['PhantomJS'], 29 | captureTimeout: 60000, 30 | singleRun: false, 31 | plugins: [ 32 | require('karma-mocha'), 33 | require('karma-spec-reporter'), 34 | require('karma-phantomjs-launcher'), 35 | require('karma-webpack'), 36 | ] 37 | }); 38 | }; 39 | 40 | -------------------------------------------------------------------------------- /client/reducers/modals/modals.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { initialState, modalsReducer } from './modals.js'; 3 | import { OPEN_MODAL_LOGIN, CLOSE_MODAL_LOGIN } from 'modals.js'; 4 | 5 | // Check data structure 6 | const checkModalsReducer = (state) => { 7 | expect(Immutable.Map.isMap(state)).toEqual(true); 8 | expect(state.size).toEqual(1); 9 | expect(state.get('isLoginModalOpen')).toBeA('boolean'); 10 | }; 11 | 12 | describe('modal reducer', () => { 13 | it('should return the initial state', () => { 14 | expect(modalsReducer(undefined, {})).toEqual(initialState); 15 | }); 16 | it('should handle OPEN_MODAL_LOGIN', () => { 17 | const finalState = modalsReducer(initialState, {type: OPEN_MODAL_LOGIN}); 18 | expect(finalState.get('isLoginModalOpen')).toEqual(true); 19 | checkModalsReducer(finalState); 20 | }); 21 | it('should handle CLOSE_MODAL_LOGIN', () => { 22 | const firstState = modalsReducer(initialState, {type: OPEN_MODAL_LOGIN}); 23 | const finalState = modalsReducer(firstState, {type: CLOSE_MODAL_LOGIN}); 24 | expect(finalState.get('isLoginModalOpen')).toEqual(false); 25 | checkModalsReducer(finalState); 26 | }); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /client/style/components/button.styl: -------------------------------------------------------------------------------- 1 | .button 2 | padding: 10px 3 | box-shadow: inset 0px -2px 0px rgba(0,0,0,0.10) 4 | font-size: fontSM px 5 | display: inline-block 6 | border-radius: 2px 7 | text-align: center 8 | cursor: pointer 9 | 10 | @media (min-width: breakpointT px) 11 | .button 12 | padding: 10px 20px 13 | 14 | .button-primary 15 | background-color: lightGreen 16 | color: lightWhite 17 | transition: background-color 0.4s; 18 | .button-primary:hover 19 | background-color: darkGreen 20 | 21 | .button-secondary 22 | background-color: darkBlue 23 | color: lightWhite 24 | transition: background-color 0.4s; 25 | .button-secondary:hover 26 | background-color: lightBlack 27 | 28 | .button-cancel 29 | background-color: lightGrey 30 | color: lightWhite 31 | transition: background-color 0.4s; 32 | .button-cancel:hover 33 | background-color: darkBlue 34 | 35 | .button-danger 36 | background-color: lightRed 37 | color: lightWhite 38 | transition: background-color 0.4s; 39 | .button-danger:hover 40 | background-color: darkRed 41 | 42 | .button-info 43 | background-color: lightYellow 44 | color: lightWhite 45 | transition: background-color 0.4s; 46 | .button-info:hover 47 | background-color: darkYellow 48 | -------------------------------------------------------------------------------- /client/reducers/toaster/toaster.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | 3 | import { initialState, toastersReducer } from './toaster.js'; 4 | import { ADD_MESSAGE, REMOVE_MESSAGE} from 'toaster.js'; 5 | 6 | // Check data structure 7 | const checkToasterReducer = (state) => { 8 | expect(Immutable.List.isList(state)).toEqual(true); 9 | }; 10 | 11 | describe('toaster reducer', () => { 12 | it('should return the initial state', () => { 13 | expect(toastersReducer(undefined, {})).toEqual(initialState); 14 | }); 15 | it('should handle ADD_MESSAGE', () => { 16 | const finalState = toastersReducer(initialState, {type: ADD_MESSAGE, message: { 17 | text: 'test', 18 | id: 0, 19 | type: 'success' 20 | }}); 21 | //REALLY UGLY 22 | expect(finalState.toJS()[0].text).toEqual('test'); 23 | expect(finalState.size).toEqual(1); 24 | checkToasterReducer(finalState); 25 | }); 26 | it('should handle REMOVE_MESSAGE', () => { 27 | const firstState = toastersReducer(initialState, {type: ADD_MESSAGE, message: { 28 | text: 'test', 29 | id: 0, 30 | type: 'success' 31 | }}); 32 | const finalState = toastersReducer(firstState, {type: REMOVE_MESSAGE, id: 0}); 33 | expect(finalState.size).toEqual(0); 34 | checkToasterReducer(finalState); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var favicon = require('serve-favicon'); 4 | var bodyParser = require('body-parser'); 5 | var compression = require('compression'); 6 | 7 | const firebaseApp = process.env.FIREBASE_URL; 8 | 9 | var app = express(); 10 | 11 | // Enable gzip 12 | app.use(compression()); 13 | 14 | // Serve dist 15 | app.use(express.static(path.resolve(__dirname, '../dist'))); 16 | 17 | // Parse json for mails 18 | app.use(bodyParser.json()); 19 | app.use(bodyParser.urlencoded({ extended: false })); 20 | 21 | // Favicon 22 | app.use(favicon(path.join(__dirname, 'fav.ico'))); 23 | 24 | // View render 25 | app.set('views', path.join(__dirname, './views')); 26 | app.set('view engine', 'ejs'); 27 | 28 | 29 | // Render files 30 | app.get('*', function (req, res) { 31 | res.render('index', {ENV: JSON.stringify({ 32 | firebaseApp: firebaseApp 33 | })}); 34 | }); 35 | 36 | 37 | // Mail endpoint 38 | app.post('/endpoint', function(req, res) { 39 | res.send({body: 'Test endpoint'}); 40 | }); 41 | 42 | // Launch app 43 | var server = app.listen((process.env.PORT || 9000), function () { 44 | 45 | var host = server.address().address; 46 | var port = server.address().port; 47 | 48 | console.log('Example app listening at http://%s:%s', host, port); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /client/components/button-velocity.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import ReactDOM from 'react-dom'; 3 | 4 | // Utils 5 | import { handleStyle } from 'style.js'; 6 | 7 | let s = getStyle(); 8 | 9 | // Main class - App 10 | export default class ButtonVelocity extends React.Component { 11 | constructor() { 12 | super(); 13 | this.handleHover = this.handleHover.bind(this); 14 | } 15 | handleHover(active) { 16 | $(ReactDOM.findDOMNode(this)).velocity('stop'); 17 | $(ReactDOM.findDOMNode(this)).velocity({scale: active ? 1.4 : 1}, {duration: 200, easing: 'easeIn'}); 18 | } 19 | render() { 20 | return ( 21 |
25 | 29 | Check source code 30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | function getStyle() { 37 | return { 38 | container: { 39 | marginTop: 20, 40 | tablet: { 41 | marginTop: 30, 42 | }, 43 | desktop: { 44 | marginTop: 30, 45 | }, 46 | }, 47 | row: { 48 | marginTop: 70, 49 | }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /client/containers/home/components/home.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { connect } from 'react-redux'; 3 | 4 | // Components 5 | import ButtonMotion from 'button-motion.jsx'; 6 | import ButtonVelocity from 'button-velocity.jsx'; 7 | import Spinner from 'spinner/spinner.jsx'; 8 | 9 | let s = getStyle(); 10 | 11 | let Home = () => 12 | (
13 | 14 |

15 | Including Webpack, React, React-Router, ImmutableJS, Redux, VelocityJS, 16 | React Motion, Stylus, Karma, Fetch API Polyfill 17 |

18 |
19 |
20 | React Motion 21 | 22 |
23 |
24 | Velocity JS 25 | 26 |
27 |
28 |

Fork the project and run npm install

29 |
); 30 | 31 | function getStyle() { 32 | return { 33 | container: { 34 | textAlign: 'center', 35 | marginTop: 40, 36 | padding: 20, 37 | }, 38 | row: { 39 | maxWidth: 600, 40 | margin: 'auto', 41 | marginBottom: 60, 42 | }, 43 | subtitle: { 44 | marginBottom: 60, 45 | marginTop: 60, 46 | }, 47 | }; 48 | } 49 | 50 | Home.displayName = 'Home'; 51 | 52 | export default connect((state) => ({viewport: state.get('viewport')}))(Home); 53 | -------------------------------------------------------------------------------- /client/style/components/input.styl: -------------------------------------------------------------------------------- 1 | .input 2 | border: 2px solid 3 | border-radius: 2px 4 | padding: 10px 5 | 6 | .input-primary 7 | border-color: lightGreen 8 | color: lightGreen 9 | 10 | .input-secondary 11 | border-color: lightGrey 12 | color: darkBlue 13 | 14 | .input-active 15 | border-color: lightGreen 16 | color: darkBlue 17 | 18 | .input-error 19 | border-color: lightRed 20 | color: lightRed 21 | 22 | .input-primary::-webkit-input-placeholder 23 | color: lightGreen 24 | .input-primary:::-moz-placeholder 25 | color: lightGreen 26 | .input-primary::::-moz-placeholder 27 | color: lightGreen 28 | .input-primary:::-ms-input-placeholder 29 | color: lightGreen 30 | 31 | .input-active::-webkit-input-placeholder 32 | color: lightGrey 33 | .input-active:::-moz-placeholder 34 | color: lightGrey 35 | .input-active::::-moz-placeholder 36 | color: lightGrey 37 | .input-active:::-ms-input-placeholder 38 | color: lightGrey 39 | 40 | .input-secondary::-webkit-input-placeholder 41 | color: lightGrey 42 | .input-secondary:::-moz-placeholder 43 | color: lightGrey 44 | .input-secondary::::-moz-placeholder 45 | color: lightGrey 46 | .input-secondary:::-ms-input-placeholder 47 | color: lightGrey 48 | 49 | .input-error::-webkit-input-placeholder 50 | color: lightRed 51 | .input-error:::-moz-placeholder 52 | color: lightRed 53 | .input-error::::-moz-placeholder 54 | color: lightRed 55 | .input-error:::-ms-input-placeholder 56 | color: lightRed 57 | -------------------------------------------------------------------------------- /client/actions/auth/login.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | 3 | // Actions 4 | import { closeModal } from 'modals.js'; 5 | import { pushMessage } from 'toaster.js'; 6 | import { browserHistory } from 'react-router'; 7 | 8 | export const REQUEST_LOGIN = 'REQUEST_LOGIN'; 9 | export const SUCCESS_LOGIN = 'SUCCESS_LOGIN'; 10 | export const FAIL_LOGIN = 'FAIL_LOGIN'; 11 | export const SUCCESS_LOGOUT = 'SUCCESS_LOGOUT'; 12 | 13 | function requestLogin() { return {type: REQUEST_LOGIN}; }; 14 | function sucessLogin(payload) { return {type: SUCCESS_LOGIN, payload}; }; 15 | function failLogin(error) { return {type: FAIL_LOGIN, error}; }; 16 | function successLogout() { return {type: SUCCESS_LOGOUT}; }; 17 | 18 | export const login = () => dispatch => { 19 | dispatch(requestLogin()); 20 | 21 | // Firebase 22 | const auth = firebase.auth(); 23 | const provider = new firebase.auth.FacebookAuthProvider(); 24 | 25 | return auth.signInWithPopup(provider) 26 | .then(authData => { 27 | dispatch(sucessLogin(authData)); 28 | dispatch(pushMessage('Login Succesful', 'success')); 29 | dispatch(closeModal()); 30 | }) 31 | .catch(error => { 32 | dispatch(failLogin(error)); 33 | dispatch(pushMessage('Oops, something went wrong', 'error')); 34 | }); 35 | }; 36 | 37 | export const logout = () => dispatch => firebase.auth().signOut().then(() => { 38 | browserHistory.push('/'); 39 | dispatch(pushMessage('Logout Succesful', 'success')); 40 | dispatch(successLogout()); 41 | }); 42 | -------------------------------------------------------------------------------- /client/containers/fetch-example/components/fetch-example.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { connect } from 'react-redux'; 3 | 4 | // Actions 5 | import { fetchData } from 'fetch.js'; 6 | 7 | // Components 8 | import Spinner from 'spinner/spinner.jsx'; 9 | 10 | let s = getStyle(); 11 | 12 | class FetchExample extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | } 16 | render() { 17 | 18 | return
19 |

Stupid API call to JSON Placeholder

20 |

Click on the button for fetching data from OPEN JSON API. A delay of 3s is set for seeing the spinner!

21 |
this.props.dispatch(fetchData())} 24 | className='button button-primary'> 25 | Fetch Data 26 |
27 | {this.props.fetchedData.get('isLoading') ? : 28 | !this.props.fetchedData.get('data').isEmpty() ? 29 |

30 | {JSON.stringify(this.props.fetchedData.get('data').toJS())} 31 |

: ''} 32 |
; 33 | } 34 | } 35 | 36 | FetchExample.displayName = 'FetchExample'; 37 | 38 | function getStyle() { 39 | return { 40 | title: { 41 | marginBottom: 30, 42 | }, 43 | container: { 44 | marginTop: 60, 45 | }, 46 | cta: { 47 | marginTop: 50, 48 | marginBottom: 30, 49 | }, 50 | data: { 51 | padding: 20, 52 | wordBreak: 'break-all', 53 | }, 54 | }; 55 | } 56 | 57 | export default connect((state) => ({fetchedData: state.get('fetchedData')}))(FetchExample); 58 | 59 | -------------------------------------------------------------------------------- /client/style/icon/styles.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @font-face { 4 | font-family: "hummingbird"; 5 | src:url("fonts/hummingbird.eot"); 6 | src:url("fonts/hummingbird.eot?#iefix") format("embedded-opentype"), 7 | url("fonts/hummingbird.woff") format("woff"), 8 | url("fonts/hummingbird.ttf") format("truetype"), 9 | url("fonts/hummingbird.svg#hummingbird") format("svg"); 10 | font-weight: normal; 11 | font-style: normal; 12 | 13 | } 14 | 15 | [data-icon]:before { 16 | font-family: "hummingbird" !important; 17 | content: attr(data-icon); 18 | font-style: normal !important; 19 | font-weight: normal !important; 20 | font-variant: normal !important; 21 | text-transform: none !important; 22 | speak: none; 23 | line-height: 1; 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | [class^="icon-"]:before, 29 | [class*=" icon-"]:before { 30 | font-family: "hummingbird" !important; 31 | font-style: normal !important; 32 | font-weight: normal !important; 33 | font-variant: normal !important; 34 | text-transform: none !important; 35 | speak: none; 36 | line-height: 1; 37 | -webkit-font-smoothing: antialiased; 38 | -moz-osx-font-smoothing: grayscale; 39 | } 40 | 41 | .icon-arrow-down:before { 42 | content: "\61"; 43 | } 44 | .icon-icomoon:before { 45 | content: "\62"; 46 | } 47 | .icon-cross:before { 48 | content: "\63"; 49 | } 50 | .icon-cross-circle:before { 51 | content: "\64"; 52 | } 53 | .icon-bin:before { 54 | content: "\65"; 55 | } 56 | .icon-icomoon-2:before { 57 | content: "\66"; 58 | } 59 | .icon-menu:before { 60 | content: "\67"; 61 | } 62 | -------------------------------------------------------------------------------- /client/components/side-menu/item.jsx: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | let s = getStyle(); 6 | 7 | export class Item extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.handleHover = this.handleHover.bind(this); 11 | } 12 | handleHover(isHover, event) { 13 | if ((this.props.isTouchDevice && event === 'touch') || 14 | (!this.props.isTouchDevice && event === 'mouse')) { 15 | if (`/${this.props.path}` !== this.props.item.path) { 16 | $(this.refs.container).velocity('stop'); 17 | $(this.refs.container).velocity({opacity: isHover ? 1 : 0.8}, {duration: 200}); 18 | } 19 | } 20 | } 21 | render() { 22 | const linkStyle = Object.assign({}, s.container, { 23 | opacity: (`/${this.props.path}` === this.props.item.path) ? 1 : 0.8}); 24 | 25 | return (
26 | 27 |
this.handleHover(true, 'mouse')} 29 | onMouseLeave={() => this.handleHover(false, 'mouse')} 30 | onTouchStart={() => this.handleHover(true, 'touch')} 31 | onTouchEnd={() => this.handleHover(false, 'touch')} 32 | style={linkStyle} 33 | ref='container'> 34 | {this.props.item.value} 35 |
36 | 37 |
); 38 | } 39 | }; 40 | 41 | function getStyle() { 42 | return { 43 | container: { 44 | padding: '10px 5px', 45 | cursor: 'pointer', 46 | color: UI.lightWhite, 47 | }, 48 | }; 49 | }; 50 | 51 | Item.displayName = 'ItemSideMenu'; 52 | 53 | export default connect((state) => ({ 54 | path: state.get('routing').get('locationBeforeTransitions').get('pathname'), 55 | }))(Item); 56 | -------------------------------------------------------------------------------- /client/components/side-menu/side-menu.jsx: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { connect } from 'react-redux'; 3 | 4 | // Component 5 | import Item from './item.jsx'; 6 | import Login from 'login/login.jsx'; 7 | 8 | // Actions 9 | import { logout } from 'auth/login.js'; 10 | 11 | 12 | let s = getStyle(); 13 | 14 | export class SideMenu extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | } 18 | render() { 19 | return ( 20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | {this.props.session.get('isLoggedIn') ?
this.props.dispatch(logout())}>Logout
: 35 | } 37 |
38 |
39 |
40 | ); 41 | } 42 | }; 43 | 44 | SideMenu.displayName = 'SideMenu'; 45 | 46 | function getStyle() { 47 | return { 48 | container: { 49 | padding: 15, 50 | backgroundColor: UI.lightGreen, 51 | height: '100%', 52 | color: UI.lightWhite, 53 | }, 54 | group: { 55 | margin: '20px 0px', 56 | }, 57 | }; 58 | }; 59 | 60 | export default connect((state) => ({ 61 | viewport: state.get('viewport'), 62 | session: state.get('session') 63 | }))(SideMenu); 64 | -------------------------------------------------------------------------------- /client/style/components/typography.styl: -------------------------------------------------------------------------------- 1 | // Colors 2 | .light-green 3 | color: lightGreen 4 | 5 | .light-white 6 | color: lightWhite 7 | 8 | .light-grey 9 | color: lightGrey 10 | 11 | .light-red 12 | color: lightRed 13 | 14 | .dark-red 15 | color: darkRed 16 | 17 | .dark-blue 18 | color: darkBlue 19 | 20 | .light-yellow 21 | color: lightYellow 22 | 23 | .dark-yellow 24 | color: darkYellow 25 | 26 | // Font Weight 27 | .text-bold 28 | font-weight: bold 29 | 30 | // Text position 31 | .text-center 32 | text-align: center 33 | 34 | .text-left 35 | text-align: left 36 | 37 | .text-right 38 | text-align: right 39 | 40 | .cursor 41 | cursor: pointer 42 | 43 | // Display 44 | .flex 45 | display: flex 46 | 47 | .inline-block 48 | display: inline-block 49 | 50 | .middle 51 | vertical-align: middle 52 | 53 | .vertical-align-middle 54 | margin-top: 50% 55 | -webkit-transform: translate3d(0, -50%, 0) 56 | -ms-transform: translate3d(0, -50%, 0) 57 | transform: translate3d(0, -50%, 0) 58 | 59 | .flex-1 60 | flex: 1 61 | 62 | .flex-2 63 | flex: 2 64 | 65 | .flex-3 66 | flex: 3 67 | 68 | .flex-center 69 | align-items: center 70 | 71 | // Font Size 72 | .font-xs 73 | font-size: fontXS px 74 | 75 | .font-sm 76 | font-size: fontSM px 77 | 78 | .font-md 79 | font-size: fontMD px 80 | 81 | .font-lg 82 | font-size: fontLG px 83 | 84 | .font-xl 85 | font-size: fontXL px 86 | 87 | .font-xxl 88 | font-size: fontXXL px 89 | 90 | // Tags 91 | h1 92 | margin: 0 93 | font-size: fontXL px 94 | font-weight: bold 95 | 96 | h2 97 | margin: 0 98 | font-size: fontMD px 99 | font-weight: bold 100 | 101 | h3 102 | margin: 0 103 | font-size: fontSM px 104 | font-weight: bold 105 | 106 | p 107 | margin: 0 108 | font-size: fontSM px 109 | -------------------------------------------------------------------------------- /client/components/button-motion.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import {spring, Motion} from 'react-motion'; 3 | 4 | // Utils 5 | import { handleStyle } from 'style.js'; 6 | 7 | var s = getStyle(); 8 | 9 | // Main class - App 10 | export default class ButtonMotion extends React.Component { 11 | constructor() { 12 | super(); 13 | this.state = {isHover: false}; 14 | this.handleHover = this.handleHover.bind(this); 15 | this.getSpringProps = this.getSpringProps.bind(this); 16 | } 17 | handleHover(active) {this.setState({isHover: active}); } 18 | getSpringProps() { 19 | return { 20 | defaultStyle: { 21 | scale: 1, 22 | }, 23 | style:{ 24 | scale: spring(this.state.isHover ? 1.4 : 1), 25 | }, 26 | }; 27 | } 28 | render() { 29 | return ( 30 | 31 | {interpolatedStyle => { 32 | return ( 33 |
37 | 42 | Check source code 43 | 44 |
45 | ); 46 | }} 47 |
48 | ); 49 | } 50 | } 51 | 52 | function getStyle() { 53 | return { 54 | container: { 55 | marginTop: 20, 56 | tablet: { 57 | marginTop: 30, 58 | }, 59 | desktop: { 60 | marginTop: 30, 61 | }, 62 | }, 63 | row: { 64 | marginTop: 70, 65 | }, 66 | }; 67 | } 68 | 69 | ButtonMotion.displayName = 'ButtonMotion'; 70 | 71 | -------------------------------------------------------------------------------- /client/components/modal-container/modal-container.jsx: -------------------------------------------------------------------------------- 1 | // Utils 2 | import Modal from 'react-modal'; 3 | import { handleStyle } from 'style.js'; 4 | 5 | let s = getStyle(); 6 | 7 | const modalStyle = { 8 | overlay: { 9 | backgroundColor: 'none', 10 | overflow: 'hidden', 11 | }, 12 | content: { 13 | backgroundColor: UI.lightGreen, 14 | border: 'none', 15 | borderRadius: 0, 16 | top: 0, 17 | left: 0, 18 | right: 0, 19 | overflow: 'visible', 20 | height: '100%', 21 | bottom: 'initial', 22 | padding: 20, 23 | position: 'absolute', 24 | tablet: { 25 | top: 'initial', 26 | left: 'initial', 27 | right: 'initial', 28 | padding: 40, 29 | maxWidth: 500, 30 | height: 'initial', 31 | borderRadius: 4, 32 | position: 'relative', 33 | }, 34 | }, 35 | }; 36 | 37 | let ModalContainer = (props) => { 38 | const style = Object.assign({}, {overlay: modalStyle.overlay}, {content: handleStyle(modalStyle.content)}); 39 | 40 | return props.closeModal()} 44 | isOpen={props.isModalOpen}> 45 |
46 | props.closeModal()}/> 47 | {props.children} 48 |
49 |
; 50 | }; 51 | 52 | function getStyle() { 53 | return { 54 | container: { 55 | }, 56 | close: { 57 | top: 20, 58 | right: 20, 59 | position: 'absolute', 60 | width: 14, 61 | cursor: 'pointer', 62 | tablet: { 63 | right: 0, 64 | top: -25, 65 | }, 66 | }, 67 | }; 68 | } 69 | ModalContainer.displayName = 'ModalContainer'; 70 | 71 | ModalContainer.propTypes = { 72 | closeModal: React.PropTypes.func, 73 | isModalOpen: React.PropTypes.bool, 74 | }; 75 | 76 | export default ModalContainer; 77 | -------------------------------------------------------------------------------- /client/components/route-transition.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { PropTypes } from 'react'; 3 | import { TransitionMotion, spring } from 'react-motion'; 4 | 5 | export default class RouteTransition extends React.Component { 6 | constructor() { 7 | super(); 8 | this.willEnter = this.willEnter.bind(this); 9 | this.willLeave = this.willLeave.bind(this); 10 | } 11 | willEnter() { 12 | return { 13 | handler: this.props.children, 14 | opacity: spring(0), 15 | position: spring(30) 16 | }; 17 | } 18 | willLeave(key, value) { 19 | return { 20 | handler: value.handler, 21 | opacity: spring(0), 22 | position: spring(30) 23 | }; 24 | } 25 | getStyles() { 26 | const { children, pathname } = this.props; 27 | return { 28 | [pathname]: { 29 | handler: children, 30 | opacity: spring(1), 31 | position: spring(1) 32 | } 33 | }; 34 | } 35 | render() { 36 | return ( 37 | 42 | {interpolated => 43 |
44 | {Object.keys(interpolated).map(key => 45 |
55 | {interpolated[key].handler} 56 |
57 | )} 58 |
59 | } 60 |
61 | ); 62 | } 63 | }; 64 | 65 | RouteTransition.propTypes = { 66 | pathname: PropTypes.string.isRequired, 67 | }; 68 | 69 | RouteTransition.displayName = 'RouteTransition'; 70 | -------------------------------------------------------------------------------- /client/reducers/fetched-data/fetched-data.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {fetchedDataReducer, initialState} from './fetched-data.js'; 3 | import { REQUEST_DATA, RECEIVE_DATA, FAIL_DATA } from 'fetch.js'; 4 | 5 | // Check data structure 6 | const checkFetchedDataReducer = (state) => { 7 | expect(Immutable.Map.isMap(state)).toEqual(true); 8 | expect(state.size).toEqual(3); 9 | expect(state.get('isLoading')).toBeA('boolean'); 10 | expect(state.get('error')).toBeA('string'); 11 | expect(Immutable.Map.isMap(state.get('data'))).toEqual(true); 12 | }; 13 | 14 | describe('fetch data reducer', () => { 15 | it('should return the initial state', () => { 16 | expect(fetchedDataReducer(undefined, {})).toEqual(initialState); 17 | }); 18 | it('should handle REQUEST_DATA', () => { 19 | const finalState = fetchedDataReducer(initialState, {type: REQUEST_DATA}); 20 | 21 | expect(finalState.get('isLoading')).toEqual(true); 22 | checkFetchedDataReducer(finalState); 23 | }); 24 | it('should handle FAIL_DATA', () => { 25 | const firstState = fetchedDataReducer(initialState, {type: REQUEST_DATA}); 26 | const finalState = fetchedDataReducer(firstState, {type: FAIL_DATA, data: 'error'}); 27 | 28 | expect(finalState.get('isLoading')).toEqual(false); 29 | expect(finalState.get('error')).toEqual('error'); 30 | expect(finalState.get('data').size).toEqual(0); 31 | checkFetchedDataReducer(finalState); 32 | }); 33 | it('should handle RECEIVE_DATA', () => { 34 | const firstState = fetchedDataReducer(initialState, {type: REQUEST_DATA}); 35 | const finalState = fetchedDataReducer(firstState, {type: RECEIVE_DATA, data: {randomField: 1}}); 36 | 37 | expect(finalState.get('isLoading')).toEqual(false); 38 | expect(finalState.get('error')).toEqual(''); 39 | expect(finalState.get('data').size).toEqual(1); 40 | expect(finalState.getIn(['data', 'randomField'])).toEqual(1); 41 | checkFetchedDataReducer(finalState); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /client/app.jsx: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import 'velocity-animate'; 3 | import './style/app.styl'; 4 | import './style/icon/styles.css'; 5 | 6 | // Actions 7 | import { setViewport } from 'viewport.js'; 8 | 9 | // Utils 10 | import ReactDOM from 'react-dom'; 11 | import { Provider } from 'react-redux'; 12 | import { Router, browserHistory } from 'react-router'; 13 | import { syncHistoryWithStore } from 'react-router-redux'; 14 | import { initializeStore } from './store/configureStore.js'; 15 | import { initializeApp } from 'firebase-conf.js'; 16 | 17 | // Pages 18 | import Index from './containers/index/index.jsx'; 19 | 20 | var FastClick = require('fastclick'); 21 | FastClick.attach(document.body); 22 | 23 | // Firebase 24 | initializeApp(); 25 | 26 | const rootRoute = { 27 | component: 'div', 28 | childRoutes: [{ 29 | path: '/', 30 | component: Index, 31 | indexRoute: require('./containers/home/home.module.js'), 32 | childRoutes: require('./containers/containers.module.js'), 33 | }] 34 | }; 35 | 36 | const store = initializeStore(); 37 | const history = syncHistoryWithStore(browserHistory, store, { 38 | selectLocationState: (state) => state.get('routing').toJS() 39 | }); 40 | 41 | // Main class - App 42 | class App extends React.Component { 43 | constructor(props) { 44 | super(props); 45 | this.handleResize = this.handleResize.bind(this); 46 | this.debouncedHandleResize = _.debounce(() => {this.handleResize(); }, 100); 47 | } 48 | handleResize() { 49 | store.dispatch(setViewport(window.innerWidth)); 50 | } 51 | componentDidMount() { 52 | this.handleResize(); 53 | window.addEventListener('resize', this.debouncedHandleResize); 54 | } 55 | componentWillUnmount() { 56 | window.removeEventListener('resize', this.debouncedHandleResize); 57 | } 58 | render() { 59 | return ( 60 | 61 | 62 | 63 | ); 64 | } 65 | } 66 | 67 | ReactDOM.render(, document.getElementById('app')); 68 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // External libs 2 | var webpack = require('webpack'); 3 | var path = require('path'); 4 | 5 | // Put React as a global variable 6 | var providePlugin = new webpack.ProvidePlugin({ 7 | 'React': 'react', 8 | 'Immutable': 'immutable', 9 | $: 'jquery', 10 | 'window.$': 'jquery', 11 | 'window.jQuery': 'jquery', 12 | 'jQuery': 'jquery', 13 | '_': 'underscore', 14 | UI: '!json-loader!' + path.resolve(__dirname, './client/style/ui-kit.json'), 15 | 'es6-promise': 'es6-promise', 16 | fetch: 'imports?this=>global!exports?global.fetch!whatwg-fetch', 17 | }); 18 | 19 | var definePlugin = new webpack.DefinePlugin({ 20 | 'process.env.FIREBASE_API_KEY': JSON.stringify(process.env.FIREBASE_API_KEY), 21 | 'process.env.FIREBASE_AUTH_DOMAIN': JSON.stringify(process.env.FIREBASE_AUTH_DOMAIN), 22 | 'process.env.FIREBASE_DATABASE_URL': JSON.stringify(process.env.FIREBASE_DATABASE_URL), 23 | }); 24 | 25 | var config = { 26 | 27 | entry: ['./client/app.jsx'], 28 | output: { 29 | path: path.resolve(__dirname, 'dist'), 30 | filename: 'bundle.js' 31 | }, 32 | devtool: 'source-map', 33 | module: { 34 | loaders: [ 35 | { 36 | id: 'jsx', 37 | test:/\.(js|jsx)?$/, 38 | loaders: ['babel?presets[]=react,presets[]=es2015,plugins[]=add-module-exports', 'eslint-loader'], 39 | exclude: /(node_modules)/, 40 | }, 41 | { 42 | id: 'img', 43 | test: /\.(png|jpg|svg)?$/, 44 | loader: 'file-loader', 45 | exclude: /node_modules/ 46 | }, 47 | { 48 | id: 'font', 49 | test: /\.(otf|eot|woff|ttf)$/, 50 | loader: 'file-loader', 51 | exclude: /node_modules/ 52 | }, 53 | { 54 | id: 'style', 55 | test: /\.(styl|css)$/, 56 | loader: 'style-loader!css-loader!stylus-loader', 57 | exclude: /(node_modules)/ 58 | }, 59 | ] 60 | }, 61 | plugins: [ 62 | providePlugin, 63 | definePlugin, 64 | ], 65 | resolve: { 66 | modulesDirectories: [ 67 | 'node_modules/', 68 | 'utils/', 69 | 'components/', 70 | './client/actions', 71 | './client/store', 72 | './client/globals' 73 | ], 74 | }, 75 | }; 76 | 77 | module.exports = config; 78 | -------------------------------------------------------------------------------- /client/reducers/side-menu/side-menu.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { initialState, sideMenuReducer } from './side-menu.js'; 3 | import { OPEN_SIDE_MENU, CLOSE_SIDE_MENU } from 'side-menu.js'; 4 | import { LOCATION_CHANGE } from 'react-router-redux'; 5 | import { SET_VIEWPORT } from 'viewport.js'; 6 | import { SUCCESS_LOGIN, SUCCESS_LOGOUT } from 'auth/login.js'; 7 | 8 | // Check data structure 9 | const checkSideMenuReducer = (state) => { 10 | expect(Immutable.Map.isMap(state)).toEqual(true); 11 | expect(state.size).toEqual(1); 12 | expect(state.get('isSideMenuOpen')).toBeA('boolean'); 13 | }; 14 | 15 | describe('side menu reducer', () => { 16 | it('should return the initial state', () => { 17 | expect(sideMenuReducer(undefined, {})).toEqual(initialState); 18 | }); 19 | it('should handle OPEN_SIDE_MENU', () => { 20 | const finalState = sideMenuReducer(initialState, {type: OPEN_SIDE_MENU}); 21 | expect(finalState.get('isSideMenuOpen')).toEqual(true); 22 | checkSideMenuReducer(finalState); 23 | }); 24 | it('should handle CLOSE_SIDE_MENU', () => { 25 | const finalState = sideMenuReducer(initialState, {type: CLOSE_SIDE_MENU}); 26 | expect(finalState.get('isSideMenuOpen')).toEqual(false); 27 | checkSideMenuReducer(finalState); 28 | }); 29 | it('should handle SET_VIEWPORT', () => { 30 | const finalState = sideMenuReducer(initialState, {type: SET_VIEWPORT}); 31 | expect(finalState.get('isSideMenuOpen')).toEqual(false); 32 | checkSideMenuReducer(finalState); 33 | }); 34 | it('should handle LOCATION_CHANGE', () => { 35 | const finalState = sideMenuReducer(initialState, {type: LOCATION_CHANGE}); 36 | expect(finalState.get('isSideMenuOpen')).toEqual(false); 37 | checkSideMenuReducer(finalState); 38 | }); 39 | it('should handle SUCCESS_LOGOUT', () => { 40 | const finalState = sideMenuReducer(initialState, {type: SUCCESS_LOGOUT}); 41 | expect(finalState.get('isSideMenuOpen')).toEqual(false); 42 | checkSideMenuReducer(finalState); 43 | }); 44 | it('should handle SUCCESS_LOGIN', () => { 45 | const finalState = sideMenuReducer(initialState, {type: SUCCESS_LOGIN}); 46 | expect(finalState.get('isSideMenuOpen')).toEqual(false); 47 | checkSideMenuReducer(finalState); 48 | }); 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /client/components/login/login.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { connect } from 'react-redux'; 3 | 4 | // Actions 5 | import { openModal, closeModal } from 'modals.js'; 6 | import { login } from 'auth/login.js'; 7 | 8 | // Components 9 | import ModalContainer from 'modal-container/modal-container.jsx'; 10 | 11 | let s = getStyle(); 12 | 13 | let Login = (props) => { 14 | 15 | const { isModalOpen, dispatch } = props; 16 | return ( 17 |
18 | dispatch(closeModal())}> 19 |
20 |
dispatch(login())} 22 | style={s.button}>Login with Facebook
23 |
24 | It won't work if you didn't set up the following ENV var : 25 |
26 |
27 |
28 |
FIREBASE_API_KEY
29 |
FIREBASE_AUTH_DOMAIN
30 |
FIREBASE_DATABASE_URL
31 |
32 |
33 |
34 | You also need to enable Facebook login and set up the callback url as per as firebase doc! 35 |
36 |
37 |
38 |
dispatch(openModal())} 41 | style={Object.assign({}, s.navbarItem, props.itemStyle ? props.itemStyle : {})}>Login
42 |
43 | ); 44 | }; 45 | 46 | function getStyle() { 47 | return { 48 | container: { 49 | textAlign: 'center', 50 | margin: '100px auto', 51 | }, 52 | button: { 53 | padding: 20, 54 | backgroundColor: UI.lightBlue, 55 | display: 'inline-block', 56 | cursor: 'pointer', 57 | }, 58 | navbarItem: { 59 | display: 'inline-block', 60 | padding: 20, 61 | cursor: 'pointer', 62 | }, 63 | close: { 64 | position: 'absolute', 65 | top: 20, 66 | right: 20, 67 | width: 12, 68 | }, 69 | info: { 70 | marginTop: 20, 71 | }, 72 | }; 73 | } 74 | Login.displayName = 'Login'; 75 | 76 | export default connect((state) => ({ 77 | viewport: state.get('viewport'), 78 | isModalOpen: state.get('modals').get('isLoginModalOpen')}))(Login); 79 | 80 | -------------------------------------------------------------------------------- /client/style/icon/fonts/hummingbird.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by Fontastic.me 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/reducers/login/login.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {loginReducer, initialState} from './login.js'; 3 | import { SUCCESS_LOGIN, FAIL_LOGIN, SUCCESS_LOGOUT } from 'auth/login.js'; 4 | 5 | // Check data structure 6 | const checkLoginReducer = (state) => { 7 | expect(Immutable.Map.isMap(state)).toEqual(true); 8 | expect(state.size).toEqual(5); 9 | expect(state.get('token')).toBeA('string'); 10 | expect(state.get('uid')).toBeA('string'); 11 | expect(state.get('provider')).toBeA('string'); 12 | expect(state.get('isLoggedIn')).toBeA('boolean'); 13 | expect(Immutable.Map.isMap(state.get('user'))).toEqual(true); 14 | }; 15 | 16 | const genUserPayload = () => ({ 17 | user: { 18 | photoURL: 'randomURL', 19 | uid: 'id1', 20 | displayName: 'test', 21 | email: 'test@test.com', 22 | }, 23 | credential: { 24 | accessToken: 'token', 25 | provider: 'facebook.com', 26 | }, 27 | }); 28 | 29 | describe('login reducer', () => { 30 | it('should return the initial state', () => { 31 | expect(loginReducer(undefined, {})).toEqual(initialState); 32 | }); 33 | it('should handle FAIL_LOGIN', () => { 34 | const finalState = loginReducer(initialState, {type: FAIL_LOGIN}); 35 | expect(finalState).toEqual(initialState); 36 | checkLoginReducer(finalState); 37 | }); 38 | it('should handle SUCCESS_LOGIN', () => { 39 | const userPayload = genUserPayload(); 40 | 41 | const finalState = loginReducer(initialState, { 42 | type: SUCCESS_LOGIN, 43 | payload: userPayload, 44 | }); 45 | 46 | expect(finalState.get('isLoggedIn')).toEqual(true); 47 | expect(finalState.get('token')).toEqual(userPayload.credential.accessToken); 48 | expect(finalState.get('uid')).toEqual(userPayload.user.uid); 49 | expect(finalState.get('provider')).toEqual(userPayload.credential.provider); 50 | expect(finalState.getIn(['user', 'displayName'])).toEqual(userPayload.user.displayName); 51 | expect(finalState.getIn(['user', 'email'])).toEqual(userPayload.user.email); 52 | expect(finalState.getIn(['user', 'id'])).toEqual(userPayload.user.uid); 53 | expect(finalState.getIn(['user', 'profileImageURL'])).toEqual(userPayload.user.photoURL); 54 | 55 | checkLoginReducer(finalState); 56 | }); 57 | it('should handle SUCCESS_LOGOUT', () => { 58 | const userPayload = genUserPayload(); 59 | 60 | const firstState = loginReducer(initialState, { 61 | type: SUCCESS_LOGIN, 62 | payload: userPayload, 63 | }); 64 | const finalState = loginReducer(firstState, {type: SUCCESS_LOGOUT}); 65 | expect(finalState).toEqual(initialState); 66 | checkLoginReducer(finalState); 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /client/components/toaster/toaster.jsx: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { connect } from 'react-redux'; 3 | 4 | // Actions 5 | import { removeMessage } from 'toaster.js'; 6 | 7 | let s = getStyle(); 8 | 9 | // Diff between lists 10 | const tabDiff = (tab1, tab2) => tab1.filter((current) => 11 | tab2.filter((current_b) => current_b.id === current.id).length === 0); 12 | 13 | // Get Toast style 14 | const getToastStyle = (type) => Object.assign({}, s.item, {backgroundColor: 15 | type === 'success' ? UI.lightGreen : 16 | type === 'error' ? UI.lightRed : 17 | type === 'info' ? UI.lightYellow : UI.lightGreen 18 | }); 19 | 20 | class Toaster extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = {list: this.props.list}; 24 | } 25 | componentWillReceiveProps(nextProps) { 26 | // Update Local state 27 | if (nextProps.list.length > this.props.list.length) { 28 | this.setState({list: nextProps.list}); 29 | } 30 | } 31 | componentDidUpdate(prevProps) { 32 | // Message deleted 33 | if (prevProps.list.length > this.props.list.length) { 34 | $(this.refs[`item-${tabDiff(prevProps.list, this.props.list)[0].id}`]).velocity( 35 | {opacity: 0, translateY: -30}, {duration: 200, complete: function() { 36 | this.setState({list: this.props.list}); 37 | }.bind(this) 38 | }); 39 | } else if (prevProps.list.length < this.props.list.length) { 40 | const newMessage = $(this.refs[`item-${tabDiff(this.props.list, prevProps.list)[0].id}`]); 41 | newMessage.velocity({translateY: -30}, {duration: 0, complete: function() { 42 | newMessage.velocity({opacity: 1,translateY: 0}, 200); 43 | }.bind(this), 44 | }); 45 | } 46 | } 47 | render() { 48 | 49 | return
50 | {this.state.list.map(item => 51 |
52 | {item.text} 53 | this.props.dispatch(removeMessage(item.id))} 55 | className='icon-cross-circle font-lg cursor' 56 | style={s.icon}> 57 |
)} 58 |
; 59 | } 60 | } 61 | 62 | function getStyle() { 63 | return { 64 | container: { 65 | position: 'fixed', 66 | top: 70, 67 | left: 20, 68 | right: 20, 69 | zIndex: 9999, 70 | }, 71 | item: { 72 | backgroundColor: UI.lightGreen, 73 | textAlign: 'left', 74 | marginBottom: 20, 75 | opacity: 0, 76 | padding: 15, 77 | borderRadius: 3, 78 | position: 'relative' 79 | }, 80 | icon: { 81 | position: 'absolute', 82 | right: 30, 83 | }, 84 | }; 85 | }; 86 | 87 | export default connect((state) => ({ 88 | list: state.get('toasters').toJS(), 89 | }))(Toaster); 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactogo", 3 | "version": "1.0.0", 4 | "description": "Kickstarter with react and webpack", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/karma start tests/karma-conf.js", 8 | "build": "webpack --config webpack.production.config.js --progress --display-modules", 9 | "start": "node server/start.js", 10 | "postinstall": "webpack --config webpack.production.config.js --progress --display-modules" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/PBRT/reactogo.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "redux", 19 | "immutable", 20 | "webpack", 21 | "hot", 22 | "reload", 23 | "boilerplate", 24 | "kickstarter", 25 | "motion" 26 | ], 27 | "author": "PBRT", 28 | "license": "ISC", 29 | "devDependencies": { 30 | "react-hot-loader": "1.3.0", 31 | "webpack-dev-server": "1.10.1" 32 | }, 33 | "dependencies": { 34 | "babel-core": "6.2.4", 35 | "babel-loader": "6.2.0", 36 | "babel-plugin-add-module-exports": "0.1.2", 37 | "babel-polyfill": "6.7.4", 38 | "babel-preset-es2015": "6.1.18", 39 | "babel-preset-react": "6.1.18", 40 | "body-parser": "1.14.1", 41 | "colors": "1.1.2", 42 | "compression": "1.6.0", 43 | "css-loader": "0.17.0", 44 | "del": "2.0.2", 45 | "ejs": "2.3.4", 46 | "es6-promise": "3.0.2", 47 | "eslint": "1.3.1", 48 | "eslint-loader": "1.0.0", 49 | "eslint-plugin-react": "3.3.1", 50 | "expect": "1.20.1", 51 | "exports-loader": "0.6.3", 52 | "express": "4.13.3", 53 | "fastclick": "1.0.6", 54 | "file-loader": "0.8.4", 55 | "firebase": "3.5.1", 56 | "history": "1.13.0", 57 | "immutable": "3.7.6", 58 | "imports-loader": "0.6.4", 59 | "isomorphic-fetch": "2.2.0", 60 | "jquery": "2.1.4", 61 | "json-loader": "0.5.4", 62 | "karma": "1.1.0", 63 | "karma-mocha": "1.1.1", 64 | "karma-phantomjs-launcher": "1.0.1", 65 | "karma-spec-reporter": "0.0.26", 66 | "karma-webpack": "1.7.0", 67 | "mocha": "2.5.3", 68 | "phantomjs-polyfill-object-assign": "0.0.2", 69 | "react": "15.0.1", 70 | "react-dom": "0.14.3", 71 | "react-modal": "0.6.1", 72 | "react-motion": "0.3.0", 73 | "react-redux": "4.4.4", 74 | "react-router": "2.0.1", 75 | "react-router-redux": "4.0.0", 76 | "redux": "3.0.5", 77 | "redux-devtools": "3.0.1", 78 | "redux-immutable": "3.0.6", 79 | "redux-logger": "2.3.1", 80 | "redux-promise": "0.5.0", 81 | "redux-thunk": "1.0.3", 82 | "serve-favicon": "2.3.0", 83 | "slideout": "0.1.12", 84 | "style-loader": "0.12.3", 85 | "stylus": "0.52.4", 86 | "stylus-loader": "1.2.1", 87 | "underscore": "1.8.3", 88 | "url-loader": "0.5.6", 89 | "velocity-animate": "1.2.2", 90 | "webpack": "1.12.2", 91 | "whatwg-fetch": "0.11.0" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // TODO: very basic initial setup. 3 | // most of the deactivated options (0) should become activated and the related code updated 4 | 5 | "ecmaFeatures": { 6 | "jsx": true, 7 | "modules": true 8 | }, 9 | "env": { 10 | "amd": true, 11 | "browser": true, 12 | "es6": true, 13 | "jquery": true, 14 | "jasmine": true, 15 | "node": true 16 | }, 17 | "globals": { 18 | "__VERSION__": true, 19 | "__APIURL__": true, 20 | "__HERMESURL__": true, 21 | "__ZEUSURL__": true, 22 | "__TESTABLE__": true, 23 | "__STRIPEKEY__": true, 24 | "__CLOUDINARYNAME__": true, 25 | "__CREDENTIALS__": true, 26 | "module": true, 27 | "exports": true, 28 | "_": true, 29 | "$": true, 30 | "analytics": true, 31 | "React": true, 32 | "UI": true, 33 | "Immutable": true, 34 | "jest": true, 35 | }, 36 | "plugins": [ 37 | "react" 38 | ], 39 | "rules": { 40 | "strict": 0, // best would be: [2, "global"] 41 | "comma-dangle": 0, 42 | "no-use-before-define": [2, "nofunc"], 43 | "no-undef": 2, 44 | "no-undef-init": 2, 45 | "no-undefined": 0, 46 | "no-unused-vars": 2, 47 | "no-cond-assign": 2, 48 | "no-trailing-spaces": 2, 49 | "no-debugger": 2, 50 | "no-eq-null": 1, 51 | "no-eval": 2, 52 | "no-new-func": 2, 53 | "no-unused-expressions": 2, 54 | "no-iterator": 2, 55 | "no-unreachable": 2, 56 | "no-multi-str": 2, 57 | "no-loop-func": 1, 58 | "no-proto": 2, 59 | "no-script-url": 2, 60 | "no-shadow": 0, 61 | "no-sparse-arrays": 2, 62 | "no-underscore-dangle": 0, 63 | "consistent-return": 0, 64 | "curly": 1, 65 | "eqeqeq": 1, 66 | "guard-for-in": 1, 67 | "wrap-iife": [2, "any"], 68 | "no-caller": 2, 69 | "camelcase": 0, 70 | "indent": [2, 2, {"SwitchCase": 1}], 71 | "new-cap": 0, 72 | "space-infix-ops": 0, 73 | "key-spacing": 0, 74 | "eol-last": 2, 75 | "quotes": [2, "single", "avoid-escape"], 76 | "semi": [2, "always"], 77 | "max-len": [1, 120, 4], 78 | "vars-on-top": 0, // should be true later 79 | "dot-notation": 0, // could be: [2, {"allowPattern": "^[a-z0-9]+(_[a-z0-9]+)+$"}], 80 | 81 | // react 82 | "react/display-name": 0, 83 | "react/jsx-boolean-value": 0, 84 | "react/jsx-no-undef": 0, 85 | "react/jsx-quotes": 0, 86 | "react/jsx-sort-prop-types": 0, 87 | "react/jsx-sort-props": 0, 88 | "react/jsx-uses-react": 0, 89 | "react/jsx-uses-vars": 1, 90 | "react/no-did-mount-set-state": 0, 91 | "react/no-did-update-set-state": 0, 92 | "react/no-multi-comp": 0, 93 | "react/no-unknown-property": 0, 94 | "react/prop-types": 0, 95 | "react/react-in-jsx-scope": 0, 96 | "react/require-extension": 0, 97 | "react/self-closing-comp": 0, 98 | "react/sort-comp": 0, 99 | "react/wrap-multilines": 0 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /client/containers/index/index.jsx: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { connect } from 'react-redux'; 3 | import { slideoutInst } from 'slideout.js'; 4 | import { handleStyle } from 'style.js'; 5 | 6 | // Actions 7 | import { openSideMenu, closeSideMenu } from 'side-menu.js'; 8 | 9 | //Components 10 | import Header from 'header/header.jsx'; 11 | import Toaster from 'toaster/toaster.jsx'; 12 | import SideMenu from 'side-menu/side-menu.jsx'; 13 | 14 | let s = getStyle(); 15 | let slider; 16 | 17 | export class Index extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | } 21 | componentDidMount() { 22 | if (this.props.viewport.get('isMobile')) { 23 | slider = slideoutInst(); 24 | this.attachListener(); 25 | } 26 | } 27 | attachListener() { 28 | slider.on('open', () => this.props.dispatch(openSideMenu())); 29 | slider.on('close', () => this.props.dispatch(closeSideMenu())); 30 | } 31 | removeListener() { 32 | slider.off('translatestart', 'close'); 33 | } 34 | componentDidUpdate(prevProps) { 35 | const isMobile = this.props.viewport.get('isMobile'); 36 | 37 | if (prevProps.viewport.get('isMobile') && !isMobile) { 38 | slider.destroy(); 39 | this.removeListener(); 40 | } else if (!prevProps.viewport.get('isMobile') && isMobile) { 41 | slider = slideoutInst(); 42 | this.attachListener(); 43 | } 44 | 45 | if (prevProps.isSideMenu !== this.props.isSideMenu && isMobile) { 46 | if (this.props.isSideMenu) { 47 | slider.open(); 48 | $(this.refs.overlay).velocity('fadeIn', 100); 49 | } else { 50 | slider.close(); 51 | $(this.refs.overlay).velocity('fadeOut', 100); 52 | } 53 | } 54 | } 55 | render() { 56 | const props = this.props; 57 | const isMobile = this.props.viewport.get('isMobile'); 58 | const overlayStyle = Object.assign({}, s.overlay, { 59 | height: $('body')[0].scrollHeight, 60 | }); 61 | 62 | return (
63 | 64 | {isMobile && } 65 |
66 |
67 | {isMobile &&
props.dispatch(closeSideMenu())}/>} 71 |
72 | {props.children} 73 |
74 |
75 |
); 76 | } 77 | } 78 | 79 | function getStyle() { 80 | return { 81 | overlay: { 82 | width: '100%', 83 | backgroundColor: 'rgba(0,0,0,0.4)', 84 | position: 'absolute', 85 | top: 0, 86 | left: 0, 87 | zIndex: 9999, 88 | display: 'none', 89 | }, 90 | pageContainer: { 91 | paddingTop: `${UI.headerHeight}px`, 92 | backgroundColor: UI.lightWhite, 93 | minHeight: window.innerHeight, 94 | }, 95 | }; 96 | }; 97 | 98 | Index.displayName = 'Index'; 99 | 100 | export default connect((state) => ({ 101 | isSideMenu: state.get('sideMenu').get('isSideMenuOpen'), 102 | viewport: state.get('viewport'), 103 | }))(Index); 104 | -------------------------------------------------------------------------------- /client/components/header/header.jsx: -------------------------------------------------------------------------------- 1 | // Libs 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | // Components 6 | import Login from 'login/login.jsx'; 7 | 8 | // Actions 9 | import { logout } from 'auth/login.js'; 10 | import { openSideMenu } from 'side-menu.js'; 11 | 12 | let s = getStyle(); 13 | 14 | let Header = (props) => 15 | (
16 |
17 | 18 | ReacToGo 19 | 20 |
21 | {!props.viewport.get('isMobile') ?
22 |
23 | 24 | About 25 | 26 |
27 |
28 | 29 | Fetch data 30 | 31 |
32 |
33 | 34 | Nested Routes 35 | 36 |
37 | {props.session.get('isLoggedIn') ?
38 |
props.dispatch(logout())} 40 | style={s.link} 41 | className='light-white cursor'>Logout
42 |
43 | 44 |
45 |
: } 46 |
: 47 |
48 | props.dispatch(openSideMenu())} 50 | className='icon-menu light-white cursor'> 51 |
} 52 |
); 53 | 54 | 55 | function getStyle() { 56 | return { 57 | container: { 58 | width: '100%', 59 | height: `${UI.headerHeight}px`, 60 | backgroundColor: UI.lightGreen, 61 | display: 'flex', 62 | alignItems: 'center', 63 | boxShadow: '0px 0px 2px 0px rgba(0,0,0,0.3)', 64 | position: 'fixed', 65 | zIndex: 1, 66 | top: 0, 67 | }, 68 | logo: { 69 | flex: 'initial', 70 | padding: 20, 71 | cursor: 'pointer', 72 | }, 73 | links: { 74 | flex: 1, 75 | textAlign: 'right', 76 | display: 'flex', 77 | justifyContent: 'flex-end', 78 | }, 79 | link: { 80 | cursor: 'pointer', 81 | }, 82 | profileContainer: { 83 | padding: '10px 20px', 84 | cursor: 'pointer', 85 | }, 86 | profileImage: { 87 | borderRadius: 100, 88 | width: 40, 89 | }, 90 | menuIcon: { 91 | marginRight: 20, 92 | }, 93 | anchorLink: { 94 | padding: 20, 95 | }, 96 | }; 97 | } 98 | Header.displayName = 'Header'; 99 | 100 | export default connect((state) => ({ 101 | viewport: state.get('viewport'), 102 | session: state.get('session') 103 | }))(Header); 104 | -------------------------------------------------------------------------------- /client/reducers/viewport/viewport.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {viewportReducer, initialState} from './viewport.js'; 3 | import { SET_VIEWPORT } from 'viewport.js'; 4 | 5 | const checkViewportStructure = (state) => { 6 | expect(Immutable.Map.isMap(state)).toEqual(true); 7 | expect(state.size).toEqual(4); 8 | }; 9 | 10 | describe('viewport reducer', () => { 11 | it('should return the initial state', () => { 12 | expect(viewportReducer(undefined, {})).toEqual(initialState); 13 | }); 14 | it('should handle SET_VIEWPORT for mobile', () => { 15 | const finalState = viewportReducer(initialState, { 16 | type: SET_VIEWPORT, 17 | width: 500, 18 | }); 19 | 20 | expect(finalState.get('isMobile')).toEqual(true); 21 | expect(finalState.get('isTablet')).toEqual(false); 22 | expect(finalState.get('isDesktop')).toEqual(false); 23 | expect(finalState.get('isTouchDevice')).toEqual(true); 24 | 25 | checkViewportStructure(finalState); 26 | }); 27 | it('should handle SET_VIEWPORT for tablet', () => { 28 | const finalState = viewportReducer(initialState, { 29 | type: SET_VIEWPORT, 30 | width: 800, 31 | }); 32 | 33 | expect(finalState.get('isMobile')).toEqual(false); 34 | expect(finalState.get('isTablet')).toEqual(true); 35 | expect(finalState.get('isDesktop')).toEqual(false); 36 | expect(finalState.get('isTouchDevice')).toEqual(true); 37 | 38 | checkViewportStructure(finalState); 39 | }); 40 | it('should handle SET_VIEWPORT for desktop', () => { 41 | const finalState = viewportReducer(initialState, { 42 | type: SET_VIEWPORT, 43 | width: 1100, 44 | }); 45 | 46 | expect(finalState.get('isMobile')).toEqual(false); 47 | expect(finalState.get('isTablet')).toEqual(false); 48 | expect(finalState.get('isDesktop')).toEqual(true); 49 | expect(finalState.get('isTouchDevice')).toEqual(true); 50 | 51 | checkViewportStructure(finalState); 52 | }); 53 | it('should handle SET_VIEWPORT from desktop to mobile', () => { 54 | const firstState = viewportReducer(initialState, { 55 | type: SET_VIEWPORT, 56 | width: 1100, 57 | }); 58 | 59 | const finalState = viewportReducer(firstState, { 60 | type: SET_VIEWPORT, 61 | width: 700, 62 | }); 63 | 64 | expect(finalState.get('isMobile')).toEqual(true); 65 | expect(finalState.get('isTablet')).toEqual(false); 66 | expect(finalState.get('isDesktop')).toEqual(false); 67 | expect(finalState.get('isTouchDevice')).toEqual(true); 68 | 69 | checkViewportStructure(finalState); 70 | }); 71 | it('should handle SET_VIEWPORT from mobile to tablet', () => { 72 | const firstState = viewportReducer(initialState, { 73 | type: SET_VIEWPORT, 74 | width: 700, 75 | }); 76 | 77 | const finalState = viewportReducer(firstState, { 78 | type: SET_VIEWPORT, 79 | width: 900, 80 | }); 81 | 82 | expect(finalState.get('isMobile')).toEqual(false); 83 | expect(finalState.get('isTablet')).toEqual(true); 84 | expect(finalState.get('isDesktop')).toEqual(false); 85 | expect(finalState.get('isTouchDevice')).toEqual(true); 86 | 87 | checkViewportStructure(finalState); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /client/components/spinner/path.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | export const path = 3 | 'M21.12,34.48 C31.84,34.16 35.76,26.08 35.76,17.76 C35.76,9.28 31.68,0.88 20.48,0.88 L0,0.88 L0,56 L5.36,56 L5.36,34.48 L14.64,34.48 L29.84,56 L36.32,56 L21.12,34.48 Z M20.48,6.16 C27.84,6.16 30.48,11.92 30.48,17.76 C30.48,23.6 27.84,29.2 20.08,29.2 L5.36,29.2 L5.36,6.16 L20.48,6.16 Z M56.0600002,56.8 C63.5800002,56.8 70.0600002,52.88 73.1000002,46.24 C71.4200002,45.84 69.3400002,45.44 67.7400002,45.04 C65.4200002,49.12 61.1000002,51.52 56.0600002,51.52 C49.1000002,51.52 43.5800002,46.96 42.6200002,39.68 L74.3000002,39.68 C74.3800002,38.96 74.3800002,38 74.3800002,37.2 C74.3800002,25.36 66.7000002,17.84 56.0600002,17.84 C45.4200002,17.84 37.1800002,25.36 37.1800002,37.2 C37.1800002,49.04 45.4200002,56.8 56.0600002,56.8 L56.0600002,56.8 Z M42.6200002,34.72 C43.6600002,27.6 49.1000002,23.12 56.0600002,23.12 C62.9400002,23.12 67.9000002,27.6 68.9400002,34.72 L42.6200002,34.72 Z M114.6,18.64 L109.24,18.64 L109.24,24.8 C106.44,20.08 100.6,17.84 95.1600004,17.84 C84.5200004,17.84 76.0400004,25.36 76.0400004,37.2 C76.0400004,49.04 84.5200004,56.8 95.1600004,56.8 C100.6,56.8 106.44,54.32 109.24,49.6 L109.24,56 L114.6,56 L114.6,18.64 Z M81.4000004,37.2 C81.4000004,28.64 87.4000004,23.2 95.1600004,23.2 C101.88,23.2 109.24,28 109.24,37.2 C109.24,46.4 102.76,51.44 95.1600004,51.44 C87.4000004,51.44 81.4000004,45.52 81.4000004,37.2 L81.4000004,37.2 Z M148.500001,47.92 C145.940001,50.24 142.500001,51.52 138.820001,51.52 C131.060001,51.52 124.580001,45.84 124.580001,37.2 C124.580001,28.56 131.060001,23.12 138.820001,23.12 C142.500001,23.12 145.940001,24.4 148.500001,26.64 L152.260001,22.88 C148.740001,19.68 144.020001,17.84 138.820001,17.84 C128.180001,17.84 119.300001,25.36 119.300001,37.2 C119.300001,49.04 128.180001,56.8 138.820001,56.8 C144.020001,56.8 148.820001,54.96 152.340001,51.68 L148.500001,47.92 Z M143.360001,0.88 L143.360001,6.16 L161.040001,6.16 L161.040001,56 L166.320001,56 L166.320001,6.16 L184.000001,6.16 L184.000001,0.88 L143.360001,0.88 Z M182.620001,28.56 C182.620001,44.16 194.700001,56.8 210.300001,56.8 C225.980001,56.8 238.060001,44.16 238.060001,28.56 C238.060001,12.88 225.980001,0.24 210.300001,0.24 C194.700001,0.24 182.620001,12.88 182.620001,28.56 L182.620001,28.56 Z M187.900001,28.56 C187.900001,15.84 197.580001,5.52 210.300001,5.52 C223.020001,5.52 232.780001,15.84 232.780001,28.56 C232.780001,41.28 223.020001,51.52 210.300001,51.52 C197.580001,51.52 187.900001,41.28 187.900001,28.56 L187.900001,28.56 Z M272.280001,28.96 L272.280001,33.92 L288.360001,33.92 C287.720001,44.88 280.600001,51.52 268.680001,51.52 C255.960001,51.52 246.280001,41.28 246.280001,28.56 C246.280001,15.84 255.960001,5.52 268.680001,5.52 C273.960001,5.52 278.760001,7.36 282.520001,10.4 L286.280001,6.64 C281.480001,2.64 275.400001,0.24 268.680001,0.24 C253.080001,0.24 241.000001,12.88 241.000001,28.56 C241.000001,44.16 253.080001,56.8 268.680001,56.8 C284.360001,56.8 294.280001,47.68 294.280001,28.96 L272.280001,28.96 Z M295.780001,28.56 C295.780001,44.16 307.860001,56.8 323.460001,56.8 C339.140001,56.8 351.220001,44.16 351.220001,28.56 C351.220001,12.88 339.140001,0.24 323.460001,0.24 C307.860001,0.24 295.780001,12.88 295.780001,28.56 L295.780001,28.56 Z M301.060001,28.56 C301.060001,15.84 310.740001,5.52 323.460001,5.52 C336.180001,5.52 345.940001,15.84 345.940001,28.56 C345.940001,41.28 336.180001,51.52 323.460001,51.52 C310.740001,51.52 301.060001,41.28 301.060001,28.56 L301.060001,28.56 Z'; 4 | export const pathMobile = 5 | 'M13.2,21.55 C19.9,21.35 22.35,16.3 22.35,11.1 C22.35,5.8 19.8,0.55 12.8,0.55 L0,0.55 L0,35 L3.35,35 L3.35,21.55 L9.15,21.55 L18.65,35 L22.7,35 L13.2,21.55 Z M12.8,3.85 C17.4,3.85 19.05,7.45 19.05,11.1 C19.05,14.75 17.4,18.25 12.55,18.25 L3.35,18.25 L3.35,3.85 L12.8,3.85 Z M22.6500001,0.55 L22.6500001,3.85 L33.7000001,3.85 L33.7000001,35 L37.0000001,35 L37.0000001,3.85 L48.0500001,3.85 L48.0500001,0.55 L22.6500001,0.55 Z M67.9000002,18.1 L67.9000002,21.2 L77.9500002,21.2 C77.5500002,28.05 73.1000002,32.2 65.6500002,32.2 C57.7000002,32.2 51.6500002,25.8 51.6500002,17.85 C51.6500002,9.9 57.7000002,3.45 65.6500002,3.45 C68.9500002,3.45 71.9500002,4.6 74.3000002,6.5 L76.6500002,4.15 C73.6500002,1.65 69.8500002,0.15 65.6500002,0.15 C55.9000002,0.15 48.3500002,8.05 48.3500002,17.85 C48.3500002,27.6 55.9000002,35.5 65.6500002,35.5 C75.4500002,35.5 81.6500002,29.8 81.6500002,18.1 L67.9000002,18.1 Z'; 6 | /*eslint-enable */ 7 | -------------------------------------------------------------------------------- /client/style/icon/icons-reference.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Font Reference - Hummingbird 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Hummingbird

15 |

This font was created withFontastic

16 |

CSS mapping

17 |
    18 |
  • 19 |
    20 | 21 |
  • 22 |
  • 23 |
    24 | 25 |
  • 26 |
  • 27 |
    28 | 29 |
  • 30 |
  • 31 |
    32 | 33 |
  • 34 |
  • 35 |
    36 | 37 |
  • 38 |
  • 39 |
    40 | 41 |
  • 42 |
  • 43 |
    44 | 45 |
  • 46 |
47 |

Character mapping

48 |
    49 |
  • 50 |
    51 | 52 |
  • 53 |
  • 54 |
    55 | 56 |
  • 57 |
  • 58 |
    59 | 60 |
  • 61 |
  • 62 |
    63 | 64 |
  • 65 |
  • 66 |
    67 | 68 |
  • 69 |
  • 70 |
    71 | 72 |
  • 73 |
  • 74 |
    75 | 76 |
  • 77 |
78 |
79 | 95 | 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt tag](https://raw.githubusercontent.com/PBRT/reactogo/master/logo.png) 2 | 3 | A simple Boilerplate including the best concepts and libraries of **React** and **Redux** plus some useful UI components (Toaster, Modals, Responsive Side Menu). Everything ready to build a **Performant**, **Immutable** and **Responsive** web application, including simple example of usage. Ideal for starting a project from scratch. 4 | 5 | > UI KIT concept explained in this article: [Share your happiness and your UI KIT](https://medium.com/@PierreBeard/share-your-happiness-and-your-ui-kit-5a9c92eab32a#.vfl3echw4) 6 | 7 | 8 | Check out the live version at [https://reactogo.herokuapp.com](https://reactogo.herokuapp.com). 9 | 10 | This boilerplate includes : 11 | 12 | * [Fetch API](https://github.com/github/fetch) 13 | * [Firebase](https://www.firebase.com) 14 | * [Fontastic Icons](http://fontastic.me/) 15 | * [ImmutableJS](https://facebook.github.io/immutable-js/) 16 | * [Karma Webpack](https://github.com/webpack/karma-webpack) 17 | * [React Hot Loader](http://gaearon.github.io/react-hot-loader) 18 | * [React Modal](https://github.com/rackt/react-modal) 19 | * [React Motion](https://github.com/chenglou/react-motion) 20 | * [React Router](https://github.com/rackt/react-router) 21 | * [React](https://facebook.github.io/react/) 22 | * [Redux Immutable](https://github.com/indexiatech/redux-immutablejs) 23 | * [Redux React Router](https://github.com/reactjs/react-router-redux) 24 | * [Redux](http://redux.js.org/) 25 | * [Side Menu Mobile](https://github.com/Mango/slideout) 26 | * [Stylus preprocessing](https://learnboost.github.io/stylus/) 27 | * [VelocityJS](http://julian.com/research/velocity/) 28 | * [Webpack building and webpack dev server](http://webpack.github.io/) 29 | * [ExpressJS](https://expressjs.com/api.html) 30 | * [Global UI Kit](https://medium.com/@PierreBeard/5a9c92eab32a) 31 | * SVG Spinner 32 | 33 | ## Motivations 34 | 35 | I spent a lot of time to make these differents librairies work together. For avoiding to re-do it for every projects I'm working on, I decide to build this kickstarter, and update it as soon as needed! 36 | 37 | ## Installation 38 | 39 | Simply for this project on your local machine and then : 40 | 41 | 42 | ```sh 43 | $ cd reacToGo 44 | $ npm install 45 | $ npm run start 46 | ``` 47 | 48 | And go to [localhost:3000](http://localhost:3000) in your favourite browser. 49 | It will start the ```webpack-dev-server``` on the 3000 port and proxy all the requests to your future production server (expressjs) on the port 9000. This enable to have automatic reload on server side code update. 50 | 51 | Also, the ```hot``` mode is set to true, i.e. you can update the style, the JSX code and the app will be updated keeping the state without reloading the page. 52 | 53 | ## Environement variables 54 | 55 | You need to set up your Firebase environment variable to have the login system. In your firebase app, you need to set up the facebook auth and put this in your variables : 56 | 57 | ``` 58 | export FIREBASE_API_KEY="API KEY" 59 | export FIREBASE_AUTH_DOMAIN="FULL AUTH DOMAIN" 60 | export FIREBASE_DATABASE_URL="FULL DBB URL" 61 | ``` 62 | 63 | Thanks to the ```DefinePlugin``` the ```NODE_ENV='production'``` for productions build. 64 | 65 | ## Features 66 | 67 | * Stylus: There is a default font and class in the `style` folder you can use 68 | * Images: Webpack handle the loading of all your images and files : `file-loader` 69 | * VelocityJS: Powerful animation lib to give life 70 | * React Router: Handle basic navigation between pages, part of the global state of the app. 71 | * React Motion: Physical animation lib 72 | * UI Kit: the `/style/ui-kit.json` file is included globally. You can access to the value with `UI`. It contain all the JS var needed to build your UI kit (breakpoints, animations, size...) 73 | * Media Queries: Included in the global state of the app. Accessible with functions in ```globals/style.js```. 74 | * Data : Handle by Redux in a global IMMUTABLE state of the app. Check the model section underneath. 75 | * Firebase facebook login 76 | * React Modals 77 | * React Hot Loader: Update your react components without reload the page and keeping the main state! 78 | * Side Menu responsive 79 | 80 | ## UI Kit and Customization 81 | 82 | #### UI Kit 83 | 84 | The UI KIT is defined is the ```style/ui-kit.json```. It's accessible in both JS and Stylus with create only one source of truth for the UI Kit of the app : 85 | - JS: It's loaded with the ```json-loader``` of webpack and exposed globally via the ```ProvidePlugin``` under the name of ```UI```. So you can simply use it for inline-style directly in the React components files without even require it: 86 | 87 | ```js 88 | let s = getStyle(); 89 | 90 | let MyReactComp = () =>
My React Comp
; 91 | 92 | function getStyle() { 93 | return { 94 | container: { 95 | textAlign: 'center', 96 | marginTop: 60, 97 | color: UI.lightGreen, 98 | }, 99 | }; 100 | } 101 | MyReactComp.displayName = 'MyReactComp'; 102 | 103 | export default MyReactComp; 104 | ``` 105 | 106 | 107 | - STYLUS/CSS: The same file ```style/ui-kit.json``` is also loaded in the ```style/app.styl```. So the same UI-KIT can be use also for define main app classes if needed: 108 | 109 | ```html 110 | .button 111 | padding: 10px 112 | box-shadow: inset 0px -2px 0px rgba(0,0,0,0.10) 113 | font-size: fontSM px 114 | display: inline-block 115 | border-radius: 2px 116 | text-align: center 117 | cursor: pointer 118 | 119 | @media (min-width: breakpointT px) 120 | .button 121 | padding: 10px 20px 122 | 123 | .button-primary 124 | background-color: lightGreen 125 | color: lightWhite 126 | transition: background-color 0.4s; 127 | .button-primary:hover 128 | background-color: darkGreen 129 | ``` 130 | 131 | This way, you can both use inline-style or stylus or both at the same time without any duplication of UI-KIT and then keep the things tidy! 132 | 133 | ```js 134 | let s = getStyle(); 135 | 136 | let MyReactButton = () =>
My React Button
; 137 | 138 | function getStyle() { 139 | return { 140 | container: { 141 | textAlign: 'center', 142 | marginTop: 60, 143 | color: UI.lightGreen, 144 | }, 145 | }; 146 | } 147 | MyReactButton.displayName = 'MyReactButton'; 148 | 149 | export default MyReactButton; 150 | ``` 151 | 152 | ## Redux Model 153 | 154 | Thanks to redux and its middlewares, the app state contain everything needed to modelling the UI of your app. Here is the schema of the current model (customizable of course): 155 | * Routing: Thanks to [Redux React Router](https://github.com/reactjs/react-router-redux) the current route and the params are hosted in the global state and handle via Action/Reducer. 156 | * Viewport: 157 | * isMobile: bool 158 | * isTablet: bool 159 | * isDesktop: bool 160 | Used for handle the media queries with inline css. Theses state are handled in the ```reducers/viewport-reducer.js``` reducer. Each time the window is resized, a debounced function will dispatch the action and update the state. Hence, in each components connected with [React Redux](https://github.com/rackt/react-redux) will be re-render. This way, if you use the ```handleStyle``` function contained in the ```globals/style.js``` you can describe in inline css the style of your component on three differents viewports. Check the function for more informations 161 | * Session: 162 | * Token: auth token by Firebase 163 | * uid: User id 164 | * provider: facebook 165 | * user: User info from facebook 166 | * Modals: 167 | * isLoginModalOpen: bool 168 | * Side Menu: 169 | * isSideMenuOpen: bool 170 | * Toaster: 171 | * List of messages: [] 172 | 173 | ## Redux Librairies 174 | 175 | * [Redux](http://redux.js.org/) 176 | * [React Redux](https://github.com/rackt/react-redux): Connection with React comp 177 | * [Redux React Router](https://github.com/reactjs/react-router-redux): Bridge with React Router 178 | * [Redux Thunk](https://github.com/gaearon/redux-thunk): Enable to dispatch functions 179 | * [Redux Logger](https://github.com/fcomb/redux-logger): Debugger in the console 180 | * [Redux Immutable](https://github.com/indexiatech/redux-immutablejs): Global immutable state 181 | 182 | ## Production 183 | 184 | All the build scripts are in the ```package.json``` file. If you want to build locally, simply run : 185 | 186 | ```sh 187 | $ npm run build 188 | ``` 189 | 190 | It will trigger the ```webpack.production.config.js``` build system and will put you everything under the ```dist``` folder. 191 | 192 | ## Tests 193 | 194 | The unit test are done with [Karma Webpack](https://github.com/webpack/karma-webpack) and triggered after each build (or deployments). You can launch them manually via: 195 | 196 | ```sh 197 | $ npm run test 198 | ``` 199 | 200 | The command is described in the ```package.json``` file. So far only the reducers functions are tested as examples. The current configuration will take all the files ending with ```*.spec.js``` and process these with [Karma Webpack](https://github.com/webpack/karma-webpack). 201 | 202 | It's using [Mocha](https://mochajs.org/) for its simplicity, [Expect](https://github.com/mjackson/expect) for the assertions and [PhantomJS](https://github.com/ariya/phantomjs) for running those in the terminal. 203 | 204 | The whole testing configuration is available in the ```/tests/karma-conf.js```. 205 | 206 | ## Deployment 207 | 208 | For deploying the APP, simply push it to your CI app. I will trigger the build automatically with: 209 | 210 | ```sh 211 | $ npm postinstall 212 | ``` 213 | 214 | If you are using [Heroku](https://www.heroku.com) the ```Procfile``` is already set up. 215 | 216 | If you want to do it manually, simply copy the following command and customize it if needed: 217 | ```sh 218 | webpack --config webpack.production.config.js 219 | ``` 220 | 221 | ## Webpack 222 | 223 | Here's the list of the Webpack dependencies and plugins: 224 | 225 | * [Webpack-Dev-Server](https://webpack.github.io/docs/webpack-dev-server.html): Used for development. 226 | * [DedupePlugin](https://github.com/webpack/docs/wiki/optimization) and [UglifyJsPlugin]((https://github.com/webpack/docs/wiki/optimization)): for optimizing the build size. 227 | * [ProvidePlugin](https://webpack.github.io/docs/list-of-plugins.html#provideplugin): For exposing global values such as the UI KIT or Velocity. 228 | * [DefinePlugin](https://webpack.github.io/docs/list-of-plugins.html#defineplugin) For setting up the NODE_ENV 229 | 230 | ## Contributions 231 | 232 | Every contributions is more than welcome! Simply create a PR and I will check it asap! 233 | --------------------------------------------------------------------------------