├── internals ├── generators │ ├── component │ │ ├── styles.css.hbs │ │ ├── test.js.hbs │ │ ├── stateless.js.hbs │ │ ├── es6.js.hbs │ │ └── index.js │ ├── container │ │ ├── styles.css.hbs │ │ ├── constants.js.hbs │ │ ├── test.js.hbs │ │ ├── actions.js.hbs │ │ ├── sagas.js.hbs │ │ ├── selectors.test.js.hbs │ │ ├── reducer.test.js.hbs │ │ ├── sagas.test.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── reducer.js.hbs │ │ ├── selectors.js.hbs │ │ ├── index.js.hbs │ │ └── index.js │ ├── route │ │ ├── route.hbs │ │ ├── routeWithReducer.hbs │ │ └── index.js │ ├── index.js │ └── utils │ │ └── componentExists.js ├── scripts │ ├── helpers │ │ ├── checkmark.js │ │ └── progress.js │ ├── pagespeed.js │ ├── analyze.js │ ├── setup.js │ └── clean.js ├── templates │ ├── selectors.test.js │ ├── selectors.js │ ├── homePage.js │ ├── notFoundPage.js │ ├── store.test.js │ ├── hooks.js │ ├── appContainer.js │ ├── index.html │ ├── reducers.js │ ├── routes.js │ ├── store.js │ ├── hooks.test.js │ └── app.js ├── testing │ ├── test-bundler.js │ └── karma.conf.js └── webpack │ ├── webpack.dev.babel.js │ ├── webpack.test.babel.js │ ├── webpack.base.babel.js │ └── webpack.prod.babel.js ├── app ├── favicon.ico ├── styles.css ├── reducers │ ├── index.js │ ├── alerts_reducer.js │ ├── modal_reducer.js │ └── todos_reducer.js ├── actions │ ├── alert_actions.js │ ├── tests │ │ └── modal_actions.test.js │ ├── modal_actions.js │ └── todo_actions.js ├── logic │ ├── show_filter.js │ ├── todo_logic.js │ ├── date_filter.js │ ├── tests │ │ ├── grouper.test.js │ │ └── date_filter.test.js │ └── grouper.js ├── containers │ ├── alerts │ │ ├── alert.css │ │ ├── alert_manager.js │ │ └── alert.js │ ├── add_todo_button.js │ ├── tests │ │ ├── add_todo_button.test.js │ │ ├── todolist.test.js │ │ └── todo.test.js │ ├── app.js │ ├── add_todo.js │ ├── modals │ │ ├── confirm_dialog_modal.js │ │ ├── add_todo_modal.js │ │ └── edit_todo_modal.js │ ├── modal_dialog.js │ ├── todolist.js │ ├── todo.js │ └── todo_actions.js ├── index.html ├── router.js ├── selectors │ └── filtered_todos.js ├── app.js ├── backends │ ├── TestBackend.test.js │ ├── TestBackend.js │ └── LocalBackend.js ├── .nginx.conf ├── .htaccess ├── components │ ├── todo_tabs.js │ └── filter_buttons.js ├── test │ └── test_helper.js ├── constants.js └── sagas.js ├── .editorconfig ├── .gitignore ├── .travis.yml ├── Rakefile ├── server ├── index.js ├── logger.js └── middlewares │ └── frontendMiddleware.js ├── README.md ├── .gitattributes └── package.json /internals/generators/component/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { 2 | 3 | } -------------------------------------------------------------------------------- /internals/generators/container/styles.css.hbs: -------------------------------------------------------------------------------- 1 | .{{ camelCase name }} { 2 | 3 | } -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gammons/todolist_frontend/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /app/styles.css: -------------------------------------------------------------------------------- 1 | .red { 2 | color: #af684c!important; 3 | } 4 | .blue { 5 | color: #555786 !important; 6 | } 7 | .purple { 8 | color: #8a556f !important; 9 | } 10 | -------------------------------------------------------------------------------- /internals/generators/container/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = 'app/{{ properCase name }}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | build 4 | node_modules 5 | stats.json 6 | 7 | # Cruft 8 | .DS_Store 9 | npm-debug.log 10 | .idea 11 | .todos.json 12 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import todos from './todos_reducer' 3 | import modal from './modal_reducer' 4 | import alerts from './alerts_reducer' 5 | 6 | export default combineReducers({ todos, modal, alerts }) 7 | -------------------------------------------------------------------------------- /internals/generators/component/test.js.hbs: -------------------------------------------------------------------------------- 1 | import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /internals/generators/container/test.js.hbs: -------------------------------------------------------------------------------- 1 | import {{ properCase name }} from '../index'; 2 | 3 | import expect from 'expect'; 4 | import { shallow } from 'enzyme'; 5 | import React from 'react'; 6 | 7 | describe('<{{ properCase name }} />', () => { 8 | 9 | }); 10 | -------------------------------------------------------------------------------- /internals/generators/route/route.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | getComponent(location, cb) { 4 | System.import('components/{{ properCase component }}') 5 | .then(loadModule(cb)) 6 | .catch(errorLoading); 7 | }, 8 | },$1 9 | -------------------------------------------------------------------------------- /internals/generators/container/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.js.hbs: -------------------------------------------------------------------------------- 1 | import { take, call, put, select } from 'redux-saga/effects'; 2 | 3 | // All sagas to be loaded 4 | export default [ 5 | defaultSaga, 6 | ]; 7 | 8 | // Individual exports for testing 9 | export function* defaultSaga() { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var chalk = require('chalk'); 3 | 4 | /** 5 | * Adds mark check symbol 6 | */ 7 | function addCheckMark(callback) { 8 | process.stdout.write(chalk.green(' ✓')); 9 | callback(); 10 | } 11 | 12 | module.exports = addCheckMark; 13 | -------------------------------------------------------------------------------- /app/actions/alert_actions.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | export function openAlert(body) { 4 | return { 5 | type: constants.OPEN_ALERT, 6 | body, 7 | } 8 | } 9 | 10 | export function dismissAlert() { 11 | return { 12 | type: constants.DISMISS_ALERT, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.test.js.hbs: -------------------------------------------------------------------------------- 1 | import { 2 | select{{ properCase name }}, 3 | } from '../selectors'; 4 | import { fromJS } from 'immutable'; 5 | import expect from 'expect'; 6 | 7 | const selector = select{{ properCase name}}(); 8 | 9 | describe('select{{ properCase name }}', () => { 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /app/logic/show_filter.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { SHOW_ARCHIVED } from '../constants' 3 | 4 | export default class ShowFilter { 5 | constructor(todos) { 6 | this.todos = todos 7 | } 8 | 9 | filterBy(show) { 10 | return _.filter(this.todos, (todo) => todo.archived === (show === SHOW_ARCHIVED)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/containers/alerts/alert.css: -------------------------------------------------------------------------------- 1 | .confirmationAlert { 2 | z-index: 10; 3 | height: 45px; 4 | position: absolute; 5 | right: 10px; 6 | width: 300px; 7 | background-color: #EEE; 8 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 9 | border: 1px solid #CCC; 10 | border-radius: 3px; 11 | margin-left: -5px; 12 | padding: 10px; 13 | } 14 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import {{ camelCase name }}Reducer from '../reducer'; 3 | import { fromJS } from 'immutable'; 4 | 5 | describe('{{ camelCase name }}Reducer', () => { 6 | it('returns the initial state', () => { 7 | expect({{ camelCase name }}Reducer(undefined, {})).toEqual(fromJS({})); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /internals/generators/container/sagas.test.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * Test sagas 3 | */ 4 | 5 | import expect from 'expect'; 6 | import { take, call, put, select } from 'redux-saga/effects'; 7 | import { defaultSaga } from '../sagas'; 8 | 9 | const generator = defaultSaga(); 10 | 11 | describe('defaultSaga Saga', () => { 12 | it('should .....', () => { 13 | 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /app/actions/tests/modal_actions.test.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../modal_actions' 2 | import * as constants from '../../constants' 3 | import { expect } from 'chai' 4 | 5 | describe('openAlert()', () => { 6 | it('returns an object with type ALERT', () => { 7 | const ret = actions.openModal('test') 8 | expect(ret).to.eql({ type: constants.MODAL, component: 'test' }) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Todolist 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /internals/generators/component/stateless.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantCSS}} 10 | import styles from './styles.css'; 11 | {{/if}} 12 | 13 | function {{ properCase name }}() { 14 | return ( 15 | {{#if wantCSS}} 16 |
17 | {{else}} 18 |
19 | {{/if}} 20 |
21 | ); 22 | } 23 | 24 | export default {{ properCase name }}; 25 | -------------------------------------------------------------------------------- /app/containers/add_todo_button.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { startCreateTodo } from '../actions/todo_actions' 3 | import { connect } from 'react-redux' 4 | import { Button } from 'react-bootstrap' 5 | 6 | export const AddTodoButton = (props) => 7 | 8 | 9 | AddTodoButton.propTypes = { 10 | startCreateTodo: PropTypes.func, 11 | } 12 | 13 | export default connect(null, { startCreateTodo })(AddTodoButton) 14 | -------------------------------------------------------------------------------- /internals/generators/container/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | defaultAction, 4 | } from '../actions'; 5 | import { 6 | DEFAULT_ACTION, 7 | } from '../constants'; 8 | 9 | describe('{{ properCase name }} actions', () => { 10 | describe('Default Action', () => { 11 | it('has a type of DEFAULT_ACTION', () => { 12 | const expected = { 13 | type: DEFAULT_ACTION, 14 | }; 15 | expect(defaultAction()).toEqual(expected); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /internals/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * generator/index.js 3 | * 4 | * Exports the generators so plop knows them 5 | */ 6 | 7 | const componentGenerator = require('./component/index.js'); 8 | const containerGenerator = require('./container/index.js'); 9 | const routeGenerator = require('./route/index.js'); 10 | 11 | module.exports = (plop) => { 12 | plop.setGenerator('component', componentGenerator); 13 | plop.setGenerator('container', containerGenerator); 14 | plop.setGenerator('route', routeGenerator); 15 | }; 16 | -------------------------------------------------------------------------------- /internals/generators/container/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} reducer 4 | * 5 | */ 6 | 7 | import { fromJS } from 'immutable'; 8 | import { 9 | DEFAULT_ACTION, 10 | } from './constants'; 11 | 12 | const initialState = fromJS({}); 13 | 14 | function {{ camelCase name }}Reducer(state = initialState, action) { 15 | switch (action.type) { 16 | case DEFAULT_ACTION: 17 | return state; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export default {{ camelCase name }}Reducer; 24 | -------------------------------------------------------------------------------- /internals/generators/component/es6.js.hbs: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | 9 | {{#if wantCSS}} 10 | import styles from './styles.css'; 11 | {{/if}} 12 | 13 | class {{ properCase name }} extends React.Component { 14 | render() { 15 | return ( 16 | {{#if wantCSS}} 17 |
18 | {{else}} 19 |
20 | {{/if}} 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default {{ properCase name }}; 27 | -------------------------------------------------------------------------------- /internals/generators/utils/componentExists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * componentExists 3 | * 4 | * Check whether the given component exist in either the components or containers directory 5 | */ 6 | 7 | const fs = require('fs'); 8 | const pageComponents = fs.readdirSync('app/components'); 9 | const pageContainers = fs.readdirSync('app/containers'); 10 | const components = pageComponents.concat(pageContainers); 11 | 12 | function componentExists(comp) { 13 | return components.indexOf(comp) >= 0; 14 | } 15 | 16 | module.exports = componentExists; 17 | -------------------------------------------------------------------------------- /internals/templates/selectors.test.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import expect from 'expect'; 3 | 4 | import { selectLocationState } from 'containers/App/selectors'; 5 | 6 | describe('selectLocationState', () => { 7 | it('should select the route as a plain JS object', () => { 8 | const route = fromJS({ 9 | locationBeforeTransitions: null, 10 | }); 11 | const mockedState = fromJS({ 12 | route, 13 | }); 14 | expect(selectLocationState()(mockedState)).toEqual(route.toJS()); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/reducers/alerts_reducer.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | const INITIAL_STATE = { 4 | openAlerts: [], 5 | } 6 | 7 | export default (state = INITIAL_STATE, action) => { 8 | switch (action.type) { 9 | case constants.OPEN_ALERT: 10 | return { 11 | openAlerts: [...state.openAlerts, { body: action.body }], 12 | } 13 | case constants.DISMISS_ALERT: 14 | return { 15 | ...state, 16 | openAlerts: state.openAlerts.slice(1), 17 | } 18 | default: 19 | return state 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | dist: trusty 4 | node_js: 5 | - "5.0" 6 | script: npm run build 7 | before_install: 8 | - export CHROME_BIN=/usr/bin/google-chrome 9 | - export DISPLAY=:99.0 10 | - sudo apt-get update 11 | - sudo apt-get install -y libappindicator1 fonts-liberation 12 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 13 | - sudo dpkg -i google-chrome*.deb 14 | - sh -e /etc/init.d/xvfb start 15 | notifications: 16 | email: 17 | on_failure: change 18 | after_success: 'npm run coveralls' 19 | -------------------------------------------------------------------------------- /internals/templates/selectors.js: -------------------------------------------------------------------------------- 1 | // selectLocationState expects a plain JS object for the routing state 2 | const selectLocationState = () => { 3 | let prevRoutingState; 4 | let prevRoutingStateJS; 5 | 6 | return (state) => { 7 | const routingState = state.get('route'); // or state.route 8 | 9 | if (!routingState.equals(prevRoutingState)) { 10 | prevRoutingState = routingState; 11 | prevRoutingStateJS = routingState.toJS(); 12 | } 13 | 14 | return prevRoutingStateJS; 15 | }; 16 | }; 17 | 18 | export { 19 | selectLocationState, 20 | }; 21 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Router, Route, browserHistory, IndexRedirect } from 'react-router' 3 | import App from './containers/app' 4 | import AddTodo from './containers/add_todo' 5 | import Todolist from './containers/todolist' 6 | 7 | const TodoRouter = () => 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | export default TodoRouter 17 | -------------------------------------------------------------------------------- /app/actions/modal_actions.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | export function openModal(component, extraProps = {}) { 4 | return { 5 | type: constants.MODAL, 6 | component, 7 | ...extraProps, 8 | } 9 | } 10 | 11 | export function openConfirmDialog(body) { 12 | return { 13 | type: constants.CONFIRM_DIALOG_MODAL, 14 | body, 15 | } 16 | } 17 | 18 | export function cancelConfirmDialog() { 19 | return { 20 | type: constants.CONFIRM_DIALOG_CANCEL, 21 | } 22 | } 23 | 24 | export function okConfirmDialog() { 25 | return { 26 | type: constants.CONFIRM_DIALOG_OK, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/reducers/modal_reducer.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | 3 | const INITIAL_STATE = { 4 | open: false, 5 | } 6 | 7 | export default (state = INITIAL_STATE, action) => { 8 | switch (action.type) { 9 | case constants.MODAL: 10 | return { ...state, open: true, component: action.component, todo: action.todo } 11 | 12 | case constants.CONFIRM_DIALOG_MODAL: 13 | return { ...state, open: true, component: constants.CONFIRM_DIALOG_MODAL, body: action.body } 14 | 15 | case constants.CONFIRM_DIALOG_OK: 16 | case constants.CONFIRM_DIALOG_CANCEL: 17 | return { open: false } 18 | 19 | default: return state 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internals/generators/container/selectors.js.hbs: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * Direct selector to the {{ camelCase name }} state domain 5 | */ 6 | const select{{ properCase name }}Domain = () => state => state.get('{{ camelCase name }}'); 7 | 8 | /** 9 | * Other specific selectors 10 | */ 11 | 12 | 13 | /** 14 | * Default selector used by {{ properCase name }} 15 | */ 16 | 17 | const select{{ properCase name }} = () => createSelector( 18 | select{{ properCase name }}Domain(), 19 | (substate) => substate.toJS() 20 | ); 21 | 22 | export default select{{ properCase name }}; 23 | export { 24 | select{{ properCase name }}Domain, 25 | }; 26 | -------------------------------------------------------------------------------- /internals/templates/homePage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * HomePage 3 | * 4 | * This is the first thing users see of our App, at the '/' route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | 14 | /* eslint-disable react/prefer-stateless-function */ 15 | export default class HomePage extends React.Component { 16 | 17 | render() { 18 | return ( 19 |

This is the Homepage!

20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internals/templates/notFoundPage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | * 6 | * NOTE: while this component should technically be a stateless functional 7 | * component (SFC), hot reloading does not currently support SFCs. If hot 8 | * reloading is not a neccessity for you then you can refactor it and remove 9 | * the linting exception. 10 | */ 11 | 12 | import React from 'react'; 13 | 14 | /* eslint-disable react/prefer-stateless-function */ 15 | export default class NotFound extends React.Component { 16 | 17 | render() { 18 | return ( 19 |

Page Not Found

20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internals/testing/test-bundler.js: -------------------------------------------------------------------------------- 1 | // needed for regenerator-runtime 2 | // (ES7 generator support is required by redux-saga) 3 | import 'babel-polyfill'; 4 | 5 | import sinon from 'sinon'; 6 | import chai from 'chai'; 7 | import chaiEnzyme from 'chai-enzyme'; 8 | 9 | chai.use(chaiEnzyme()); 10 | 11 | global.chai = chai; 12 | global.sinon = sinon; 13 | global.expect = chai.expect; 14 | global.should = chai.should(); 15 | 16 | // Include all .js files under `app`, except app.js, reducers.js, routes.js and 17 | // store.js. This is for isparta code coverage 18 | const context = require.context('../../app', true, /^^((?!(app|reducers|routes|store)).)*\.js$/); 19 | context.keys().forEach(context); 20 | -------------------------------------------------------------------------------- /app/selectors/filtered_todos.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect' 2 | import Grouper from '../logic/grouper' 3 | import DateFilter from '../logic/date_filter' 4 | import ShowFilter from '../logic/show_filter' 5 | 6 | const todosSelector = state => state.todos.todos 7 | const filtersSelector = (state, props) => props.params 8 | 9 | const filterTodos = (todos, filter) => { 10 | const showFilter = new ShowFilter(todos) 11 | const dateFilter = new DateFilter(showFilter.filterBy(filter.show)) 12 | const grouper = new Grouper(dateFilter.filterBy(filter.due)) 13 | return grouper.grouped(filter.group) 14 | } 15 | 16 | export default createSelector( 17 | todosSelector, 18 | filtersSelector, 19 | filterTodos 20 | ) 21 | -------------------------------------------------------------------------------- /internals/templates/store.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test store addons 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from './store'; 7 | import { browserHistory } from 'react-router'; 8 | 9 | describe('configureStore', () => { 10 | let store; 11 | 12 | before(() => { 13 | store = configureStore({}, browserHistory); 14 | }); 15 | 16 | describe('asyncReducers', () => { 17 | it('should contain an object for async reducers', () => { 18 | expect(typeof store.asyncReducers).toEqual('object'); 19 | }); 20 | }); 21 | 22 | describe('runSaga', () => { 23 | it('should contain a hook for `sagaMiddleware.run`', () => { 24 | expect(typeof store.runSaga).toEqual('function'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var readline = require('readline'); 4 | 5 | /** 6 | * Adds an animated progress indicator 7 | * 8 | * @param {string} message The message to write next to the indicator 9 | * @param {number} amountOfDots The amount of dots you want to animate 10 | */ 11 | function animateProgress(message, amountOfDots) { 12 | if (typeof amountOfDots !== 'number') { 13 | amountOfDots = 3; 14 | } 15 | 16 | var i = 0; 17 | return setInterval(function () { 18 | readline.cursorTo(process.stdout, 0); 19 | i = (i + 1) % (amountOfDots + 1); 20 | var dots = new Array(i + 1).join("."); 21 | process.stdout.write(message + dots); 22 | }, 500); 23 | } 24 | 25 | module.exports = animateProgress; 26 | -------------------------------------------------------------------------------- /internals/templates/hooks.js: -------------------------------------------------------------------------------- 1 | import createReducer from 'reducers.js'; 2 | 3 | /** 4 | * Inject an asynchronously loaded reducer 5 | */ 6 | export function injectAsyncReducer(store) { 7 | return (name, asyncReducer) => { 8 | store.asyncReducers[name] = asyncReducer; // eslint-disable-line 9 | store.replaceReducer(createReducer(store.asyncReducers)); 10 | }; 11 | } 12 | 13 | /** 14 | * Inject an asynchronously loaded saga 15 | */ 16 | export function injectAsyncSagas(store) { 17 | return (sagas) => sagas.map(store.runSaga); 18 | } 19 | 20 | /** 21 | * Helper for creating injectors 22 | */ 23 | export function getHooks(store) { 24 | return { 25 | injectReducer: injectAsyncReducer(store), 26 | injectSagas: injectAsyncSagas(store), 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /app/containers/alerts/alert_manager.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | import Alert from './alert' 5 | 6 | class AlertManager extends Component { 7 | static get propTypes() { 8 | return { 9 | alerts: PropTypes.object, 10 | } 11 | } 12 | 13 | renderAlert(alert, idx) { 14 | return 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 | {this.props.alerts.openAlerts.map(this.renderAlert)} 21 |
22 | ) 23 | } 24 | } 25 | 26 | function mapStateToProps(state) { 27 | return { 28 | alerts: state.alerts, 29 | } 30 | } 31 | 32 | export default connect(mapStateToProps)(AlertManager) 33 | -------------------------------------------------------------------------------- /internals/generators/route/routeWithReducer.hbs: -------------------------------------------------------------------------------- 1 | { 2 | path: '{{ path }}', 3 | name: '{{ camelCase component }}', 4 | getComponent(nextState, cb) { 5 | const importModules = Promise.all([ 6 | System.import('containers/{{ properCase component }}/reducer'), 7 | System.import('containers/{{ properCase component }}/sagas'), 8 | System.import('containers/{{ properCase component }}'), 9 | ]); 10 | 11 | const renderRoute = loadModule(cb); 12 | 13 | importModules.then(([reducer, sagas, component]) => { 14 | injectReducer('{{ camelCase component }}', reducer.default); 15 | injectSagas(sagas.default); 16 | renderRoute(component); 17 | }); 18 | 19 | importModules.catch(errorLoading); 20 | }, 21 | },$1 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | Version = "0.6.1" 2 | 3 | desc "Builds the production app" 4 | task :build do 5 | system `rm -rf build/*` 6 | system `yarn run build` 7 | end 8 | 9 | desc "Uploads to S3" 10 | task :upload do 11 | `mv build/common*.js build/common.js` 12 | `mv build/main*.js build/main.js` 13 | `mv build/main*.css build/main.css` 14 | `rm -rf build/appcache` 15 | 16 | Dir.foreach("./build") do |file| 17 | next if [".",".."].include?(file) 18 | puts "uploading #{file}" 19 | `aws s3 cp build/#{file} s3://todolist-local/#{Version}/#{file} --acl public-read` 20 | end 21 | end 22 | 23 | desc "Uploads the demo to s3" 24 | task :upload_demo do 25 | Dir.foreach("./build") do |file| 26 | `aws s3 cp build/#{file} s3://todolist-demo/#{file} --acl public-read` 27 | end 28 | end 29 | 30 | task default: [:build, :upload] 31 | -------------------------------------------------------------------------------- /app/containers/alerts/alert.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { dismissAlert } from '../../actions/alert_actions' 4 | 5 | import styles from './alert.css' 6 | 7 | class Alert extends Component { 8 | static get propTypes() { 9 | return { 10 | dismissAlert: PropTypes.func, 11 | id: PropTypes.number, 12 | body: PropTypes.string, 13 | idx: PropTypes.number, 14 | } 15 | } 16 | 17 | componentWillMount() { 18 | setTimeout(this.props.dismissAlert, 2000) 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |

{this.props.body}

25 |
26 | ) 27 | } 28 | } 29 | 30 | export default connect(null, { dismissAlert })(Alert) 31 | -------------------------------------------------------------------------------- /internals/scripts/pagespeed.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable */ 4 | 5 | process.stdin.resume(); 6 | process.stdin.setEncoding('utf8'); 7 | 8 | var ngrok = require('ngrok'); 9 | var psi = require('psi'); 10 | var chalk = require('chalk'); 11 | 12 | log('\nStarting ngrok tunnel'); 13 | 14 | startTunnel(runPsi); 15 | 16 | function runPsi(url) { 17 | log('\nStarting PageSpeed Insights'); 18 | psi.output(url).then(function (err) { 19 | process.exit(0); 20 | }); 21 | } 22 | 23 | function startTunnel(cb) { 24 | ngrok.connect(3000, function (err, url) { 25 | if (err) { 26 | log(chalk.red('\nERROR\n' + err)); 27 | process.exit(0); 28 | } 29 | 30 | log('\nServing tunnel from: ' + chalk.magenta(url)); 31 | cb(url); 32 | }); 33 | } 34 | 35 | function log(string) { 36 | process.stdout.write(string); 37 | } 38 | -------------------------------------------------------------------------------- /internals/templates/appContainer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.react.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | * NOTE: while this component should technically be a stateless functional 9 | * component (SFC), hot reloading does not currently support SFCs. If hot 10 | * reloading is not a neccessity for you then you can refactor it and remove 11 | * the linting exception. 12 | */ 13 | 14 | import React from 'react'; 15 | 16 | /* eslint-disable react/prefer-stateless-function */ 17 | export default class App extends React.Component { 18 | 19 | static propTypes = { 20 | children: React.PropTypes.node, 21 | }; 22 | 23 | render() { 24 | return ( 25 |
26 | {this.props.children} 27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internals/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React.js Boilerplate 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /internals/generators/container/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{ properCase name }} 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { connect } from 'react-redux'; 9 | import select{{properCase name}} from './selectors'; 10 | {{#if wantCSS}} 11 | import styles from './styles.css'; 12 | {{/if}} 13 | 14 | export class {{ properCase name }} extends React.Component { // eslint-disable-line react/prefer-stateless-function 15 | render() { 16 | return ( 17 | {{#if wantCSS}} 18 |
19 | {{else}} 20 |
21 | {{/if}} 22 | This is {{properCase name}} container ! 23 |
24 | ); 25 | } 26 | } 27 | 28 | const mapStateToProps = select{{properCase name}}(); 29 | 30 | function mapDispatchToProps(dispatch) { 31 | return { 32 | dispatch, 33 | }; 34 | } 35 | 36 | export default connect(mapStateToProps, mapDispatchToProps)({{ properCase name }}); 37 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { createStore, applyMiddleware, compose } from 'redux' 5 | import { Provider } from 'react-redux' 6 | 7 | import createSagaMiddleware from 'redux-saga' 8 | 9 | import todoSagas from './sagas' 10 | import TodoRouter from './router' 11 | import reducers from './reducers' 12 | 13 | const sagaMiddleware = createSagaMiddleware() 14 | const devTools = window.devToolsExtension || (() => noop => noop) 15 | 16 | const middlewares = [ 17 | sagaMiddleware, 18 | ] 19 | 20 | const enhancers = [ 21 | applyMiddleware(...middlewares), 22 | devTools(), 23 | ] 24 | 25 | const store = createStore( 26 | reducers, 27 | compose(...enhancers) 28 | ) 29 | 30 | sagaMiddleware.run(todoSagas) 31 | 32 | const App = () => 33 | 34 | 35 | 36 | 37 | ReactDOM.render(, document.getElementById('app')) 38 | -------------------------------------------------------------------------------- /internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable */ 3 | 4 | var shelljs = require('shelljs'); 5 | var animateProgress = require('./helpers/progress'); 6 | var chalk = require('chalk'); 7 | var addCheckMark = require('./helpers/checkmark'); 8 | 9 | var progress = animateProgress('Generating stats'); 10 | 11 | // Generate stats.json file with webpack 12 | shelljs.exec( 13 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 14 | addCheckMark.bind(null, callback) // Output a checkmark on completion 15 | ); 16 | 17 | // Called after webpack has finished generating the stats.json file 18 | function callback() { 19 | clearInterval(progress); 20 | process.stdout.write( 21 | '\n\nOpen ' + chalk.magenta('http://webpack.github.io/analyse/') + ' in your browser and upload the stats.json file!' + 22 | chalk.blue('\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n') 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /app/containers/tests/add_todo_button.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AddTodoButton } from '../add_todo_button' 3 | import expect from 'expect' 4 | import { mount } from 'enzyme' 5 | 6 | function setup() { 7 | const props = { 8 | startCreateTodo: expect.createSpy(), 9 | } 10 | const enzymeWrapper = mount() 11 | 12 | return { props, enzymeWrapper } 13 | } 14 | 15 | describe('', () => { 16 | it('has an onClick function', () => { 17 | const { enzymeWrapper } = setup() 18 | const props = enzymeWrapper.find('Button').props() 19 | expect(props.onClick).toBeA('function') 20 | }) 21 | 22 | it('calls startCreateTodo if clicked', () => { 23 | const { enzymeWrapper, props } = setup() 24 | const button = enzymeWrapper.find('Button') 25 | expect(props.startCreateTodo.calls.length).toBe(0) 26 | button.props().onClick() 27 | expect(props.startCreateTodo.calls.length).toBe(1) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /app/containers/app.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Navbar, Grid, Col } from 'react-bootstrap' 3 | 4 | import TodoTabs from '../components/todo_tabs' 5 | import ModalDialog from './modal_dialog' 6 | import AlertManager from './alerts/alert_manager' 7 | import AddTodoButton from './add_todo_button' 8 | 9 | const App = (props) => { 10 | const { due, show, group } = props.params 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | Todolist 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {props.children} 28 | 29 | 30 | 31 | 32 |
33 | ) 34 | } 35 | 36 | App.contextTypes = { 37 | router: PropTypes.object, 38 | } 39 | 40 | App.propTypes = { 41 | children: PropTypes.object, 42 | params: PropTypes.object, 43 | } 44 | 45 | export default App 46 | -------------------------------------------------------------------------------- /app/reducers/todos_reducer.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | import _ from 'lodash' 3 | 4 | const INITIAL_STATE = { 5 | todos: [], 6 | } 7 | 8 | export default (state = INITIAL_STATE, action) => { 9 | switch (action.type) { 10 | 11 | case constants.ADD_TODO: 12 | return { ...state, todos: [...state.todos, action.todo] } 13 | 14 | case constants.UPDATE_TODO: 15 | return updateTodo(state, action) 16 | 17 | case constants.DELETE_TODO: 18 | return deleteTodo(state, action) 19 | 20 | case constants.TODOS_FETCHED: 21 | return { ...state, todos: action.payload } 22 | 23 | default: 24 | return state 25 | } 26 | } 27 | 28 | const updateTodo = (state, action) => { 29 | const idx = _.findIndex(state.todos, (todo) => todo.id === action.todo.id) 30 | return { ...state, todos: [...state.todos.slice(0, idx), _.cloneDeep(action.todo), ...state.todos.slice(idx + 1)] } 31 | } 32 | 33 | const deleteTodo = (state, action) => { 34 | const idx = _.findIndex(state.todos, (todo) => todo.id === action.todo.id) 35 | return { ...state, todos: [...state.todos.slice(0, idx), ...state.todos.slice(idx + 1)] } 36 | } 37 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | const ngrok = require('ngrok'); 6 | 7 | const frontend = require('./middlewares/frontendMiddleware'); 8 | const isDev = process.env.NODE_ENV !== 'production'; 9 | 10 | const app = express(); 11 | 12 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 13 | // app.use('/api', myApi); 14 | 15 | // Initialize frontend middleware that will serve your JS app 16 | const webpackConfig = isDev 17 | ? require('../internals/webpack/webpack.dev.babel') 18 | : require('../internals/webpack/webpack.prod.babel'); 19 | 20 | app.use(frontend(webpackConfig)); 21 | 22 | const port = process.env.PORT || 3000; 23 | 24 | // Start your app. 25 | app.listen(port, (err) => { 26 | if (err) { 27 | return logger.error(err); 28 | } 29 | 30 | // Connect to ngrok in dev mode 31 | if (isDev) { 32 | ngrok.connect(port, (innerErr, url) => { 33 | if (innerErr) { 34 | return logger.error(innerErr); 35 | } 36 | 37 | logger.appStarted(port, url); 38 | }); 39 | } else { 40 | logger.appStarted(port); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /app/containers/tests/todolist.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import expect from 'expect' 3 | import { mount } from 'enzyme' 4 | import { Provider } from 'react-redux' 5 | 6 | import { fakeStore, todos as todoFixtures } from '../../test/test_helper' 7 | import * as constants from '../../constants' 8 | 9 | import Todolist from '../todolist' 10 | 11 | function setup(todos = []) { 12 | const store = fakeStore({ todos: { todos } }) 13 | const props = { 14 | params: { show: constants.SHOW_UNARCHIVED, due: constants.ALL, group: constants.ALL }, 15 | } 16 | 17 | const todolist = mount() 18 | return { props, todolist } 19 | } 20 | 21 | describe('', () => { 22 | it('shows todos', () => { 23 | const { todolist } = setup([todoFixtures[0]]) 24 | expect(todolist.find('h3').text()).toEqual('All todos') 25 | expect(todolist.find('p').text()).toEqual('Call with @Bob and @Frank about +bigProject') 26 | }) 27 | 28 | it('shows "No todos" if there are no todos', () => { 29 | const { todolist } = setup() 30 | expect(todolist.find('h3').text()).toEqual('All todos') 31 | expect(todolist.find('p').text()).toEqual('No todos.') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /app/containers/add_todo.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import moment from 'moment' 3 | import { connect } from 'react-redux' 4 | 5 | import { createTodo } from '../actions/todo_actions' 6 | import Todolist from './todolist' 7 | 8 | class AddTodo extends Component { 9 | 10 | static get propTypes() { 11 | return { 12 | createTodo: PropTypes.func, 13 | due: PropTypes.string, 14 | location: PropTypes.object, 15 | } 16 | } 17 | 18 | handleSave() { 19 | this.props.createTodo(this.refs.subject.getValue(), this.refs.due.getDate()) 20 | } 21 | 22 | dueDate() { 23 | if (this.props.due === undefined) { 24 | return new Date() 25 | } 26 | return new Date(`${this.props.due} 05:00:00 GMT-${new Date().getTimezoneOffset() / 60}`) 27 | } 28 | 29 | formatDate(date) { 30 | return moment(date).format('ddd MMM D') 31 | } 32 | 33 | render() { 34 | const { due, show, group } = this.props.location.state 35 | const focus = () => this.refs.subject.focus() 36 | setTimeout(focus.bind(this), 200) 37 | 38 | return ( 39 |
40 | add todo 41 | 42 |
43 | ) 44 | } 45 | } 46 | 47 | export default connect(null, { createTodo })(AddTodo) 48 | -------------------------------------------------------------------------------- /app/containers/modals/confirm_dialog_modal.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Modal, Button } from 'react-bootstrap' 3 | import { okConfirmDialog, cancelConfirmDialog } from '../../actions/modal_actions' 4 | import { connect } from 'react-redux' 5 | 6 | class ConfirmDialogModal extends Component { 7 | static get propTypes() { 8 | return { 9 | okConfirmDialog: PropTypes.func, 10 | cancelConfirmDialog: PropTypes.func, 11 | modal: PropTypes.object, 12 | } 13 | } 14 | 15 | constructor() { 16 | super() 17 | this.handleOk = this.handleOk.bind(this) 18 | this.handleCancel = this.handleCancel.bind(this) 19 | } 20 | 21 | handleOk() { 22 | this.props.okConfirmDialog() 23 | } 24 | handleCancel() { 25 | this.props.cancelConfirmDialog() 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 | 32 | {this.props.modal.body} 33 | 34 | 35 | 36 | 37 | 38 |
39 | ) 40 | } 41 | } 42 | 43 | export default connect(null, { okConfirmDialog, cancelConfirmDialog })(ConfirmDialogModal) 44 | -------------------------------------------------------------------------------- /app/backends/TestBackend.test.js: -------------------------------------------------------------------------------- 1 | import TestBackend from './TestBackend' 2 | 3 | import expect from 'expect' 4 | 5 | describe('fetchTodos()', () => { 6 | it('fetches todos', (done) => { 7 | const backend = new TestBackend() 8 | backend.fetchTodos().then((todos) => { 9 | expect(todos.length).toEqual(6) 10 | done() 11 | }) 12 | }) 13 | }) 14 | 15 | describe('add()', () => { 16 | it('adds a todo', (done) => { 17 | const backend = new TestBackend() 18 | const todo = { id: 10, subject: 'yet another thing', completed: false } 19 | 20 | backend.add(todo).then(() => { 21 | backend.fetchTodos().then((todos) => { 22 | expect(todos.length).toEqual(7) 23 | expect(todos[todos.length - 1].subject).toEqual('yet another thing') 24 | done() 25 | }) 26 | }) 27 | }) 28 | }) 29 | 30 | describe('update()', () => { 31 | it('updates a todo', (done) => { 32 | const backend = new TestBackend() 33 | 34 | backend.fetchTodos().then((todos) => { 35 | const todo = todos[0] 36 | todo.subject = 'updated' 37 | 38 | backend.update(todo).then(() => { 39 | backend.fetchTodos().then((newTodos) => { 40 | expect(newTodos[0].subject).toEqual('updated') 41 | done() 42 | }) 43 | }) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /app/logic/todo_logic.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import _ from 'lodash' 3 | 4 | export default class TodoLogic { 5 | addTodo(subject, inDue) { 6 | let due = inDue 7 | 8 | if (due != null) { 9 | due = moment(due).format('YYYY-MM-DD') 10 | } 11 | const todo = { subject, due, projects: [], contexts: [], completed: false, archived: false } 12 | todo.contexts = this.getContexts(todo.subject) 13 | todo.projects = this.getProjects(todo.subject) 14 | return todo 15 | } 16 | 17 | updateTodo(inTodo) { 18 | const todo = inTodo 19 | 20 | todo.contexts = this.getContexts(todo.subject) 21 | todo.projects = this.getProjects(todo.subject) 22 | 23 | if (todo.due != null) { 24 | todo.due = moment(todo.due).format('YYYY-MM-DD') 25 | } 26 | return todo 27 | } 28 | 29 | getContexts(subject) { 30 | const regex = /@\w+/g 31 | const matches = subject.match(regex) 32 | if (matches === null) { 33 | return [] 34 | } 35 | return _.map(matches, (match) => match.replace(/@/, '')) 36 | } 37 | 38 | getProjects(subject) { 39 | const regex = /\+\w+/g 40 | const matches = subject.match(regex) 41 | if (matches === null) { 42 | return [] 43 | } 44 | return _.map(matches, (match) => match.replace(/\+/, '')) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internals/templates/reducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | * If we were to do this in store.js, reducers wouldn't be hot reloadable. 4 | */ 5 | 6 | import { combineReducers } from 'redux-immutable'; 7 | import { fromJS } from 'immutable'; 8 | import { LOCATION_CHANGE } from 'react-router-redux'; 9 | 10 | /* 11 | * routeReducer 12 | * 13 | * The reducer merges route location changes into our immutable state. 14 | * The change is necessitated by moving to react-router-redux@4 15 | * 16 | */ 17 | 18 | // Initial routing state 19 | const routeInitialState = fromJS({ 20 | locationBeforeTransitions: null, 21 | }); 22 | 23 | /** 24 | * Merge route into the global application state 25 | */ 26 | function routeReducer(state = routeInitialState, action) { 27 | switch (action.type) { 28 | /* istanbul ignore next */ 29 | case LOCATION_CHANGE: 30 | return state.merge({ 31 | locationBeforeTransitions: action.payload, 32 | }); 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | /** 39 | * Creates the main reducer with the asynchronously loaded ones 40 | */ 41 | export default function createReducer(asyncReducers) { 42 | return combineReducers({ 43 | route: routeReducer, 44 | ...asyncReducers, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console,prefer-template */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | 13 | // Called whenever there's an error on the server we want to print 14 | error: err => { 15 | console.log(chalk.red(err)); 16 | }, 17 | 18 | // Called when express.js app starts on given port w/o errors 19 | appStarted: (port, tunnelStarted) => { 20 | console.log('Server started ' + chalk.green('✓')); 21 | 22 | // If the tunnel started, log that and the URL it's available at 23 | if (tunnelStarted) { 24 | console.log('Tunnel initialised ' + chalk.green('✓')); 25 | } 26 | 27 | console.log( 28 | chalk.bold('\nAccess URLs:') + 29 | divider + 30 | '\nLocalhost: ' + chalk.magenta('http://localhost:' + port) + 31 | '\n LAN: ' + chalk.magenta('http://' + ip.address() + ':' + port) + 32 | (tunnelStarted ? '\n Proxy: ' + chalk.magenta(tunnelStarted) : '') + 33 | divider, 34 | chalk.blue('\nPress ' + chalk.italic('CTRL-C') + ' to stop\n') 35 | ); 36 | }, 37 | }; 38 | 39 | module.exports = logger; 40 | -------------------------------------------------------------------------------- /app/backends/TestBackend.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { todos as todoFixtures } from '../test/test_helper' 3 | 4 | let todos = todoFixtures 5 | 6 | export default class TestBackend { 7 | constructor() { 8 | this.cachedTodos = null 9 | } 10 | 11 | fetchTodos() { 12 | return new Promise((resolve) => { 13 | if (this.cachedTodos) { 14 | resolve(this.cachedTodos) 15 | } else { 16 | const yes = () => { 17 | this.cachedTodos = _.cloneDeep(todos) 18 | resolve(this.cachedTodos) 19 | } 20 | setTimeout(yes, 400) 21 | } 22 | }) 23 | } 24 | 25 | add(todo) { 26 | return new Promise((resolve) => { 27 | todos.push(todo) 28 | this.cachedTodos = null 29 | resolve() 30 | }) 31 | } 32 | 33 | update(todo) { 34 | return new Promise((resolve) => { 35 | this.cachedTodos = null 36 | const idx = _.findIndex(todos, (t) => t.id === todo.id) 37 | todos = [...todos.slice(0, idx), todo, ...todos.slice(idx + 1)] 38 | resolve() 39 | }) 40 | } 41 | 42 | delete(todo) { 43 | return new Promise((resolve) => { 44 | this.cachedTodos = null 45 | const idx = _.findIndex(todos, (t) => t.id === todo.id) 46 | todos = [...todos.slice(0, idx), ...todos.slice(idx + 1)] 47 | resolve() 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internals/testing/karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('../webpack/webpack.test.babel'); 2 | const path = require('path'); 3 | 4 | module.exports = (config) => { 5 | config.set({ 6 | frameworks: ['mocha'], 7 | reporters: ['coverage', 'mocha'], 8 | /*eslint-disable */ 9 | browsers: process.env.TRAVIS 10 | ? ['ChromeTravis'] 11 | : process.env.APPVEYOR 12 | ? ['IE'] : ['Chrome'], 13 | /*eslint-enable */ 14 | 15 | autoWatch: false, 16 | singleRun: true, 17 | 18 | files: [ 19 | { 20 | pattern: './test-bundler.js', 21 | watched: false, 22 | served: true, 23 | included: true, 24 | }, 25 | ], 26 | 27 | preprocessors: { 28 | ['./test-bundler.js']: ['webpack', 'sourcemap'], // eslint-disable-line no-useless-computed-key 29 | }, 30 | 31 | webpack: webpackConfig, 32 | 33 | // make Webpack bundle generation quiet 34 | webpackMiddleware: { 35 | noInfo: true, 36 | }, 37 | 38 | customLaunchers: { 39 | ChromeTravis: { 40 | base: 'Chrome', 41 | flags: ['--no-sandbox'], 42 | }, 43 | }, 44 | 45 | coverageReporter: { 46 | dir: path.join(process.cwd(), 'coverage'), 47 | reporters: [ 48 | { type: 'lcov', subdir: 'lcov' }, 49 | { type: 'html', subdir: 'html' }, 50 | { type: 'text-summary' }, 51 | ], 52 | }, 53 | 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /internals/templates/routes.js: -------------------------------------------------------------------------------- 1 | // These are the pages you can go to. 2 | // They are all wrapped in the App component, which should contain the navbar etc 3 | // See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information 4 | // about the code splitting business 5 | // import { getHooks } from 'utils/hooks'; 6 | 7 | const errorLoading = (err) => { 8 | console.error('Dynamic page loading failed', err); // eslint-disable-line no-console 9 | }; 10 | 11 | const loadModule = (cb) => (componentModule) => { 12 | cb(null, componentModule.default); 13 | }; 14 | 15 | export default function createRoutes() { 16 | // Create reusable async injectors using getHooks factory 17 | // const { injectReducer, injectSagas } = getHooks(store); 18 | 19 | return [ 20 | { 21 | path: '/', 22 | name: 'home', 23 | getComponent(nextState, cb) { 24 | const importModules = Promise.all([ 25 | System.import('components/HomePage'), 26 | ]); 27 | 28 | const renderRoute = loadModule(cb); 29 | 30 | importModules.then(([component]) => { 31 | renderRoute(component); 32 | }); 33 | 34 | importModules.catch(errorLoading); 35 | }, 36 | }, { 37 | path: '*', 38 | name: 'notfound', 39 | getComponent(nextState, cb) { 40 | System.import('components/NotFoundPage') 41 | .then(loadModule(cb)) 42 | .catch(errorLoading); 43 | }, 44 | }, 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /app/containers/modal_dialog.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Modal } from 'react-bootstrap' 3 | import { connect } from 'react-redux' 4 | 5 | import { cancelConfirmDialog } from '../actions/modal_actions' 6 | import AddTodoModal from './modals/add_todo_modal' 7 | import EditTodoModal from './modals/edit_todo_modal' 8 | import ConfirmDialogModal from './modals/confirm_dialog_modal' 9 | 10 | const MODAL_COMPONENTS = { 11 | ADD_TODO_MODAL: AddTodoModal, 12 | EDIT_TODO_MODAL: EditTodoModal, 13 | CONFIRM_DIALOG_MODAL: ConfirmDialogModal, 14 | } 15 | 16 | class ModalDialog extends Component { 17 | static get propTypes() { 18 | return { 19 | cancelConfirmDialog: PropTypes.func, 20 | modal: PropTypes.object, 21 | } 22 | } 23 | 24 | constructor() { 25 | super() 26 | this.handleHide = this.handleHide.bind(this) 27 | } 28 | 29 | handleHide() { 30 | this.props.cancelConfirmDialog() 31 | } 32 | 33 | render() { 34 | if (!this.props.modal.component) { 35 | return
36 | } 37 | 38 | const SpecificModal = MODAL_COMPONENTS[this.props.modal.component] 39 | return ( 40 | 41 | 42 | 43 | ) 44 | } 45 | } 46 | 47 | function mapStateToProps(state) { 48 | return { 49 | modal: state.modal, 50 | } 51 | } 52 | 53 | export default connect(mapStateToProps, { cancelConfirmDialog })(ModalDialog) 54 | -------------------------------------------------------------------------------- /app/.nginx.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Put this file in /etc/nginx/conf.d folder and make sure 3 | # you have line 'include /etc/nginx/conf.d/*.conf;' 4 | # in your main nginx configuration file 5 | ## 6 | 7 | ## 8 | # Redirect to the same URL with https:// 9 | ## 10 | 11 | server { 12 | 13 | listen 80; 14 | 15 | # Type your domain name below 16 | server_name example.com; 17 | 18 | return 301 https://$server_name$request_uri; 19 | 20 | } 21 | 22 | ## 23 | # HTTPS configurations 24 | ## 25 | 26 | server { 27 | 28 | listen 443; 29 | 30 | # Type your domain name below 31 | server_name example.com; 32 | 33 | ssl on; 34 | ssl_certificate /path/to/certificate.crt; 35 | ssl_certificate_key /path/to/server.key; 36 | 37 | # Use only TSL protocols for more secure 38 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 39 | 40 | # Always serve index.html for any request 41 | location / { 42 | # Set path 43 | root /var/www/; 44 | try_files $uri /index.html; 45 | } 46 | 47 | ## 48 | # If you want to use Node/Rails/etc. API server 49 | # on the same port (443) config Nginx as a reverse proxy. 50 | # For security reasons use a firewall like ufw in Ubuntu 51 | # and deny port 3000/tcp. 52 | ## 53 | 54 | # location /api/ { 55 | # 56 | # proxy_pass http://localhost:3000; 57 | # proxy_http_version 1.1; 58 | # proxy_set_header X-Forwarded-Proto https; 59 | # proxy_set_header Upgrade $http_upgrade; 60 | # proxy_set_header Connection 'upgrade'; 61 | # proxy_set_header Host $host; 62 | # proxy_cache_bypass $http_upgrade; 63 | # 64 | # } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /internals/templates/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with asynchronously loaded reducers 3 | */ 4 | 5 | import { createStore, applyMiddleware, compose } from 'redux'; 6 | import { fromJS } from 'immutable'; 7 | import { routerMiddleware } from 'react-router-redux'; 8 | import createSagaMiddleware from 'redux-saga'; 9 | import createReducer from './reducers'; 10 | 11 | const sagaMiddleware = createSagaMiddleware(); 12 | const devtools = window.devToolsExtension || (() => noop => noop); 13 | 14 | export default function configureStore(initialState = {}, history) { 15 | // Create the store with two middlewares 16 | // 1. sagaMiddleware: Makes redux-sagas work 17 | // 2. routerMiddleware: Syncs the location/URL path to the state 18 | const middlewares = [ 19 | sagaMiddleware, 20 | routerMiddleware(history), 21 | ]; 22 | 23 | const enhancers = [ 24 | applyMiddleware(...middlewares), 25 | devtools(), 26 | ]; 27 | 28 | const store = createStore( 29 | createReducer(), 30 | fromJS(initialState), 31 | compose(...enhancers) 32 | ); 33 | 34 | // Create hook for async sagas 35 | store.runSaga = sagaMiddleware.run; 36 | 37 | // Make reducers hot reloadable, see http://mxs.is/googmo 38 | /* istanbul ignore next */ 39 | if (module.hot) { 40 | System.import('./reducers').then((reducerModule) => { 41 | const createReducers = reducerModule.default; 42 | const nextReducers = createReducers(store.asyncReducers); 43 | 44 | store.replaceReducer(nextReducers); 45 | }); 46 | } 47 | 48 | // Initialize it with no other reducers 49 | store.asyncReducers = {}; 50 | return store; 51 | } 52 | -------------------------------------------------------------------------------- /app/logic/date_filter.js: -------------------------------------------------------------------------------- 1 | import { TODAY, TOMORROW, THIS_WEEK, ALL } from '../constants' 2 | import moment from 'moment' 3 | import _ from 'lodash' 4 | 5 | export default class DateFilter { 6 | constructor(todos) { 7 | this.todos = todos 8 | } 9 | 10 | filterBy(dueFilter) { 11 | switch (dueFilter) { 12 | case TODAY: 13 | return this.filterByDay(moment(), true) 14 | case TOMORROW: 15 | return this.filterByDay(moment().add(1, 'day'), false) 16 | case THIS_WEEK: 17 | return this.filterByWeek() 18 | case ALL: 19 | return this.todos 20 | default: 21 | return null 22 | } 23 | } 24 | 25 | filterByDay(day = moment(), includePastUncompleted = true) { 26 | return _.filter(this.todos, (todo) => { 27 | const todoDue = moment(todo.due).clone().startOf('day') 28 | const dateDue = day.clone().startOf('day') 29 | if (includePastUncompleted && todo.completed === false && todoDue.isBefore(dateDue)) { 30 | return true 31 | } else if (todoDue.isSame(dateDue)) { 32 | return true 33 | } 34 | return false 35 | }) 36 | } 37 | 38 | filterByWeek(due = moment()) { 39 | const sunday = due.clone().day('Sunday') 40 | const saturday = due.clone().day('Sunday').add(6, 'days') 41 | 42 | return _.filter(this.todos, (todo) => { 43 | if (todo.completed === false && moment(todo.due).isSameOrBefore(sunday)) { 44 | return true 45 | } else if (moment(todo.due).isSameOrAfter(sunday) && 46 | moment(todo.due).isSameOrBefore(saturday)) { 47 | return true 48 | } 49 | return false 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/backends/LocalBackend.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default class LocalBackend { 4 | constructor() { 5 | this.cachedTodos = null 6 | } 7 | 8 | fetchTodos() { 9 | return new Promise((resolve, reject) => { 10 | if (this.cachedTodos) { 11 | resolve(this.cachedTodos) 12 | } else { 13 | fetch('http://localhost:7890/todos') 14 | .then((resp) => resp.json()) 15 | .then((json) => { 16 | this.cachedTodos = json 17 | resolve(json) 18 | }) 19 | .catch((error) => { reject(error) }) 20 | } 21 | }) 22 | } 23 | 24 | add(todo) { 25 | const maxId = _.max(this.cachedTodos.map(t => t.id)) 26 | todo.id = maxId + 1 27 | return this.save([...this.cachedTodos, todo]) 28 | } 29 | 30 | update(todo) { 31 | const idx = _.findIndex(this.cachedTodos, (t) => t.id === todo.id) 32 | const todos = [...this.cachedTodos.slice(0, idx), todo, ...this.cachedTodos.slice(idx + 1)] 33 | return this.save(todos) 34 | } 35 | 36 | delete(todo) { 37 | const idx = _.findIndex(this.cachedTodos, (t) => t.id === todo.id) 38 | const todos = [...this.cachedTodos.slice(0, idx), ...this.cachedTodos.slice(idx + 1)] 39 | return this.save(todos) 40 | } 41 | 42 | save(todos) { 43 | return new Promise((resolve, reject) => { 44 | fetch('http://localhost:7890/todos', { 45 | method: 'POST', 46 | headers: { Accept: 'application/json' }, 47 | body: JSON.stringify(todos), 48 | }) 49 | .then((resp) => { 50 | this.cachedTodos = todos 51 | resolve(resp) 52 | }) 53 | .catch((error) => { 54 | reject(error) 55 | }) 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internals/generators/route/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Generator 3 | */ 4 | 5 | const fs = require('fs'); 6 | const componentExists = require('../utils/componentExists'); 7 | 8 | function reducerExists(comp) { 9 | try { 10 | fs.accessSync(`app/containers/${comp}/reducer.js`, fs.F_OK); 11 | return true; 12 | } catch (e) { 13 | return false; 14 | } 15 | } 16 | 17 | module.exports = { 18 | description: 'Add a route', 19 | prompts: [{ 20 | type: 'input', 21 | name: 'component', 22 | message: 'Which component should the route show?', 23 | validate: value => { 24 | if ((/.+/).test(value)) { 25 | return componentExists(value) ? true : `"${value}" doesn't exist.`; 26 | } 27 | 28 | return 'The path is required'; 29 | }, 30 | }, { 31 | type: 'input', 32 | name: 'path', 33 | message: 'Enter the path of the route.', 34 | default: '/about', 35 | validate: value => { 36 | if ((/.+/).test(value)) { 37 | return true; 38 | } 39 | 40 | return 'path is required'; 41 | }, 42 | }], 43 | 44 | // Add the route to the routes.js file above the error route 45 | // TODO smarter route adding 46 | actions: data => { 47 | const actions = []; 48 | if (reducerExists(data.component)) { 49 | actions.push({ 50 | type: 'modify', 51 | path: '../../app/routes.js', 52 | pattern: /(\s{\n\s{6}path: '\*',)/g, 53 | templateFile: './route/routeWithReducer.hbs', 54 | }); 55 | } else { 56 | actions.push({ 57 | type: 'modify', 58 | path: '../../app/routes.js', 59 | pattern: /(\s{\n\s{6}path: '\*',)/g, 60 | templateFile: './route/route.hbs', 61 | }); 62 | } 63 | 64 | return actions; 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # todolist_frontend 2 | 3 | [![Build Status](https://travis-ci.org/gammons/todolist_frontend.svg?branch=master)](https://travis-ci.org/gammons/todolist_frontend) 4 | 5 | The web frontend to [todolist](http://todolist.site). It is based off of [react-boilerplate](https://github.com/mxstbr/react-boilerplate), which provides a lot of nice things if you're doing react development. 6 | 7 | ## To run locally: 8 | 9 | 1. `npm install --save` 10 | 2. `npm start` 11 | 12 | If you don't want to use a live backend, switch to using a `LocalBackend` in `TodoStore`. At some point this will probably be controlled by url params, but for now you need to modify it yourself. 13 | 14 | # License 15 | 16 | MIT License 17 | 18 | Copyright (c) 2016 Grant Ammons 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in all 28 | copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. 37 | -------------------------------------------------------------------------------- /internals/generators/component/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add an unconnected component', 9 | prompts: [{ 10 | type: 'list', 11 | name: 'type', 12 | message: 'Select the type of component', 13 | default: 'Stateless Function', 14 | choices: () => ['ES6 Class', 'Stateless Function'], 15 | }, { 16 | type: 'input', 17 | name: 'name', 18 | message: 'What should it be called?', 19 | default: 'Button', 20 | validate: value => { 21 | if ((/.+/).test(value)) { 22 | return componentExists(value) ? 'A component or container with this name already exists' : true; 23 | } 24 | 25 | return 'The name is required'; 26 | }, 27 | }, { 28 | type: 'confirm', 29 | name: 'wantCSS', 30 | default: true, 31 | message: 'Does it have styling?', 32 | }], 33 | actions: data => { 34 | // Generate index.js and index.test.js 35 | const actions = [{ 36 | type: 'add', 37 | path: '../../app/components/{{properCase name}}/index.js', 38 | templateFile: data.type === 'ES6 Class' ? './component/es6.js.hbs' : './component/stateless.js.hbs', 39 | abortOnFail: true, 40 | }, { 41 | type: 'add', 42 | path: '../../app/components/{{properCase name}}/tests/index.test.js', 43 | templateFile: './component/test.js.hbs', 44 | abortOnFail: true, 45 | }]; 46 | 47 | // If they want a CSS file, add styles.css 48 | if (data.wantCSS) { 49 | actions.push({ 50 | type: 'add', 51 | path: '../../app/components/{{properCase name}}/styles.css', 52 | templateFile: './component/styles.css.hbs', 53 | abortOnFail: true, 54 | }); 55 | } 56 | 57 | return actions; 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /app/components/todo_tabs.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Tabs, Tab } from 'react-bootstrap' 3 | 4 | import { TODAY, TOMORROW, THIS_WEEK, ALL, SHOW_UNARCHIVED } from '../constants' 5 | 6 | export default class TodoTabs extends Component { 7 | static get contextTypes() { 8 | return { 9 | router: PropTypes.object, 10 | } 11 | } 12 | 13 | static get propTypes() { 14 | return { 15 | due: PropTypes.string, 16 | group: PropTypes.string, 17 | show: PropTypes.string, 18 | } 19 | } 20 | 21 | static get defaultProps() { 22 | return { 23 | show: SHOW_UNARCHIVED, 24 | due: ALL, 25 | group: ALL, 26 | } 27 | } 28 | 29 | constructor() { 30 | super() 31 | this.handleChange = this.handleChange.bind(this) 32 | } 33 | 34 | getSelectedIndex() { 35 | switch (this.props.due) { 36 | case TODAY: return 0 37 | case TOMORROW: return 1 38 | case THIS_WEEK: return 2 39 | case ALL: return 3 40 | default: return 3 41 | } 42 | } 43 | 44 | getDueFromIndex(idx) { 45 | switch (idx) { 46 | case 0: return TODAY 47 | case 1: return TOMORROW 48 | case 2: return THIS_WEEK 49 | case 3: return ALL 50 | default: return ALL 51 | } 52 | } 53 | 54 | handleChange(dueIdx) { 55 | const { show, group } = this.props 56 | const due = this.getDueFromIndex(dueIdx) 57 | this.context.router.push(`/${show}/${due}/${group}`) 58 | } 59 | 60 | render() { 61 | return ( 62 | 63 | 64 | 65 | 66 | 67 | 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const compression = require('compression'); 4 | const webpackDevMiddleware = require('webpack-dev-middleware'); 5 | const webpackHotMiddleware = require('webpack-hot-middleware'); 6 | const webpack = require('webpack'); 7 | 8 | // Dev middleware 9 | const addDevMiddlewares = (app, options) => { 10 | const compiler = webpack(options); 11 | const middleware = webpackDevMiddleware(compiler, { 12 | noInfo: true, 13 | publicPath: options.output.publicPath, 14 | silent: true, 15 | }); 16 | 17 | app.use(middleware); 18 | app.use(webpackHotMiddleware(compiler)); 19 | 20 | // Since webpackDevMiddleware uses memory-fs internally to store build 21 | // artifacts, we use it instead 22 | const fs = middleware.fileSystem; 23 | 24 | app.get('*', (req, res) => { 25 | const file = fs.readFileSync(path.join(compiler.outputPath, 'index.html')); 26 | res.send(file.toString()); 27 | }); 28 | }; 29 | 30 | // Production middlewares 31 | const addProdMiddlewares = (app, options) => { 32 | // compression middleware compresses your server responses which makes them 33 | // smaller (applies also to assets). You can read more about that technique 34 | // and other good practices on official Express.js docs http://mxs.is/googmy 35 | app.use(compression()); 36 | app.use(options.output.publicPath, express.static(options.output.path)); 37 | 38 | app.get('*', (req, res) => res.sendFile(path.join(options.output.path, 'index.html'))); 39 | }; 40 | 41 | /** 42 | * Front-end middleware 43 | */ 44 | module.exports = (options) => { 45 | const isProd = process.env.NODE_ENV === 'production'; 46 | 47 | const app = express(); 48 | 49 | if (isProd) { 50 | addProdMiddlewares(app, options); 51 | } else { 52 | addDevMiddlewares(app, options); 53 | } 54 | 55 | return app; 56 | }; 57 | -------------------------------------------------------------------------------- /app/test/test_helper.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export const fakeStore = (state) => { 4 | return { 5 | default: () => {}, 6 | subscribe: () => {}, 7 | dispatch: () => {}, 8 | getState: () => { 9 | return { ...state }; 10 | }, 11 | }; 12 | }; 13 | 14 | export let todos = [ 15 | { 16 | id: 1, 17 | subject: 'Call with @Bob and @Frank about +bigProject', 18 | projects: ['bigProject'], 19 | contexts: ['Bob','Frank'], 20 | due: moment().add(1, 'day').format('YYYY-MM-DD'), 21 | completed: false, 22 | archived: false 23 | }, 24 | { 25 | id: 2, 26 | subject: 'Strategy for +mobile @pomodoro', 27 | projects: ['mobile'], 28 | contexts: [], 29 | due: moment().format('YYYY-MM-DD'), 30 | completed: false, 31 | archived: false 32 | }, 33 | { 34 | id: 3, 35 | subject: 'Send phone udid to @Marty to test +mobile projects', 36 | projects: ['mobile'], 37 | contexts: ['marty'], 38 | due: moment().format('YYYY-MM-DD'), 39 | completed: false, 40 | archived: false 41 | }, 42 | { 43 | id: 4, 44 | subject: 'Did @john call me back about the +testProject?', 45 | projects: ['testProject'], 46 | contexts: ['john'], 47 | due: moment().format('YYYY-MM-DD'), 48 | due: '2016-04-28', 49 | completed: true, 50 | archived: false 51 | }, 52 | { 53 | id: 5, 54 | subject: 'Follow up with @nick about 6-month salary increase', 55 | projects: [], 56 | contexts: ['Nick'], 57 | due: moment().subtract(2, 'day').format('YYYY-MM-DD'), 58 | completed: true, 59 | archived: true 60 | }, 61 | { 62 | id: 6, 63 | subject: 'Work on +budget presentation for leadership team, sell to @Nick first', 64 | projects: [], 65 | contexts: ['Nick'], 66 | due: moment().subtract(2, 'day').format('YYYY-MM-DD'), 67 | completed: false, 68 | archived: false 69 | } 70 | ]; 71 | -------------------------------------------------------------------------------- /app/logic/tests/grouper.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import Grouper from '../grouper' 3 | 4 | describe('Grouper', () => { 5 | it('_getContexts', () => { 6 | const todos = [ 7 | { contexts: ['one', 'two'] }, 8 | { contexts: ['two', 'three'] }, 9 | { contexts: ['three', 'four'] }, 10 | ] 11 | const grouper = new Grouper(todos) 12 | expect(grouper.getContexts(todos)).toEqual(['one', 'two', 'three', 'four']) 13 | }) 14 | 15 | it('_todosWithContext', () => { 16 | const todos = [ 17 | { contexts: ['one', 'two'] }, 18 | { contexts: ['two', 'three'] }, 19 | { contexts: ['three', 'four'] }, 20 | ] 21 | const grouper = new Grouper(todos) 22 | expect(grouper.todosWithContext('two')).toEqual([{ contexts: ['one', 'two'] }, { contexts: ['two', 'three'] }]) 23 | }) 24 | 25 | it('byContext', () => { 26 | const todos = [ 27 | { contexts: ['one', 'two'] }, 28 | { contexts: ['two', 'three'] }, 29 | { contexts: ['three', 'four'] }, 30 | ] 31 | const grouper = new Grouper(todos) 32 | const expected = [ 33 | { title: 'one', todos: [{ contexts: ['one', 'two'] }] }, 34 | { title: 'two', todos: [{ contexts: ['one', 'two'] }, { contexts: ['two', 'three'] }] }, 35 | { title: 'three', todos: [{ contexts: ['two', 'three'] }, { contexts: ['three', 'four'] }] }, 36 | { title: 'four', todos: [{ contexts: ['three', 'four'] }] }, 37 | ] 38 | expect(grouper.byContext()).toEqual(expected) 39 | }) 40 | 41 | it('byContext includes todos with no contexts', () => { 42 | const todos = [ 43 | { contexts: ['one', 'two'] }, 44 | { contexts: [] }, 45 | ] 46 | const grouper = new Grouper(todos) 47 | const expected = [ 48 | { title: 'one', todos: [{ contexts: ['one', 'two'] }] }, 49 | { title: 'two', todos: [{ contexts: ['one', 'two'] }] }, 50 | { title: 'No contexts', todos: [{ contexts: [] }] }, 51 | ] 52 | expect(grouper.byContext()).toEqual(expected) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /app/constants.js: -------------------------------------------------------------------------------- 1 | // ******************* 2 | // Groupings 3 | // ******************* 4 | export const BY_CONTEXT = 'context' 5 | export const BY_PROJECT = 'project' 6 | 7 | // ******************* 8 | // Archived or unarchived 9 | // ******************* 10 | 11 | export const SHOW_UNARCHIVED = 'unarchived' 12 | export const SHOW_ARCHIVED = 'archived' 13 | 14 | // ******************* 15 | // Tabs 16 | // ******************* 17 | 18 | export const TODAY = 'today' 19 | export const TOMORROW = 'tomorrow' 20 | export const THIS_WEEK = 'this_week' 21 | export const ALL = 'all' 22 | 23 | // ******************* 24 | // Actions 25 | // ******************* 26 | 27 | export const ALERT_CANCEL = 'ALERT_CANCEL' 28 | export const ALERT_OK = 'ALERT_OK' 29 | 30 | export const ADD_TODO = 'ADD_TODO' 31 | export const CREATE_TODO = 'CREATE_TODO' 32 | export const UPDATE_TODO = 'UPDATE_TODO' 33 | export const DELETE_TODO = 'DELETE_TODO' 34 | export const FETCH_TODOS = 'FETCH_TODOS' 35 | 36 | // ******************* 37 | // saga stuff 38 | // ******************* 39 | export const START_ARCHIVE_TODO_SAGA = 'START_ARCHIVE_TODO_SAGA' 40 | export const START_UNARCHIVE_TODO_SAGA = 'START_UNARCHIVE_TODO_SAGA' 41 | export const START_EDIT_TODO_SAGA = 'START_EDIT_TODO_SAGA' 42 | export const START_UPDATE_TODO_SAGA = 'START_UPDATE_TODO_SAGA' 43 | export const START_CREATE_TODO_SAGA = 'START_CREATE_TODO_SAGA' 44 | export const START_DELETE_TODO_SAGA = 'START_DELETE_TODO_SAGA' 45 | export const TODOS_FETCHED = 'TODOS_FETCHED' 46 | 47 | // Used to open a modal dialog 48 | export const MODAL = 'MODAL' 49 | 50 | // Modal types 51 | export const ADD_TODO_MODAL = 'ADD_TODO_MODAL' 52 | export const EDIT_TODO_MODAL = 'EDIT_TODO_MODAL' 53 | export const CONFIRM_DIALOG_MODAL = 'CONFIRM_DIALOG_MODAL' 54 | 55 | // confirm dialog actions 56 | export const CONFIRM_DIALOG_OK = 'CONFIRM_DIALOG' 57 | export const CONFIRM_DIALOG_CANCEL = 'CONFIRM_DIALOG_CANCEL' 58 | 59 | // alert actions 60 | export const OPEN_ALERT = 'OPEN_ALERT' 61 | export const DISMISS_ALERT = 'DISMISS_ALERT' 62 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | .gitconfig text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | 73 | # Heroku 74 | Procfile text 75 | .slugignore text 76 | 77 | # Documentation 78 | *.md text 79 | LICENSE text 80 | AUTHORS text 81 | 82 | 83 | # 84 | ## These files are binary and should be left untouched 85 | # 86 | 87 | # (binary is a macro for -text -diff) 88 | *.png binary 89 | *.jpg binary 90 | *.jpeg binary 91 | *.gif binary 92 | *.ico binary 93 | *.mov binary 94 | *.mp4 binary 95 | *.mp3 binary 96 | *.flv binary 97 | *.fla binary 98 | *.swf binary 99 | *.gz binary 100 | *.zip binary 101 | *.7z binary 102 | *.ttf binary 103 | *.eot binary 104 | *.woff binary 105 | *.pyc binary 106 | *.pdf binary 107 | -------------------------------------------------------------------------------- /internals/webpack/webpack.dev.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DEVELOPMENT WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | 9 | // PostCSS plugins 10 | const cssnext = require('postcss-cssnext'); 11 | const postcssFocus = require('postcss-focus'); 12 | const postcssReporter = require('postcss-reporter'); 13 | 14 | module.exports = require('./webpack.base.babel')({ 15 | // Add hot reloading in development 16 | entry: [ 17 | 'eventsource-polyfill', // Necessary for hot reloading with IE 18 | 'webpack-hot-middleware/client', 19 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js 20 | ], 21 | 22 | // Don't use hashes in dev mode for better performance 23 | output: { 24 | filename: '[name].js', 25 | chunkFilename: '[name].chunk.js', 26 | }, 27 | 28 | // Load the CSS in a style tag in development 29 | cssLoaders: 'style-loader!css-loader?localIdentName=[local]__[path][name]__[hash:base64:5]&modules&importLoaders=1&sourceMap!postcss-loader', 30 | 31 | // Process the CSS with PostCSS 32 | postcssPlugins: [ 33 | postcssFocus(), // Add a :focus to every :hover 34 | cssnext({ // Allow future CSS features to be used, also auto-prefixes the CSS... 35 | browsers: ['last 2 versions', 'IE > 10'], // ...based on this browser list 36 | }), 37 | postcssReporter({ // Posts messages from plugins to the terminal 38 | clearMessages: true, 39 | }), 40 | ], 41 | 42 | // Add hot reloading 43 | plugins: [ 44 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading 45 | new webpack.NoErrorsPlugin(), 46 | new HtmlWebpackPlugin({ 47 | template: 'app/index.html', 48 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js 49 | }), 50 | ], 51 | 52 | // Tell babel that we want to hot-reload 53 | babelQuery: { 54 | presets: ['react-hmre'], 55 | }, 56 | 57 | // Emit a source map for easier debugging 58 | devtool: 'cheap-module-eval-source-map', 59 | }); 60 | -------------------------------------------------------------------------------- /internals/scripts/setup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable */ 4 | 5 | var exec = require('child_process').exec; 6 | var path = require('path'); 7 | var fs = require('fs'); 8 | var animateProgress = require('./helpers/progress'); 9 | var addCheckMark = require('./helpers/checkmark'); 10 | var readline = require('readline'); 11 | 12 | process.stdin.resume(); 13 | process.stdin.setEncoding('utf8'); 14 | 15 | var dir = process.cwd(); 16 | 17 | process.stdout.write('\n'); 18 | var interval = animateProgress('Cleaning old repository'); 19 | process.stdout.write('Cleaning old repository'); 20 | cleanRepo(dir, function () { 21 | clearInterval(interval); 22 | process.stdout.write('\nInstalling dependencies... (This might take a while)'); 23 | setTimeout(function () { 24 | readline.cursorTo(process.stdout, 0); 25 | interval = animateProgress('Installing dependencies'); 26 | }, 500); 27 | 28 | process.stdout.write('Installing dependencies'); 29 | installDeps(function (error) { 30 | clearInterval(interval); 31 | if (error) { 32 | process.stdout.write(error); 33 | } 34 | 35 | deleteFileInCurrentDir('setup.js', function () { 36 | process.stdout.write('\n'); 37 | interval = animateProgress('Initialising new repository'); 38 | process.stdout.write('Initialising new repository'); 39 | initGit(dir, function () { 40 | clearInterval(interval); 41 | process.stdout.write('\nDone!'); 42 | process.exit(0); 43 | }); 44 | }); 45 | }); 46 | }); 47 | 48 | /** 49 | * Deletes the .git folder in dir 50 | */ 51 | function cleanRepo(dir, callback) { 52 | exec('rm -Rf .git/', addCheckMark.bind(null, callback)); 53 | } 54 | 55 | /** 56 | * Initializes git again 57 | */ 58 | function initGit(dir, callback) { 59 | exec('git init && git add . && git commit -m "Initial commit"', addCheckMark.bind(null, callback)); 60 | } 61 | 62 | /** 63 | * Deletes a file in the current directory 64 | */ 65 | function deleteFileInCurrentDir(file, callback) { 66 | fs.unlink(path.join(__dirname, file), callback); 67 | } 68 | 69 | /** 70 | * Installs dependencies 71 | */ 72 | function installDeps(callback) { 73 | exec('npm install', addCheckMark.bind(null, callback)); 74 | } 75 | -------------------------------------------------------------------------------- /internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('shelljs/global'); 3 | 4 | /** 5 | * Adds mark check symbol 6 | */ 7 | function addCheckMark(callback) { 8 | process.stdout.write(' ✓'); 9 | callback(); 10 | } 11 | 12 | if (!which('git')) { 13 | echo('Sorry, this script requires git'); 14 | exit(1); 15 | } 16 | 17 | if (!test('-e', 'internals/templates')) { 18 | echo('The example is deleted already.'); 19 | exit(1); 20 | } 21 | 22 | process.stdout.write('Cleanup started...'); 23 | 24 | // Cleanup components folder 25 | rm('-rf', 'app/components/*'); 26 | 27 | // Cleanup containers folder 28 | rm('-rf', 'app/containers/*'); 29 | mkdir('app/containers/App'); 30 | mkdir('app/components/NotFoundPage'); 31 | mkdir('app/components/HomePage'); 32 | cp('internals/templates/appContainer.js', 'app/containers/App/index.js'); 33 | cp('internals/templates/notFoundPage.js', 'app/components/NotFoundPage/index.js'); 34 | cp('internals/templates/homePage.js', 'app/components/HomePage/index.js'); 35 | 36 | // Copy selectors 37 | mkdir('app/containers/App/tests'); 38 | cp('internals/templates/selectors.js', 39 | 'app/containers/App/selectors.js'); 40 | cp('internals/templates/selectors.test.js', 41 | 'app/containers/App/tests/selectors.test.js'); 42 | 43 | // Utils 44 | rm('-rf', 'app/utils'); 45 | mkdir('app/utils'); 46 | mkdir('app/utils/tests'); 47 | cp('internals/templates/hooks.js', 48 | 'app/utils/hooks.js'); 49 | cp('internals/templates/hooks.test.js', 50 | 'app/utils/tests/hooks.test.js'); 51 | 52 | // Replace the files in the root app/ folder 53 | cp('internals/templates/app.js', 'app/app.js'); 54 | cp('internals/templates/index.html', 'app/index.html'); 55 | cp('internals/templates/reducers.js', 'app/reducers.js'); 56 | cp('internals/templates/routes.js', 'app/routes.js'); 57 | cp('internals/templates/store.js', 'app/store.js'); 58 | cp('internals/templates/store.test.js', 'app/store.test.js'); 59 | 60 | // Remove the templates folder 61 | rm('-rf', 'internals/templates'); 62 | 63 | process.stdout.write(' ✓'); 64 | 65 | // Commit the changes 66 | if (exec('git add . --all && git commit -qm "Remove default example"').code !== 0) { 67 | echo('\nError: Git commit failed'); 68 | exit(1); 69 | } 70 | 71 | echo('\nCleanup done. Happy Coding!!!'); 72 | -------------------------------------------------------------------------------- /app/containers/modals/add_todo_modal.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Modal, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap' 3 | import { connect } from 'react-redux' 4 | import DatePicker from 'react-bootstrap-date-picker' 5 | 6 | import { createTodo } from '../../actions/todo_actions' 7 | import { cancelConfirmDialog } from '../../actions/modal_actions' 8 | 9 | class AddTodoModal extends Component { 10 | static get propTypes() { 11 | return { 12 | cancelConfirmDialog: PropTypes.func, 13 | createTodo: PropTypes.func, 14 | } 15 | } 16 | 17 | constructor() { 18 | super() 19 | this.handleSubmit = this.handleSubmit.bind(this) 20 | this.handleHide = this.handleHide.bind(this) 21 | this.subjectChange = this.subjectChange.bind(this) 22 | this.dueChange = this.dueChange.bind(this) 23 | } 24 | 25 | handleHide() { 26 | this.props.cancelConfirmDialog() 27 | } 28 | 29 | subjectChange(e) { 30 | this.subject = e.target.value 31 | } 32 | dueChange(e) { 33 | this.due = e 34 | } 35 | handleSubmit() { 36 | this.props.createTodo(this.subject, this.due) 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | 43 | Add todo 44 | 45 | 46 |
47 | 48 | Subject 49 | 54 | 55 | 56 | Due 57 | 58 | 59 |
60 |
61 | 62 | 63 | 64 | 65 |
66 | ) 67 | } 68 | } 69 | 70 | export default connect(null, { cancelConfirmDialog, createTodo })(AddTodoModal) 71 | -------------------------------------------------------------------------------- /app/containers/todolist.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { fetchTodos } from '../actions/todo_actions' 3 | import { connect } from 'react-redux' 4 | import Todo from './todo' 5 | import { ListGroup } from 'react-bootstrap' 6 | 7 | import filteredTodosSelector from '../selectors/filtered_todos' 8 | 9 | import FilterButtons from '../components/filter_buttons' 10 | 11 | class Todolist extends Component { 12 | 13 | static get propTypes() { 14 | return { 15 | params: PropTypes.object, 16 | fetchTodos: PropTypes.func, 17 | todos: PropTypes.array, 18 | } 19 | } 20 | 21 | componentWillMount() { 22 | const { show, due, group } = this.props.params 23 | this.props.fetchTodos(show, due, group) 24 | } 25 | 26 | componentWillReceiveProps(nextProps) { 27 | if (nextProps.params.due !== this.props.params.due 28 | || nextProps.params.show !== this.props.params.show 29 | || nextProps.params.group !== this.props.params.group) { 30 | this.props.fetchTodos(nextProps.params.show, nextProps.params.due, nextProps.params.group) 31 | } 32 | } 33 | 34 | showGroup(group, idx) { 35 | return ( 36 |
37 |

{group.title}

38 | 39 | {this.renderTodosForGroup(group.todos)} 40 | 41 |
42 | ) 43 | } 44 | 45 | renderTodosForGroup(todos) { 46 | if (todos.length === 0) { 47 | return

No todos.

48 | } 49 | return todos.map(this.renderTodo.bind(this)) 50 | } 51 | 52 | renderTodo(todo, idx) { 53 | return ( 54 | 55 | ) 56 | } 57 | 58 | render() { 59 | let todos = [] 60 | if (this.props.todos) { todos = this.props.todos } 61 | const { show, due, group } = this.props.params 62 | 63 | return ( 64 |
65 | 66 | 67 | 68 | 69 | {todos.map(this.showGroup.bind(this))} 70 |
71 | ) 72 | } 73 | } 74 | 75 | function mapStateToProps(state, ownProps) { 76 | return { 77 | todos: filteredTodosSelector(state, ownProps), 78 | } 79 | } 80 | 81 | export default connect(mapStateToProps, { fetchTodos })(Todolist) 82 | -------------------------------------------------------------------------------- /app/actions/todo_actions.js: -------------------------------------------------------------------------------- 1 | import * as constants from '../constants' 2 | import moment from 'moment' 3 | 4 | import TodoLogic from '../logic/todo_logic' 5 | 6 | export function fetchTodos() { 7 | return { type: constants.FETCH_TODOS } 8 | } 9 | 10 | export function startCreateTodo() { 11 | return { type: constants.START_CREATE_TODO_SAGA } 12 | } 13 | 14 | export function startUpdateTodo(todo) { 15 | const logic = new TodoLogic() 16 | return { type: constants.START_UPDATE_TODO_SAGA, todo: logic.updateTodo(todo) } 17 | } 18 | 19 | export function dueToday(todo) { 20 | let modifiedTodo = todo 21 | const logic = new TodoLogic() 22 | modifiedTodo.due = moment() 23 | modifiedTodo = logic.updateTodo(modifiedTodo) 24 | return { type: constants.START_UPDATE_TODO_SAGA, todo: modifiedTodo } 25 | } 26 | 27 | export function dueTomorrow(todo) { 28 | let modifiedTodo = todo 29 | const logic = new TodoLogic() 30 | modifiedTodo.due = moment().add(1, 'day') 31 | modifiedTodo = logic.updateTodo(modifiedTodo) 32 | return { type: constants.START_UPDATE_TODO_SAGA, todo: modifiedTodo } 33 | } 34 | 35 | export function startDeleteTodo(todo) { 36 | return { type: constants.START_DELETE_TODO_SAGA, todo } 37 | } 38 | 39 | export function startToggleComplete(todo) { 40 | const modifiedTodo = todo 41 | modifiedTodo.completed = !todo.completed 42 | return { type: constants.START_UPDATE_TODO_SAGA, todo } 43 | } 44 | 45 | export function startArchiveTodo(todo) { 46 | const modifiedTodo = todo 47 | modifiedTodo.archived = !todo.archived 48 | return { type: constants.START_ARCHIVE_TODO_SAGA, todo } 49 | } 50 | 51 | export function startUnarchiveTodo(todo) { 52 | const modifiedTodo = todo 53 | modifiedTodo.archived = !todo.archived 54 | return { type: constants.START_UNARCHIVE_TODO_SAGA, todo: modifiedTodo } 55 | } 56 | 57 | export function createTodo(subject, due) { 58 | const logic = new TodoLogic() 59 | const todo = logic.addTodo(subject, due) 60 | return { type: constants.CREATE_TODO, todo } 61 | } 62 | 63 | export function addTodo(todo) { 64 | return { type: constants.ADD_TODO, todo } 65 | } 66 | 67 | export function updateTodo(todo) { 68 | return { type: constants.UPDATE_TODO, todo } 69 | } 70 | 71 | export function deleteTodo(todo) { 72 | return { type: constants.DELETE_TODO, todo } 73 | } 74 | -------------------------------------------------------------------------------- /app/components/filter_buttons.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { ButtonGroup, DropdownButton, MenuItem } from 'react-bootstrap' 3 | 4 | import { ALL, SHOW_UNARCHIVED, SHOW_ARCHIVED, BY_CONTEXT, BY_PROJECT } from '../constants' 5 | 6 | export default class FilterButtons extends Component { 7 | static get contextTypes() { 8 | return { 9 | router: PropTypes.object, 10 | } 11 | } 12 | 13 | static get propTypes() { 14 | return { 15 | show: PropTypes.string, 16 | group: PropTypes.string, 17 | due: PropTypes.string, 18 | } 19 | } 20 | 21 | constructor() { 22 | super() 23 | this.handleChangeShow = this.handleChangeShow.bind(this) 24 | this.handleChangeGroup = this.handleChangeGroup.bind(this) 25 | } 26 | 27 | getShowTitle() { 28 | return (this.props.show === SHOW_UNARCHIVED) ? 'Unarchived' : 'Archived' 29 | } 30 | 31 | getGroupTitle() { 32 | switch (this.props.group) { 33 | case ALL: return 'No grouping' 34 | case BY_CONTEXT: return 'By context' 35 | case BY_PROJECT: return 'By Project' 36 | default: return null 37 | } 38 | } 39 | 40 | handleChangeShow(show) { 41 | const { group, due } = this.props 42 | this.context.router.push(`/${show}/${due}/${group}`) 43 | } 44 | 45 | handleChangeGroup(group) { 46 | const { show, due } = this.props 47 | this.context.router.push(`/${show}/${due}/${group}`) 48 | } 49 | 50 | render() { 51 | return ( 52 | 53 | 59 | Unarchived 60 | Archived 61 | 62 | 68 | No grouping 69 | By Context 70 | By Project 71 | 72 | 73 | ) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/containers/tests/todo.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { expect } from 'chai' 3 | import { render } from 'enzyme' 4 | import { Provider } from 'react-redux' 5 | import { fakeStore } from '../../test/test_helper' 6 | import moment from 'moment' 7 | 8 | import Todo from '../todo' 9 | 10 | function setup(todo) { 11 | return render() 12 | } 13 | 14 | describe('', () => { 15 | describe('Completed checkbox', () => { 16 | it('is checked when completed is true', () => { 17 | const todoComponent = setup({ subject: 'is complete', completed: true }) 18 | expect(todoComponent.find('input')).to.be.checked() 19 | }) 20 | 21 | it('is unchecked when completed is false', () => { 22 | const todoComponent = setup({ subject: 'is not complete', completed: false }) 23 | expect(todoComponent.find('input')).to.not.be.checked() 24 | }) 25 | }) 26 | 27 | describe('Archive todo menu item', () => { 28 | it('Shows "Archive" when the todo is not archived', () => { 29 | const todoComponent = setup({ subject: 'unarchived', archived: false }) 30 | expect(todoComponent.find('ul[class="dropdown-menu"]')).to.have.text().match(/Archive/) 31 | }) 32 | 33 | it('Shows "Un-archive" when the todo is archived', () => { 34 | const todoComponent = setup({ subject: 'archived', archived: true }) 35 | expect(todoComponent.find('ul[class="dropdown-menu"]')).to.have.text().match(/Un-archive/) 36 | }) 37 | }) 38 | 39 | describe('Showing due dates', () => { 40 | it('shows "Today" if the due date is today', () => { 41 | const todoComponent = setup({ subject: 'archived', archived: true, due: moment().format('YYYY-MM-DD') }) 42 | expect(todoComponent.find('small')).to.have.text('Today') 43 | }) 44 | 45 | it('shows "Tomorrow" if the due date is tomorrow', () => { 46 | const todoComponent = setup({ subject: 'archived', archived: true, due: moment().add(1, 'day').format('YYYY-MM-DD') }) 47 | expect(todoComponent.find('small')).to.have.text('Tomorrow') 48 | }) 49 | 50 | it('shows a regular date if it\'s not today or tomorrow', () => { 51 | const todoComponent = setup({ subject: 'archived', archived: true, due: '2016-08-23' }) 52 | expect(todoComponent.find('small')).to.have.text('Tue Aug 23') 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /app/containers/todo.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import moment from 'moment' 3 | import { ListGroupItem, Row, Grid, Col } from 'react-bootstrap' 4 | import styles from '../styles.css' 5 | import { startToggleComplete } from '../actions/todo_actions' 6 | import { connect } from 'react-redux' 7 | import TodoActions from './todo_actions' 8 | 9 | class Todo extends Component { 10 | static get propTypes() { 11 | return { 12 | todo: PropTypes.object, 13 | startToggleComplete: PropTypes.func, 14 | } 15 | } 16 | 17 | constructor() { 18 | super() 19 | this.handleCheck = this.handleCheck.bind(this) 20 | } 21 | 22 | handleCheck() { 23 | this.props.startToggleComplete(this.props.todo) 24 | } 25 | 26 | formatDue(inDue) { 27 | if (inDue === '' || inDue === null) return '' 28 | const due = moment(inDue).startOf('day') 29 | const today = moment().startOf('day') 30 | const tomorrow = moment().add(1, 'day').startOf('day') 31 | 32 | if (due.isBefore(today)) { 33 | return (
{due.format('ddd MMM D')}
) 34 | } 35 | if (today.isSame(due)) { 36 | return (
Today
) 37 | } 38 | 39 | if (due.isSame(tomorrow)) { 40 | return (
Tomorrow
) 41 | } 42 | return due.format('ddd MMM D') 43 | } 44 | 45 | renderSubject() { 46 | if (this.props.todo.completed) { 47 | return {this.props.todo.subject} 48 | } 49 | return

{this.props.todo.subject}

50 | } 51 | 52 | render() { 53 | const { todo } = this.props 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {this.renderSubject()} 64 | {this.formatDue(todo.due)} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ) 73 | } 74 | } 75 | 76 | export default connect(null, { startToggleComplete })(Todo) 77 | -------------------------------------------------------------------------------- /internals/templates/hooks.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test hooks 3 | */ 4 | 5 | import expect from 'expect'; 6 | import configureStore from 'store.js'; 7 | import { memoryHistory } from 'react-router'; 8 | import { put } from 'redux-saga/effects'; 9 | import { fromJS } from 'immutable'; 10 | 11 | import { 12 | injectAsyncReducer, 13 | injectAsyncSagas, 14 | getHooks, 15 | } from 'utils/hooks'; 16 | 17 | // Fixtures 18 | 19 | const initialState = fromJS({ reduced: 'soon' }); 20 | 21 | const reducer = (state = initialState, action) => { 22 | switch (action.type) { 23 | case 'TEST': 24 | return state.set('reduced', action.payload); 25 | default: 26 | return state; 27 | } 28 | }; 29 | 30 | const sagas = [ 31 | function* testSaga() { 32 | yield put({ type: 'TEST', payload: 'yup' }); 33 | }, 34 | ]; 35 | 36 | describe('hooks', () => { 37 | let store; 38 | 39 | describe('getHooks', () => { 40 | before(() => { 41 | store = configureStore({}, memoryHistory); 42 | }); 43 | 44 | it('given a store, should return all hooks', () => { 45 | const { injectReducer, injectSagas } = getHooks(store); 46 | 47 | injectReducer('test', reducer); 48 | injectSagas(sagas); 49 | 50 | const actual = store.getState().get('test'); 51 | const expected = initialState.merge({ reduced: 'yup' }); 52 | 53 | expect(actual.toJS()).toEqual(expected.toJS()); 54 | }); 55 | }); 56 | 57 | describe('helpers', () => { 58 | before(() => { 59 | store = configureStore({}, memoryHistory); 60 | }); 61 | 62 | describe('injectAsyncReducer', () => { 63 | it('given a store, it should provide a function to inject a reducer', () => { 64 | const injectReducer = injectAsyncReducer(store); 65 | 66 | injectReducer('test', reducer); 67 | 68 | const actual = store.getState().get('test'); 69 | const expected = initialState; 70 | 71 | expect(actual.toJS()).toEqual(expected.toJS()); 72 | }); 73 | }); 74 | 75 | describe('injectAsyncSagas', () => { 76 | it('given a store, it should provide a function to inject a saga', () => { 77 | const injectSagas = injectAsyncSagas(store); 78 | 79 | injectSagas(sagas); 80 | 81 | const actual = store.getState().get('test'); 82 | const expected = initialState.merge({ reduced: 'yup' }); 83 | 84 | expect(actual.toJS()).toEqual(expected.toJS()); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /app/containers/todo_actions.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | import { DropdownButton, MenuItem } from 'react-bootstrap' 4 | import { EDIT_TODO_MODAL } from '../constants' 5 | 6 | import { dueToday, dueTomorrow, startArchiveTodo, startUnarchiveTodo, startDeleteTodo } from '../actions/todo_actions' 7 | import { openModal } from '../actions/modal_actions' 8 | 9 | class TodoActions extends Component { 10 | static get propTypes() { 11 | return { 12 | dueToday: PropTypes.func, 13 | dueTomorrow: PropTypes.func, 14 | openModal: PropTypes.func, 15 | startArchiveTodo: PropTypes.func, 16 | startUnarchiveTodo: PropTypes.func, 17 | startDeleteTodo: PropTypes.func, 18 | id: PropTypes.string, 19 | todo: PropTypes.object, 20 | } 21 | } 22 | 23 | constructor() { 24 | super() 25 | this.handleChange = this.handleChange.bind(this) 26 | } 27 | 28 | handleChange(action) { 29 | switch (action) { 30 | case 'today': 31 | this.props.dueToday(this.props.todo) 32 | break 33 | case 'tomorrow': 34 | this.props.dueTomorrow(this.props.todo) 35 | break 36 | case 'edit': 37 | this.props.openModal(EDIT_TODO_MODAL, { todo: this.props.todo }) 38 | break 39 | case 'archive': 40 | this.props.startArchiveTodo(this.props.todo) 41 | break 42 | case 'unarchive': 43 | this.props.startUnarchiveTodo(this.props.todo) 44 | break 45 | case 'delete': 46 | this.props.startDeleteTodo(this.props.todo) 47 | break 48 | default: 49 | } 50 | } 51 | 52 | showArchiveOption() { 53 | if (this.props.todo.archived) { 54 | return Un-archive 55 | } 56 | return Archive 57 | } 58 | 59 | render() { 60 | return ( 61 | 62 | 68 | Due today 69 | Due tomorrow 70 | Edit 71 | 72 | {this.showArchiveOption()} 73 | Delete 74 | 75 | 76 | ) 77 | } 78 | } 79 | 80 | export default connect(null, { dueToday, dueTomorrow, startArchiveTodo, startUnarchiveTodo, startDeleteTodo, openModal })(TodoActions) 81 | -------------------------------------------------------------------------------- /app/containers/modals/edit_todo_modal.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react' 2 | import { Modal, FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap' 3 | import { connect } from 'react-redux' 4 | import DatePicker from 'react-bootstrap-date-picker' 5 | import moment from 'moment' 6 | 7 | import { startUpdateTodo } from '../../actions/todo_actions' 8 | import { cancelConfirmDialog } from '../../actions/modal_actions' 9 | 10 | class EditTodoModal extends Component { 11 | static get propTypes() { 12 | return { 13 | modal: PropTypes.object, 14 | startUpdateTodo: PropTypes.func, 15 | cancelConfirmDialog: PropTypes.func, 16 | } 17 | } 18 | 19 | constructor() { 20 | super() 21 | this.handleSubmit = this.handleSubmit.bind(this) 22 | this.onSubjectChange = this.onSubjectChange.bind(this) 23 | this.onDueChange = this.onDueChange.bind(this) 24 | this.handleHide = this.handleHide.bind(this) 25 | } 26 | 27 | onSubjectChange(e) { 28 | this.props.modal.todo.subject = e.target.value 29 | } 30 | 31 | onDueChange(e) { 32 | this.props.modal.todo.due = e 33 | } 34 | 35 | handleSubmit() { 36 | this.props.startUpdateTodo(this.props.modal.todo) 37 | this.props.cancelConfirmDialog() 38 | } 39 | 40 | handleHide() { 41 | this.props.cancelConfirmDialog() 42 | } 43 | 44 | isoDue(due) { 45 | if (due) { 46 | return moment(due).format() 47 | } 48 | return '' 49 | } 50 | 51 | render() { 52 | const todo = this.props.modal.todo 53 | return ( 54 |
55 | 56 | Edit todo 57 | 58 | 59 |
60 | 61 | Subject 62 | 68 | 69 | 70 | Due 71 | 72 | 73 |
74 |
75 | 76 | 77 | 78 | 79 |
80 | ) 81 | } 82 | 83 | } 84 | 85 | export default connect(null, { cancelConfirmDialog, startUpdateTodo })(EditTodoModal) 86 | -------------------------------------------------------------------------------- /internals/webpack/webpack.test.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const modules = [ 8 | 'app', 9 | 'node_modules', 10 | ]; 11 | 12 | module.exports = { 13 | devtool: 'inline-source-map', 14 | isparta: { 15 | babel: { 16 | presets: ['es2015', 'react', 'stage-0'], 17 | }, 18 | }, 19 | module: { 20 | // Some libraries don't like being run through babel. 21 | // If they gripe, put them here. 22 | noParse: [ 23 | /node_modules(\\|\/)sinon/, 24 | /node_modules(\\|\/)acorn/, 25 | ], 26 | preLoaders: [ 27 | { test: /\.js$/, 28 | loader: 'isparta', 29 | include: path.resolve('app/'), 30 | }, 31 | ], 32 | loaders: [ 33 | { test: /\.json$/, loader: 'json-loader' }, 34 | { test: /\.css$/, loader: 'null-loader' }, 35 | 36 | // sinon.js--aliased for enzyme--expects/requires global vars. 37 | // imports-loader allows for global vars to be injected into the module. 38 | // See https://github.com/webpack/webpack/issues/304 39 | { test: /sinon(\\|\/)pkg(\\|\/)sinon\.js/, 40 | loader: 'imports?define=>false,require=>false', 41 | }, 42 | { test: /\.js$/, 43 | loader: 'babel', 44 | exclude: [/node_modules/], 45 | }, 46 | { test: /\.jpe?g$|\.gif$|\.png$/i, 47 | loader: 'null-loader', 48 | }, 49 | ], 50 | }, 51 | 52 | plugins: [ 53 | 54 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 55 | // inside your code for any environment checks; UglifyJS will automatically 56 | // drop any unreachable code. 57 | new webpack.DefinePlugin({ 58 | 'process.env': { 59 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 60 | }, 61 | })], 62 | 63 | // Some node_modules pull in Node-specific dependencies. 64 | // Since we're running in a browser we have to stub them out. See: 65 | // https://webpack.github.io/docs/configuration.html#node 66 | // https://github.com/webpack/node-libs-browser/tree/master/mock 67 | // https://github.com/webpack/jade-loader/issues/8#issuecomment-55568520 68 | node: { 69 | fs: 'empty', 70 | child_process: 'empty', 71 | net: 'empty', 72 | tls: 'empty', 73 | }, 74 | 75 | // required for enzyme to work properly 76 | externals: { 77 | jsdom: 'window', 78 | 'react/addons': true, 79 | 'react/lib/ExecutionEnvironment': true, 80 | 'react/lib/ReactContext': 'window', 81 | }, 82 | resolve: { 83 | modulesDirectories: modules, 84 | modules, 85 | alias: { 86 | // required for enzyme to work properly 87 | sinon: 'sinon/pkg/sinon', 88 | }, 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /app/logic/grouper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { ALL, BY_CONTEXT, BY_PROJECT } from '../constants' 3 | 4 | export default class Grouper { 5 | constructor(todos) { 6 | this.todos = todos 7 | } 8 | 9 | grouped(grouping) { 10 | switch (grouping) { 11 | case ALL: return this.byAll() 12 | case BY_CONTEXT: return this.byContext() 13 | case BY_PROJECT: return this.byProject() 14 | default: return this.byAll() 15 | } 16 | } 17 | 18 | byContext() { 19 | const grouped = [] 20 | _.map(this.getContexts(), (context) => { 21 | grouped.push({ title: context, todos: this.todosWithContext(context) }) 22 | }) 23 | const contextLengths = _.map(this.todos, (todo) => todo.contexts.length) 24 | if (_.some(contextLengths, (l) => l === 0)) { 25 | grouped.push({ title: 'No contexts', todos: this.todosWithNoContext() }) 26 | } 27 | return grouped 28 | } 29 | 30 | byProject() { 31 | const grouped = [] 32 | _.map(this.getProjects(), (project) => { 33 | grouped.push({ title: project, todos: this.todosWithProject(project) }) 34 | }) 35 | 36 | const projectLengths = _.map(this.todos, (todo) => todo.projects.length) 37 | if (_.some(projectLengths, (l) => l === 0)) { 38 | grouped.push({ title: 'No projects', todos: this.todosWithNoProject() }) 39 | } 40 | return grouped 41 | } 42 | 43 | byAll() { 44 | return [{ title: 'All todos', todos: this.todos }] 45 | } 46 | 47 | todosWithContext(context) { 48 | const ret = [] 49 | _.each(this.todos, (todo) => { 50 | if (_.includes(todo.contexts, context)) { 51 | ret.push(todo) 52 | } 53 | }) 54 | return ret 55 | } 56 | 57 | todosWithNoContext() { 58 | return _.filter(this.todos, (todo) => todo.contexts.length === 0) 59 | } 60 | 61 | todosWithProject(project) { 62 | const ret = [] 63 | _.each(this.todos, (todo) => { 64 | if (_.includes(todo.projects, project)) { 65 | ret.push(todo) 66 | } 67 | }) 68 | return ret 69 | } 70 | 71 | todosWithNoProject() { 72 | return _.filter(this.todos, (todo) => todo.projects.length === 0) 73 | } 74 | 75 | getContexts() { 76 | const contexts = [] 77 | _.each(this.todos, (todo) => { 78 | _.each(todo.contexts, (context) => { 79 | if (!_.includes(contexts, context)) { 80 | contexts.push(context) 81 | } 82 | }) 83 | }) 84 | return contexts 85 | } 86 | 87 | getProjects() { 88 | const projects = [] 89 | _.each(this.todos, (todo) => { 90 | _.each(todo.projects, (project) => { 91 | if (!_.includes(projects, project)) { 92 | projects.push(project) 93 | } 94 | }) 95 | }) 96 | return projects 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/logic/tests/date_filter.test.js: -------------------------------------------------------------------------------- 1 | import DateFilter from '../date_filter' 2 | import expect from 'expect' 3 | import moment from 'moment' 4 | 5 | describe('DateFilter', () => { 6 | describe('Filter by day', () => { 7 | it('shows past todos that are not compconste', () => { 8 | const t1 = { subject: 'one', due: '2016-06-05', completed: false } 9 | const t2 = { subject: 'two', due: '2016-06-06', completed: false } 10 | const t3 = { subject: 'two', due: '2016-06-07', completed: false } 11 | const todos = [t1, t2, t3] 12 | 13 | const dateFilter = new DateFilter(todos) 14 | const results = dateFilter.filterByDay(moment('2016-06-06')) 15 | expect(results).toEqual([t1, t2]) 16 | }) 17 | 18 | it('does not show past todos that are compconste', () => { 19 | const t1 = { subject: 'one', due: '2016-06-05', completed: true } 20 | const t2 = { subject: 'two', due: '2016-06-06', completed: false } 21 | const t3 = { subject: 'two', due: '2016-06-07', completed: false } 22 | const todos = [t1, t2, t3] 23 | 24 | const dateFilter = new DateFilter(todos) 25 | const results = dateFilter.filterByDay(moment('2016-06-06')) 26 | expect(results).toEqual([t2]) 27 | }) 28 | }) 29 | 30 | describe('Filter by week', () => { 31 | it('shows todos due during the week', () => { 32 | const t1 = { subject: 'one', due: '2016-06-05', completed: true } 33 | const t2 = { subject: 'two', due: '2016-06-06', completed: false } 34 | const t3 = { subject: 'three', due: '2016-06-07', completed: false } 35 | const todos = [t1, t2, t3] 36 | 37 | const dateFilter = new DateFilter(todos) 38 | const results = dateFilter.filterByWeek(moment('2016-06-06')) 39 | expect(results).toEqual([t1, t2, t3]) 40 | }) 41 | 42 | it('shows past todos that are not completed', () => { 43 | const t1 = { subject: 'one', due: '2000-01-05', completed: false } 44 | const t2 = { subject: 'two', due: '2016-06-06', completed: false } 45 | const t3 = { subject: 'three', due: '2016-06-07', completed: false } 46 | const todos = [t1, t2, t3] 47 | 48 | const dateFilter = new DateFilter(todos) 49 | const results = dateFilter.filterByWeek(moment('2016-06-06')) 50 | expect(results).toEqual([t1, t2, t3]) 51 | }) 52 | 53 | it('does not show todos that are ahead of the current week', () => { 54 | const t1 = { subject: 'two', due: '2016-06-06', completed: false } 55 | const t2 = { subject: 'two', due: '2016-06-11', completed: false } 56 | const t3 = { subject: 'three', due: '2016-06-12', completed: false } 57 | const todos = [t1, t2, t3] 58 | 59 | const dateFilter = new DateFilter(todos) 60 | const results = dateFilter.filterByWeek(moment('2016-06-06')) 61 | expect(results).toEqual([t1, t2]) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /internals/templates/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * This is the entry file for the application, only setup and boilerplate 5 | * code. 6 | */ 7 | import 'babel-polyfill'; 8 | 9 | // TODO constrain eslint import/no-unresolved rule to this block 10 | // Load the manifest.json file and the .htaccess file 11 | import 'file?name=[name].[ext]!./manifest.json'; // eslint-disable-line import/no-unresolved 12 | import 'file?name=[name].[ext]!./.htaccess'; // eslint-disable-line import/no-unresolved 13 | 14 | // Import all the third party stuff 15 | import React from 'react'; 16 | import ReactDOM from 'react-dom'; 17 | import { Provider } from 'react-redux'; 18 | import { applyRouterMiddleware, Router, browserHistory } from 'react-router'; 19 | import { syncHistoryWithStore } from 'react-router-redux'; 20 | import useScroll from 'react-router-scroll'; 21 | import configureStore from './store'; 22 | 23 | // Import the CSS reset, which HtmlWebpackPlugin transfers to the build folder 24 | import 'sanitize.css/lib/sanitize.css'; 25 | 26 | // Create redux store with history 27 | // this uses the singleton browserHistory provided by react-router 28 | // Optionally, this could be changed to leverage a created history 29 | // e.g. `const browserHistory = useRouterHistory(createBrowserHistory)();` 30 | const initialState = {}; 31 | const store = configureStore(initialState, browserHistory); 32 | 33 | // Sync history and store, as the react-router-redux reducer 34 | // is under the non-default key ("routing"), selectLocationState 35 | // must be provided for resolving how to retrieve the "route" in the state 36 | import { selectLocationState } from 'containers/App/selectors'; 37 | const history = syncHistoryWithStore(browserHistory, store, { 38 | selectLocationState: selectLocationState(), 39 | }); 40 | 41 | // Set up the router, wrapping all Routes in the App component 42 | import App from 'containers/App'; 43 | import createRoutes from './routes'; 44 | const rootRoute = { 45 | component: App, 46 | childRoutes: createRoutes(store), 47 | }; 48 | 49 | ReactDOM.render( 50 | 51 | { 60 | if (!prevProps || !props) { 61 | return true; 62 | } 63 | 64 | if (prevProps.location.pathname !== props.location.pathname) { 65 | return [0, 0]; 66 | } 67 | 68 | return true; 69 | } 70 | ) 71 | ) 72 | } 73 | /> 74 | , 75 | document.getElementById('app') 76 | ); 77 | 78 | // Install ServiceWorker and AppCache in the end since 79 | // it's not most important operation and if main code fails, 80 | // we do not want it installed 81 | import { install } from 'offline-plugin/runtime'; 82 | install(); 83 | -------------------------------------------------------------------------------- /internals/webpack/webpack.base.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * COMMON WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | 8 | module.exports = (options) => ({ 9 | entry: options.entry, 10 | output: Object.assign({ // Compile into js/build.js 11 | path: path.resolve(process.cwd(), 'build'), 12 | publicPath: '/', 13 | }, options.output), // Merge with env dependent settings 14 | module: { 15 | loaders: [{ 16 | test: /\.js$/, // Transform all .js files required somewhere with Babel 17 | loader: 'babel', 18 | exclude: /node_modules/, 19 | query: options.babelQuery, 20 | }, { 21 | // Transform our own .css files with PostCSS and CSS-modules 22 | test: /\.css$/, 23 | exclude: /node_modules/, 24 | loader: options.cssLoaders, 25 | }, { 26 | // Do not transform vendor's CSS with CSS-modules 27 | // The point is that they remain in global scope. 28 | // Since we require these CSS files in our JS or CSS files, 29 | // they will be a part of our compilation either way. 30 | // So, no need for ExtractTextPlugin here. 31 | test: /\.css$/, 32 | include: /node_modules/, 33 | loaders: ['style-loader', 'css-loader'], 34 | }, { 35 | test: /\.jpe?g$|\.gif$|\.png$|\.svg$/i, 36 | loader: 'url-loader?limit=10000', 37 | }, { 38 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 39 | loader: 'file?name=fonts/[name].[hash].[ext]&mimetype=application/font-woff', 40 | }, { 41 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 42 | loader: 'file?name=fonts/[name].[hash].[ext]&mimetype=application/font-woff', 43 | }, { 44 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 45 | loader: 'file?name=fonts/[name].[hash].[ext]&mimetype=application/octet-stream', 46 | }, { 47 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 48 | loader: 'file?name=fonts/[name].[hash].[ext]', 49 | }, { 50 | test: /\.html$/, 51 | loader: 'html-loader', 52 | }, { 53 | test: /\.json$/, 54 | loader: 'json-loader', 55 | }], 56 | }, 57 | plugins: options.plugins.concat([ 58 | new webpack.optimize.CommonsChunkPlugin('common.js'), 59 | new webpack.ProvidePlugin({ 60 | // make fetch available 61 | fetch: 'exports?self.fetch!whatwg-fetch', 62 | }), 63 | 64 | // Always expose NODE_ENV to webpack, in order to use `process.env.NODE_ENV` 65 | // inside your code for any environment checks; UglifyJS will automatically 66 | // drop any unreachable code. 67 | new webpack.DefinePlugin({ 68 | 'process.env': { 69 | NODE_ENV: JSON.stringify(process.env.NODE_ENV), 70 | }, 71 | }), 72 | ]), 73 | postcss: () => options.postcssPlugins, 74 | resolve: { 75 | modules: ['app', 'node_modules'], 76 | extensions: [ 77 | '', 78 | '.js', 79 | '.jsx', 80 | '.react.js', 81 | ], 82 | packageMains: [ 83 | 'jsnext:main', 84 | 'main', 85 | ], 86 | }, 87 | devtool: options.devtool, 88 | target: 'web', // Make web variables accessible to webpack, e.g. window 89 | stats: false, // Don't show stats in the console 90 | progress: true, 91 | }); 92 | -------------------------------------------------------------------------------- /internals/webpack/webpack.prod.babel.js: -------------------------------------------------------------------------------- 1 | // Important modules this config uses 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const OfflinePlugin = require('offline-plugin'); 7 | 8 | // PostCSS plugins 9 | const cssnext = require('postcss-cssnext'); 10 | const postcssFocus = require('postcss-focus'); 11 | const postcssReporter = require('postcss-reporter'); 12 | 13 | module.exports = require('./webpack.base.babel')({ 14 | // In production, we skip all hot-reloading stuff 15 | entry: [ 16 | path.join(process.cwd(), 'app/app.js'), 17 | ], 18 | 19 | // Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets 20 | output: { 21 | filename: '[name].[chunkhash].js', 22 | chunkFilename: '[name].[chunkhash].chunk.js', 23 | }, 24 | 25 | // We use ExtractTextPlugin so we get a seperate CSS file instead 26 | // of the CSS being in the JS and injected as a style tag 27 | cssLoaders: ExtractTextPlugin.extract( 28 | 'style-loader', 29 | 'css-loader?modules&importLoaders=1!postcss-loader' 30 | ), 31 | 32 | // In production, we minify our CSS with cssnano 33 | postcssPlugins: [ 34 | postcssFocus(), 35 | cssnext({ 36 | browsers: ['last 2 versions', 'IE > 10'], 37 | }), 38 | postcssReporter({ 39 | clearMessages: true, 40 | }), 41 | ], 42 | plugins: [ 43 | 44 | // OccurrenceOrderPlugin is needed for long-term caching to work properly. 45 | // See http://mxs.is/googmv 46 | new webpack.optimize.OccurrenceOrderPlugin(true), 47 | 48 | // Merge all duplicate modules 49 | new webpack.optimize.DedupePlugin(), 50 | 51 | // Minify and optimize the JavaScript 52 | new webpack.optimize.UglifyJsPlugin({ 53 | compress: { 54 | warnings: false, // ...but do not show warnings in the console (there is a lot of them) 55 | }, 56 | }), 57 | 58 | // Minify and optimize the index.html 59 | new HtmlWebpackPlugin({ 60 | template: 'app/index.html', 61 | minify: { 62 | removeComments: true, 63 | collapseWhitespace: true, 64 | removeRedundantAttributes: true, 65 | useShortDoctype: true, 66 | removeEmptyAttributes: true, 67 | removeStyleLinkTypeAttributes: true, 68 | keepClosingSlash: true, 69 | minifyJS: true, 70 | minifyCSS: true, 71 | minifyURLs: true, 72 | }, 73 | inject: true, 74 | }), 75 | 76 | // Extract the CSS into a seperate file 77 | new ExtractTextPlugin('[name].[contenthash].css'), 78 | 79 | // Put it in the end to capture all the HtmlWebpackPlugin's 80 | // assets manipulations and do leak its manipulations to HtmlWebpackPlugin 81 | new OfflinePlugin({ 82 | // No need to cache .htaccess. See http://mxs.is/googmp, 83 | // this is applied before any match in `caches` section 84 | excludes: ['.htaccess'], 85 | 86 | caches: { 87 | main: [':rest:'], 88 | 89 | // All chunks marked as `additional`, loaded after main section 90 | // and do not prevent SW to install. Change to `optional` if 91 | // do not want them to be preloaded at all (cached only when first loaded) 92 | additional: ['*.chunk.js'], 93 | }, 94 | 95 | // Removes warning for about `additional` section usage 96 | safeToUseOptionalCaches: true, 97 | 98 | AppCache: { 99 | // Starting from offline-plugin:v3, AppCache by default caches only 100 | // `main` section. This lets it use `additional` section too 101 | caches: ['main', 'additional'], 102 | }, 103 | }), 104 | ], 105 | }); 106 | -------------------------------------------------------------------------------- /app/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeEvery } from 'redux-saga' 2 | import { okConfirmDialog, openConfirmDialog, openModal } from './actions/modal_actions' 3 | import { openAlert } from './actions/alert_actions' 4 | import { addTodo, updateTodo, deleteTodo } from './actions/todo_actions' 5 | import { take, put, call } from 'redux-saga/effects' 6 | import TestBackend from './backends/TestBackend' 7 | import LocalBackend from './backends/LocalBackend' 8 | import * as constants from './constants' 9 | 10 | let backend 11 | if (window.location.hostname === 'demo.todolist.site') { 12 | backend = new TestBackend() 13 | } else { 14 | backend = new LocalBackend() 15 | } 16 | 17 | export function* runCreateTodo() { 18 | yield put(openModal(constants.ADD_TODO_MODAL)) 19 | const ret = yield take(constants.CREATE_TODO) 20 | yield put(okConfirmDialog()) 21 | try { 22 | yield call(backend.add.bind(backend), ret.todo) 23 | yield put(addTodo(ret.todo)) 24 | yield put(openAlert('The todo has been added.')) 25 | } catch (error) { 26 | yield put(openAlert('A backend failure occurred.')) 27 | } 28 | } 29 | 30 | export function* runDeleteTodo(action) { 31 | yield put(openConfirmDialog('Are you sure you wish to delete this todo?')) 32 | yield take(constants.CONFIRM_DIALOG_OK) 33 | try { 34 | yield call(backend.delete.bind(backend), action.todo) 35 | yield put(deleteTodo(action.todo)) 36 | yield put(openAlert('The todo has been deleted.')) 37 | } catch (error) { 38 | yield put(openAlert('A backend failure occurred.')) 39 | } 40 | } 41 | 42 | export function* runArchiveTodo(action) { 43 | yield put(openConfirmDialog('Are you sure you wish to archive this todo?')) 44 | yield take(constants.CONFIRM_DIALOG_OK) 45 | try { 46 | yield call(backend.update.bind(backend), action.todo) 47 | yield put(updateTodo(action.todo)) 48 | yield put(openAlert('The todo has been archived.')) 49 | } catch (error) { 50 | yield put(openAlert('A backend failure occurred.')) 51 | } 52 | } 53 | 54 | export function* runUnarchiveTodo(action) { 55 | yield put(openConfirmDialog('Are you sure you wish to un-archive this todo?')) 56 | yield take(constants.CONFIRM_DIALOG_OK) 57 | try { 58 | yield call(backend.update.bind(backend), action.todo) 59 | yield put(updateTodo(action.todo)) 60 | yield put(openAlert('The todo has been un-archived.')) 61 | } catch (error) { 62 | yield put(openAlert('A backend failure occurred.')) 63 | } 64 | } 65 | 66 | export function* runFetchTodos() { 67 | try { 68 | const todos = yield call(backend.fetchTodos.bind(backend)) 69 | yield put({ type: constants.TODOS_FETCHED, payload: todos }) 70 | } catch (error) { 71 | yield put(openAlert('A backend failure occurred.')) 72 | } 73 | } 74 | 75 | export function* runUpdateTodo(action) { 76 | try { 77 | yield call(backend.update.bind(backend), action.todo) 78 | yield put(updateTodo(action.todo)) 79 | yield put(openAlert('The todo has been updated.')) 80 | } catch (error) { 81 | yield put(openAlert('A backend failure occurred.')) 82 | } 83 | } 84 | 85 | export function* watchArchiveTodo() { 86 | yield* takeEvery(constants.START_ARCHIVE_TODO_SAGA, runArchiveTodo) 87 | } 88 | 89 | export function* watchUnarchiveTodo() { 90 | yield* takeEvery(constants.START_UNARCHIVE_TODO_SAGA, runUnarchiveTodo) 91 | } 92 | 93 | export function* watchFetchTodos() { 94 | yield* takeEvery(constants.FETCH_TODOS, runFetchTodos) 95 | } 96 | 97 | export function* watchCreateTodo() { 98 | yield* takeEvery(constants.START_CREATE_TODO_SAGA, runCreateTodo) 99 | } 100 | 101 | export function* watchDeleteTodo() { 102 | yield* takeEvery(constants.START_DELETE_TODO_SAGA, runDeleteTodo) 103 | } 104 | 105 | export function* watchUpdateTodo() { 106 | yield* takeEvery(constants.START_UPDATE_TODO_SAGA, runUpdateTodo) 107 | } 108 | 109 | export default function* rootSaga() { 110 | yield [ 111 | watchArchiveTodo(), 112 | watchUnarchiveTodo(), 113 | watchFetchTodos(), 114 | watchCreateTodo(), 115 | watchDeleteTodo(), 116 | watchUpdateTodo(), 117 | ] 118 | } 119 | -------------------------------------------------------------------------------- /internals/generators/container/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Container Generator 3 | */ 4 | 5 | const componentExists = require('../utils/componentExists'); 6 | 7 | module.exports = { 8 | description: 'Add a container component', 9 | prompts: [{ 10 | type: 'input', 11 | name: 'name', 12 | message: 'What should it be called?', 13 | default: 'Form', 14 | validate: value => { 15 | if ((/.+/).test(value)) { 16 | return componentExists(value) ? 'A component or container with this name already exists' : true; 17 | } 18 | 19 | return 'The name is required'; 20 | }, 21 | }, { 22 | type: 'confirm', 23 | name: 'wantCSS', 24 | default: false, 25 | message: 'Does it have styling?', 26 | }, { 27 | type: 'confirm', 28 | name: 'wantActionsAndReducer', 29 | default: true, 30 | message: 'Do you want an actions/constants/selectors/reducer tupel for this container?', 31 | }, { 32 | type: 'confirm', 33 | name: 'wantSagas', 34 | default: true, 35 | message: 'Do you want sagas for asynchronous flows? (e.g. fetching data)', 36 | }], 37 | actions: data => { 38 | // Generate index.js and index.test.js 39 | const actions = [{ 40 | type: 'add', 41 | path: '../../app/containers/{{properCase name}}/index.js', 42 | templateFile: './container/index.js.hbs', 43 | abortOnFail: true, 44 | }, { 45 | type: 'add', 46 | path: '../../app/containers/{{properCase name}}/tests/index.test.js', 47 | templateFile: './container/test.js.hbs', 48 | abortOnFail: true, 49 | }]; 50 | 51 | // If they want a CSS file, add styles.css 52 | if (data.wantCSS) { 53 | actions.push({ 54 | type: 'add', 55 | path: '../../app/containers/{{properCase name}}/styles.css', 56 | templateFile: './container/styles.css.hbs', 57 | abortOnFail: true, 58 | }); 59 | } 60 | 61 | // If they want actions and a reducer, generate actions.js, constants.js, 62 | // reducer.js and the corresponding tests for actions and the reducer 63 | if (data.wantActionsAndReducer) { 64 | // Actions 65 | actions.push({ 66 | type: 'add', 67 | path: '../../app/containers/{{properCase name}}/actions.js', 68 | templateFile: './container/actions.js.hbs', 69 | abortOnFail: true, 70 | }); 71 | actions.push({ 72 | type: 'add', 73 | path: '../../app/containers/{{properCase name}}/tests/actions.test.js', 74 | templateFile: './container/actions.test.js.hbs', 75 | abortOnFail: true, 76 | }); 77 | 78 | // Constants 79 | actions.push({ 80 | type: 'add', 81 | path: '../../app/containers/{{properCase name}}/constants.js', 82 | templateFile: './container/constants.js.hbs', 83 | abortOnFail: true, 84 | }); 85 | 86 | // Selectors 87 | actions.push({ 88 | type: 'add', 89 | path: '../../app/containers/{{properCase name}}/selectors.js', 90 | templateFile: './container/selectors.js.hbs', 91 | abortOnFail: true, 92 | }); 93 | actions.push({ 94 | type: 'add', 95 | path: '../../app/containers/{{properCase name}}/tests/selectors.test.js', 96 | templateFile: './container/selectors.test.js.hbs', 97 | abortOnFail: true, 98 | }); 99 | 100 | // Reducer 101 | actions.push({ 102 | type: 'add', 103 | path: '../../app/containers/{{properCase name}}/reducer.js', 104 | templateFile: './container/reducer.js.hbs', 105 | abortOnFail: true, 106 | }); 107 | actions.push({ 108 | type: 'add', 109 | path: '../../app/containers/{{properCase name}}/tests/reducer.test.js', 110 | templateFile: './container/reducer.test.js.hbs', 111 | abortOnFail: true, 112 | }); 113 | actions.push({ // Add the reducer to the reducer.js file 114 | type: 'modify', 115 | path: '../../app/reducers.js', 116 | pattern: /(\.\.\.asyncReducers,\n {2}}\);)/gi, 117 | template: '{{camelCase name}}: {{camelCase name}}Reducer,\n $1', 118 | }); 119 | actions.push({ 120 | type: 'modify', 121 | path: '../../app/reducers.js', 122 | pattern: /(export default function createReducer)/gi, 123 | template: 'import {{camelCase name}}Reducer from \'containers/{{properCase name}}/reducer\';\n$1', 124 | }); 125 | } 126 | 127 | // Sagas 128 | if (data.wantSagas) { 129 | actions.push({ 130 | type: 'add', 131 | path: '../../app/containers/{{properCase name}}/sagas.js', 132 | templateFile: './container/sagas.js.hbs', 133 | abortOnFail: true, 134 | }); 135 | actions.push({ 136 | type: 'add', 137 | path: '../../app/containers/{{properCase name}}/tests/sagas.test.js', 138 | templateFile: './container/sagas.test.js.hbs', 139 | abortOnFail: true, 140 | }); 141 | } 142 | 143 | return actions; 144 | }, 145 | }; 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boilerplate", 3 | "version": "3.1.0", 4 | "description": "A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/mxstbr/react-boilerplate.git" 8 | }, 9 | "engines": { 10 | "yarn": ">=3" 11 | }, 12 | "author": "Max Stoiber", 13 | "license": "MIT", 14 | "scripts": { 15 | "analyze:clean": "rimraf stats.json", 16 | "preanalyze": "yarn run analyze:clean", 17 | "analyze": "node ./internals/scripts/analyze.js", 18 | "extract-intl": "babel-node --presets es2015,stage-0 -- ./internals/scripts/extract-intl.js", 19 | "yarncheckversion": "node ./internals/scripts/npmcheckversion.js", 20 | "prebuild": "yarn run build:clean", 21 | "build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.babel.js --color -p", 22 | "build:clean": "yarn run test:clean && rimraf ./build", 23 | "build:dll": "node ./internals/scripts/dependencies.js", 24 | "start": "cross-env NODE_ENV=development node server", 25 | "start:tunnel": "cross-env NODE_ENV=development ENABLE_TUNNEL=true node server", 26 | "start:production": "yarn run build && yarn run start:prod", 27 | "start:prod": "cross-env NODE_ENV=production node server", 28 | "pagespeed": "node ./internals/scripts/pagespeed.js", 29 | "presetup": "yarn i chalk shelljs", 30 | "setup": "node ./internals/scripts/setup.js", 31 | "clean": "shjs ./internals/scripts/clean.js", 32 | "clean:all": "yarn run analyze:clean && yarn run test:clean && yarn run build:clean", 33 | "generate": "plop --plopfile internals/generators/index.js", 34 | "lint": "yarn run lint:js && yarn run lint:css", 35 | "lint:eslint": "eslint --ignore-path .gitignore --ignore-pattern internals/scripts", 36 | "lint:js": "yarn run lint:eslint -- . ", 37 | "lint:css": "stylelint ./app/**/*.css", 38 | "lint:staged": "lint-staged", 39 | "pretest": "yarn run test:clean", 40 | "test:clean": "rimraf ./coverage", 41 | "test": "cross-env NODE_ENV=test karma start internals/testing/karma.conf.js --single-run", 42 | "test:watch": "yarn run test -- --auto-watch --no-single-run", 43 | "test:firefox": "yarn run test -- --browsers Firefox", 44 | "test:safari": "yarn run test -- --browsers Safari", 45 | "test:ie": "yarn run test -- --browsers IE", 46 | "coveralls": "cat ./coverage/lcov/lcov.info | coveralls" 47 | }, 48 | "lint-staged": { 49 | "*.js": "lint:eslint", 50 | "*.css": "stylelint" 51 | }, 52 | "pre-commit": "lint:staged", 53 | "babel": { 54 | "presets": [ 55 | [ 56 | "es2015", 57 | { 58 | "modules": false 59 | } 60 | ], 61 | "react", 62 | "stage-0" 63 | ], 64 | "env": { 65 | "production": { 66 | "only": [ 67 | "app" 68 | ], 69 | "plugins": [ 70 | "transform-react-remove-prop-types", 71 | "transform-react-constant-elements", 72 | "transform-react-inline-elements" 73 | ] 74 | } 75 | } 76 | }, 77 | "eslintConfig": { 78 | "parser": "babel-eslint", 79 | "extends": "airbnb", 80 | "env": { 81 | "browser": true, 82 | "node": true, 83 | "mocha": true, 84 | "es6": true 85 | }, 86 | "plugins": [ 87 | "react", 88 | "jsx-a11y" 89 | ], 90 | "parserOptions": { 91 | "ecmaVersion": 6, 92 | "sourceType": "module", 93 | "ecmaFeatures": { 94 | "jsx": true 95 | } 96 | }, 97 | "rules": { 98 | "semi": [ 99 | "error", 100 | "never" 101 | ], 102 | "no-multiple-empty-lines": [ 103 | "error", 104 | { 105 | "max": 1, 106 | "maxEOF": 0 107 | } 108 | ], 109 | "arrow-body-style": [ 110 | 2, 111 | "as-needed" 112 | ], 113 | "comma-dangle": [ 114 | 2, 115 | "always-multiline" 116 | ], 117 | "import/imports-first": 0, 118 | "import/newline-after-import": 0, 119 | "import/no-extraneous-dependencies": 0, 120 | "import/no-named-as-default": 0, 121 | "import/no-unresolved": 2, 122 | "import/prefer-default-export": 0, 123 | "indent": [ 124 | 2, 125 | 2, 126 | { 127 | "SwitchCase": 1 128 | } 129 | ], 130 | "jsx-a11y/aria-props": 2, 131 | "jsx-a11y/heading-has-content": 0, 132 | "jsx-a11y/href-no-hash": 2, 133 | "jsx-a11y/label-has-for": 2, 134 | "jsx-a11y/mouse-events-have-key-events": 2, 135 | "jsx-a11y/role-has-required-aria-props": 2, 136 | "jsx-a11y/role-supports-aria-props": 2, 137 | "max-len": 0, 138 | "newline-per-chained-call": 0, 139 | "no-console": 1, 140 | "no-use-before-define": 0, 141 | "prefer-template": 2, 142 | "react/jsx-filename-extension": 0, 143 | "react/jsx-no-target-blank": 0, 144 | "react/require-extension": 0, 145 | "react/self-closing-comp": 0, 146 | "require-yield": 0 147 | }, 148 | "settings": { 149 | "import/resolver": { 150 | "webpack": { 151 | "config": "./internals/webpack/webpack.test.babel.js" 152 | } 153 | } 154 | } 155 | }, 156 | "stylelint": { 157 | "extends": "stylelint-config-standard", 158 | "rules": { 159 | "color-hex-case": "upper", 160 | "string-quotes": "single", 161 | "font-family-name-quotes": "always-where-recommended", 162 | "selector-pseudo-class-no-unknown": [ 163 | true, 164 | { 165 | "ignorePseudoClasses": [ 166 | "global" 167 | ] 168 | } 169 | ], 170 | "indentation": 2 171 | } 172 | }, 173 | "dllPlugin": { 174 | "path": "node_modules/react-boilerplate-dlls", 175 | "exclude": [ 176 | "chalk", 177 | "compression", 178 | "cross-env", 179 | "express", 180 | "ip", 181 | "minimist", 182 | "sanitize.css" 183 | ], 184 | "include": [ 185 | "core-js", 186 | "lodash", 187 | "eventsource-polyfill" 188 | ] 189 | }, 190 | "dependencies": { 191 | "babel-polyfill": "6.13.0", 192 | "chalk": "1.1.3", 193 | "compression": "1.6.2", 194 | "express": "4.14.0", 195 | "fontfaceobserver": "2.0.1", 196 | "history": "3.0.0", 197 | "immutable": "3.8.1", 198 | "intl": "1.2.4", 199 | "invariant": "2.2.1", 200 | "ip": "1.1.3", 201 | "lodash": "4.15.0", 202 | "moment": "^2.14.1", 203 | "react": "15.3.0", 204 | "react-bootstrap": "^0.30.3", 205 | "react-bootstrap-date-picker": "^3.3.1", 206 | "react-dom": "15.3.0", 207 | "react-helmet": "3.1.0", 208 | "react-intl": "2.1.3", 209 | "react-redux": "4.4.5", 210 | "react-router": "2.6.1", 211 | "react-router-redux": "4.0.5", 212 | "react-router-scroll": "0.2.1", 213 | "redux": "3.5.2", 214 | "redux-immutable": "3.0.7", 215 | "redux-logger": "^2.7.4", 216 | "redux-saga": "0.11.0", 217 | "reselect": "2.5.3", 218 | "sanitize.css": "4.1.0", 219 | "warning": "3.0.0", 220 | "whatwg-fetch": "1.0.0" 221 | }, 222 | "devDependencies": { 223 | "babel-cli": "6.11.4", 224 | "babel-core": "6.13.2", 225 | "babel-eslint": "6.1.2", 226 | "babel-loader": "6.2.4", 227 | "babel-plugin-react-intl": "2.1.3", 228 | "babel-plugin-react-transform": "2.0.2", 229 | "babel-plugin-transform-react-constant-elements": "6.9.1", 230 | "babel-plugin-transform-react-inline-elements": "6.8.0", 231 | "babel-plugin-transform-react-remove-prop-types": "0.2.9", 232 | "babel-preset-es2015": "6.13.2", 233 | "babel-preset-react": "6.11.1", 234 | "babel-preset-react-hmre": "1.1.1", 235 | "babel-preset-stage-0": "6.5.0", 236 | "chai": "3.5.0", 237 | "chai-enzyme": "0.5.0", 238 | "cheerio": "0.20.0", 239 | "coveralls": "2.11.12", 240 | "cross-env": "2.0.0", 241 | "css-loader": "0.23.1", 242 | "enzyme": "2.4.1", 243 | "eslint": "3.3.0", 244 | "eslint-config-airbnb": "10.0.1", 245 | "eslint-import-resolver-webpack": "0.5.1", 246 | "eslint-plugin-import": "1.12.0", 247 | "eslint-plugin-jsx-a11y": "2.1.0", 248 | "eslint-plugin-react": "6.0.0", 249 | "eventsource-polyfill": "0.9.6", 250 | "expect": "1.20.2", 251 | "expect-jsx": "2.6.0", 252 | "exports-loader": "0.6.3", 253 | "extract-text-webpack-plugin": "1.0.1", 254 | "file-loader": "0.9.0", 255 | "html-loader": "0.4.3", 256 | "html-webpack-plugin": "2.22.0", 257 | "image-webpack-loader": "2.0.0", 258 | "imports-loader": "0.6.5", 259 | "isparta": "4.0.0", 260 | "isparta-loader": "2.0.0", 261 | "json-loader": "0.5.4", 262 | "karma": "1.2.0", 263 | "karma-chrome-launcher": "1.0.1", 264 | "karma-coverage": "1.1.1", 265 | "karma-firefox-launcher": "1.0.0", 266 | "karma-ie-launcher": "1.0.0", 267 | "karma-mocha": "1.1.1", 268 | "karma-mocha-reporter": "2.1.0", 269 | "karma-safari-launcher": "1.0.0", 270 | "karma-sourcemap-loader": "0.3.7", 271 | "karma-webpack": "1.8.0", 272 | "lint-staged": "2.0.3", 273 | "minimist": "1.2.0", 274 | "mocha": "3.0.2", 275 | "ngrok": "2.2.2", 276 | "null-loader": "0.1.1", 277 | "offline-plugin": "3.4.2", 278 | "plop": "1.5.0", 279 | "postcss-cssnext": "2.7.0", 280 | "postcss-focus": "1.0.0", 281 | "postcss-loader": "0.9.1", 282 | "postcss-reporter": "1.4.1", 283 | "pre-commit": "1.1.3", 284 | "psi": "2.0.4", 285 | "rimraf": "2.5.4", 286 | "shelljs": "0.7.3", 287 | "sinon": "2.0.0-pre", 288 | "style-loader": "0.13.1", 289 | "stylelint": "7.1.0", 290 | "stylelint-config-standard": "12.0.0", 291 | "url-loader": "0.5.7", 292 | "webpack": "2.1.0-beta.15", 293 | "webpack-dev-middleware": "1.6.1", 294 | "webpack-hot-middleware": "2.12.2" 295 | } 296 | } 297 | --------------------------------------------------------------------------------