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 | Cancel
36 | Ok
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 | [](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 |
60 |
61 |
62 | Close
63 | Save changes
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 |
74 |
75 |
76 | Close
77 | Save changes
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 |
--------------------------------------------------------------------------------