12 |
--------------------------------------------------------------------------------
/gulp/config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const dest = './build';
4 | const src = './src';
5 | const relativeSrcPath = path.relative('.', src);
6 |
7 | module.exports = {
8 |
9 | dest: dest,
10 |
11 | js: {
12 | src: `${src}/js/**`,
13 | dest: `${dest}/js`,
14 | uglify: false
15 | },
16 |
17 | copy: {
18 | src: [
19 | `${src}/www/**`
20 | ],
21 | dest: dest
22 | },
23 |
24 | stylus: {
25 | src: [
26 | `${src}/styl/**/!(_)*`
27 | ],
28 | dest: `${dest}/css/`,
29 | output: 'app.css',
30 | autoprefixer: {
31 | browsers: ['last 2 versions']
32 | },
33 | minify: false
34 | },
35 |
36 | watch: {
37 | js: `${relativeSrcPath}/js/**`,
38 | styl: `${relativeSrcPath}/styl/**`,
39 | www: `${relativeSrcPath}/www/index.html`
40 | },
41 |
42 | webserver: {
43 | host: 'localhost',
44 | port: 8000,
45 | livereload: true,
46 | fallback: dest,
47 | open: 'http://localhost:8000'
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/gulp/tasks/build.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 |
3 | gulp.task('build', ['copy', 'stylus']);
4 |
--------------------------------------------------------------------------------
/gulp/tasks/copy.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const config = require('../config').copy;
3 |
4 | gulp.task('copy', () => {
5 | gulp.src(config.src)
6 | .pipe(gulp.dest(config.dest));
7 | });
8 |
--------------------------------------------------------------------------------
/gulp/tasks/default.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 |
3 | gulp.task('default', ['build', 'watch', 'webserver']);
4 |
--------------------------------------------------------------------------------
/gulp/tasks/stylus.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const gulpif = require('gulp-if');
3 | const plumber = require('gulp-plumber');
4 | const stylus = require('gulp-stylus');
5 | const concat = require('gulp-concat');
6 | const autoprefixer = require('gulp-autoprefixer');
7 | const minify = require('gulp-minify-css');
8 | const config = require('../config').stylus;
9 |
10 | gulp.task('stylus', () => {
11 | gulp.src(config.src)
12 | .pipe(plumber())
13 | .pipe(stylus())
14 | .pipe(concat(config.output))
15 | .pipe(autoprefixer(config.autoprefixer))
16 | .pipe(gulpif(config.minify, minify()))
17 | .pipe(gulp.dest(config.dest));
18 | });
19 |
--------------------------------------------------------------------------------
/gulp/tasks/watch.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const watch = require('gulp-watch');
3 | const config = require('../config').watch;
4 |
5 | gulp.task('watch', () => {
6 | // js
7 | watch(config.js, () => {
8 | gulp.start(['webpack']);
9 | });
10 |
11 | // styl
12 | watch(config.styl, () => {
13 | gulp.start(['stylus']);
14 | });
15 |
16 | // www
17 | watch(config.www, () => {
18 | gulp.start(['copy']);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/gulp/tasks/webserver.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const webserver = require('gulp-webserver');
3 | const config = require('../config');
4 |
5 | gulp.task('webserver', () => {
6 | gulp.src(config.dest)
7 | .pipe(webserver(config.webserver));
8 | });
9 |
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | const requireDir = require('require-dir');
2 |
3 | requireDir('./gulp/tasks', { recurse: true });
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-flux-todomvc-example",
3 | "version": "0.1.0",
4 | "description": "",
5 | "main": "src/js/app.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "gulp build && webpack",
9 | "start": "npm run build && gulp watch webserver"
10 | },
11 | "author": "sskyu",
12 | "license": "MIT",
13 | "devDependencies": {
14 | "babel-cli": "^6.16.0",
15 | "babel-core": "^6.16.0",
16 | "babel-loader": "^6.2.5",
17 | "babel-plugin-transform-class-properties": "^6.16.0",
18 | "babel-preset-es2015": "^6.16.0",
19 | "babel-preset-react": "^6.16.0",
20 | "babel-register": "^6.16.3",
21 | "gulp": "~3.9.0",
22 | "gulp-autoprefixer": "^2.0.0",
23 | "gulp-concat": "^2.4.2",
24 | "gulp-if": "~1.2.5",
25 | "gulp-minify-css": "^0.3.11",
26 | "gulp-plumber": "^0.6.6",
27 | "gulp-stylus": "^1.3.4",
28 | "gulp-watch": "^3.0.0",
29 | "gulp-webserver": "^0.9.0",
30 | "require-dir": "~0.1.0",
31 | "webpack": "^1.13.2"
32 | },
33 | "dependencies": {
34 | "babel-polyfill": "^6.16.0",
35 | "classnames": "^2.2.5",
36 | "events": "~1.0.2",
37 | "flux": "~3.0.0",
38 | "keymirror": "~0.1.1",
39 | "react": "^15.3.2",
40 | "react-dom": "^15.3.2"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/js/actions/todoActions.js:
--------------------------------------------------------------------------------
1 | import TodoDispatcher from '../dispatcher/TodoDispatcher';
2 | import todoConstants from '../constants/todo';
3 | import todoApi from '../utils/todoApi';
4 |
5 | export default {
6 | // user actions
7 | create: (text) => {
8 | TodoDispatcher.dispatch({
9 | actionType: todoConstants.CREATE,
10 | text: text
11 | });
12 |
13 | todoApi.save(text);
14 | },
15 |
16 | updateText: (id, text) => {
17 | TodoDispatcher.dispatch({
18 | actionType: todoConstants.UPDATE_TEXT,
19 | id: id,
20 | text: text
21 | });
22 |
23 | todoApi.update({
24 | id: id,
25 | text: text
26 | });
27 | },
28 |
29 | toggleComplete: (todo) => {
30 | let id = todo.id;
31 | let actionType = todo.complete ?
32 | todoConstants.UNDO_COMPLETE :
33 | todoConstants.COMPLETE;
34 |
35 | TodoDispatcher.dispatch({
36 | actionType: actionType,
37 | id: id
38 | });
39 |
40 | todoApi.update({
41 | id: todo.id,
42 | text: todo.text,
43 | complete: !todo.complete
44 | });
45 | },
46 |
47 | toggleCompleteAll: () => {
48 | TodoDispatcher.dispatch({
49 | actionType: todoConstants.TOGGLE_COMPLETE_ALL
50 | });
51 |
52 | todoApi.toggleComplete();
53 | },
54 |
55 | destroy: (id) => {
56 | TodoDispatcher.dispatch({
57 | actionType: todoConstants.DESTROY,
58 | id: id
59 | });
60 |
61 | todoApi.destroy(id);
62 | },
63 |
64 | destroyCompleted: () => {
65 | TodoDispatcher.dispatch({
66 | actionType: todoConstants.DESTROY_COMPLETED
67 | });
68 |
69 | todoApi.destroyCompleted();
70 | },
71 |
72 | // api actions
73 | fetchTodos: () => {
74 | todoApi.fetch((todos) => {
75 | TodoDispatcher.dispatch({
76 | actionType: todoConstants.FETCH_TODOS,
77 | todos: todos
78 | });
79 | });
80 | },
81 |
82 | syncTodos: (todos) => {
83 | TodoDispatcher.dispatch({
84 | actionType: todoConstants.SYNC_TODOS,
85 | todos: todos
86 | });
87 | }
88 | }
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import React from 'react';
3 | import { render } from 'react-dom';
4 | import TodoApp from './components/TodoApp';
5 |
6 | render(
7 | ,
8 | document.getElementById('todoapp')
9 | );
10 |
--------------------------------------------------------------------------------
/src/js/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import todoActions from '../actions/todoActions';
3 |
4 | const PT = React.PropTypes;
5 |
6 | export default class Footer extends Component {
7 |
8 | static propTypes = {
9 | allTodos: PropTypes.object.isRequired
10 | };
11 |
12 | constructor(...args) {
13 | super(...args);
14 |
15 | this.handleClick = this.handleClick.bind(this);
16 | }
17 |
18 | render() {
19 | let total = this._getTotal();
20 |
21 | if (!total) {
22 | return null;
23 | }
24 |
25 | let completedNum = this._getCompletedNum();
26 | let itemsLeft = total - completedNum;
27 | let itemsLeftPhrase = itemsLeft === 1 ? ' item ' : ' items ';
28 | itemsLeftPhrase += 'left';
29 |
30 | let clearCompleteButton = this._createClearCompleteButton(completedNum);
31 |
32 | return (
33 |
42 | );
43 | }
44 |
45 | _getTotal() {
46 | return Object.keys(this.props.allTodos).length;
47 | }
48 |
49 | _getCompletedNum() {
50 | let completed = 0;
51 | let allTodos = this.props.allTodos;
52 |
53 | for (let key in allTodos) {
54 | if (allTodos[key].complete) {
55 | completed++;
56 | }
57 | }
58 |
59 | return completed;
60 | }
61 |
62 | _createClearCompleteButton(completed) {
63 | if (!completed) {
64 | return null;
65 | }
66 |
67 | return (
68 |
74 | );
75 | }
76 |
77 | handleClick() {
78 | todoActions.destroyCompleted();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/js/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import todoActions from '../actions/todoActions';
3 | import TodoTextInput from './TodoTextInput';
4 |
5 | export default class Header extends Component {
6 |
7 | constructor(...args) {
8 | super(...args);
9 |
10 | this.handleSave = this.handleSave.bind(this);
11 | }
12 |
13 | render() {
14 | return (
15 |
23 | );
24 | }
25 |
26 | handleSave(text) {
27 | if (text.trim()) {
28 | todoActions.create(text);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/js/components/MainSection.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import TodoActions from '../actions/todoActions';
3 | import TodoItem from './TodoItem';
4 |
5 | export default class MainSection extends Component {
6 |
7 | static propTypes = {
8 | allTodos: PropTypes.object.isRequired,
9 | areAllComplete: PropTypes.bool.isRequired
10 | };
11 |
12 | constructor(...args) {
13 | super(...args);
14 |
15 | this.handleChange = this.handleChange.bind(this);
16 | }
17 |
18 | render() {
19 | if (!this._hasTodo()) {
20 | return null;
21 | }
22 |
23 | let todos = this._getAllTodos();
24 |
25 | return (
26 |
38 | );
39 | }
40 |
41 | _hasTodo() {
42 | return Object.keys(this.props.allTodos).length > 0;
43 | }
44 |
45 | _getAllTodos() {
46 | let allTodos = this.props.allTodos;
47 | let todos = [];
48 |
49 | for (let key in allTodos) {
50 | todos.push();
51 | }
52 |
53 | return todos;
54 | }
55 |
56 | handleChange() {
57 | TodoActions.toggleCompleteAll();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/js/components/TodoApp.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import todoStore from '../stores/todoStore';
3 | import todoActions from '../actions/todoActions';
4 | import Header from './Header';
5 | import MainSection from './MainSection';
6 | import Footer from './Footer';
7 |
8 | function getTodoState() {
9 | return {
10 | allTodos: todoStore.getAll(),
11 | areAllComplete: todoStore.areAllComplete()
12 | }
13 | }
14 |
15 | export default class TodoApp extends React.Component {
16 |
17 | state = getTodoState();
18 |
19 | componentDidMount() {
20 | todoStore.addChangeListener(this._onChange.bind(this));
21 |
22 | todoActions.fetchTodos();
23 | }
24 |
25 | componentWillUnmount() {
26 | todoStore.removeChangeListener(this._onChange.bind(this));
27 | }
28 |
29 | render() {
30 | return (
31 |
32 |
33 |
37 |
38 |
39 | );
40 | }
41 |
42 | _onChange() {
43 | this.setState(getTodoState());
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/js/components/TodoItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import classNames from 'classnames';
3 | import todoActions from '../actions/todoActions';
4 | import TodoTextInput from './TodoTextInput';
5 |
6 | export default class TodoItem extends Component {
7 |
8 | static propTypes = {
9 | todo: PropTypes.object.isRequired
10 | };
11 |
12 | state = { isEditing: false };
13 |
14 | constructor(...args) {
15 | super(...args);
16 |
17 | this.handleSave = this.handleSave.bind(this);
18 | this.handleChangeCheckbox = this.handleChangeCheckbox.bind(this);
19 | this.handleDoubleClick = this.handleDoubleClick.bind(this);
20 | this.handleDestroyClick = this.handleDestroyClick.bind(this);
21 | }
22 |
23 | render() {
24 | let todo = this.props.todo;
25 | let input;
26 |
27 | if (this.state.isEditing) {
28 | input =
29 |
34 | }
35 |
36 | return (
37 |
41 |
42 |
48 |
51 |
55 |
56 | {input}
57 |
58 | );
59 | }
60 |
61 | _getListClassName(todo) {
62 | return classNames({
63 | 'completed': todo.complete,
64 | 'editing': this.state.isEditing
65 | });
66 | }
67 |
68 | handleChangeCheckbox() {
69 | todoActions.toggleComplete(this.props.todo);
70 | }
71 |
72 | handleDoubleClick() {
73 | this.setState({ isEditing: true });
74 | }
75 |
76 | handleSave(text) {
77 | todoActions.updateText(this.props.todo.id, text);
78 | this.setState({ isEditing: false });
79 | }
80 |
81 | handleDestroyClick() {
82 | todoActions.destroy(this.props.todo.id);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/js/components/TodoTextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | const ENTER_KEY_CODE = 13;
4 |
5 | export default class TodoTextInput extends Component {
6 |
7 | static propTypes = {
8 | className : PropTypes.string,
9 | id : PropTypes.string,
10 | placeholder : PropTypes.string,
11 | onSave : PropTypes.func.isRequired,
12 | value : PropTypes.string
13 | }
14 |
15 | static defaultProps = { value: '' };
16 |
17 | state = { value: this.props.value };
18 |
19 | constructor(...args) {
20 | super(...args);
21 |
22 | this.handleBlur = this.handleBlur.bind(this);
23 | this.handleChange = this.handleChange.bind(this);
24 | this.handleKeyDown = this.handleKeyDown.bind(this);
25 | }
26 |
27 | render() {
28 | return (
29 |
39 | );
40 | }
41 |
42 | _save() {
43 | this.props.onSave(this.state.value);
44 | this.setState({
45 | value: ''
46 | });
47 | }
48 |
49 | handleBlur() {
50 | this._save();
51 | }
52 |
53 | handleChange(e) {
54 | this.setState({
55 | value: e.target.value
56 | });
57 | }
58 |
59 | handleKeyDown(e) {
60 | if (e.keyCode === ENTER_KEY_CODE) {
61 | this._save();
62 | }
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/src/js/constants/todo.js:
--------------------------------------------------------------------------------
1 | import keyMirror from 'keymirror';
2 |
3 | const todoConstant = keyMirror({
4 | CREATE : null,
5 | UPDATE_TEXT : null,
6 | COMPLETE : null,
7 | UNDO_COMPLETE : null,
8 | TOGGLE_COMPLETE_ALL : null,
9 | DESTROY : null,
10 | DESTROY_COMPLETE : null,
11 | FETCH_TODOS : null,
12 | SYNC_TODOS : null,
13 | });
14 |
15 | export default todoConstant
16 |
--------------------------------------------------------------------------------
/src/js/dispatcher/TodoDispatcher.js:
--------------------------------------------------------------------------------
1 | // var Dispatcher = require('flux').Dispatcher;
2 |
3 | import Flux from 'flux';
4 |
5 | module.exports = new Flux.Dispatcher();
6 |
--------------------------------------------------------------------------------
/src/js/stores/todoStore.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 | import TodoDispatcher from '../dispatcher/TodoDispatcher';
3 | import todoConstants from '../constants/todo';
4 |
5 | const CHANGE_EVENT = 'change';
6 |
7 | let _todos = {};
8 |
9 | function create(text) {
10 | let id = ('temp_' + (Math.random() * 999999 | 0)).toString(36);
11 | _todos[id] = {
12 | 'id': id,
13 | complete: false,
14 | text: text,
15 | };
16 | }
17 |
18 | function update(id, updates) {
19 | _todos[id] = Object.assign({}, _todos[id], updates);
20 | }
21 |
22 | function updateAll(updates) {
23 | for (let id in _todos) {
24 | update(id, updates);
25 | }
26 | }
27 |
28 | function destroy(id) {
29 | delete _todos[id];
30 | }
31 |
32 | function destroyCompleted() {
33 | for (let id in _todos) {
34 | if (_todos[id].complete) {
35 | destroy(id);
36 | }
37 | }
38 | }
39 |
40 | class TodoStore extends EventEmitter {
41 |
42 | constructor() {
43 | super();
44 |
45 | TodoDispatcher.register(this.handler.bind(this));
46 | }
47 |
48 | areAllComplete() {
49 | for (let id in _todos) {
50 | if (!_todos[id].complete) {
51 | return false;
52 | }
53 | }
54 | return true;
55 | }
56 |
57 | getAll() {
58 | return _todos;
59 | }
60 |
61 | emitChange() {
62 | this.emit(CHANGE_EVENT);
63 | }
64 |
65 | addChangeListener(callback) {
66 | this.on(CHANGE_EVENT, callback);
67 | }
68 |
69 | removeChangeListener(callback) {
70 | this.removeListener(CHANGE_EVENT, callback);
71 | }
72 |
73 | /**
74 | * Register callback to handle all updates
75 | *
76 | * @param {Object} action
77 | */
78 | handler(action) {
79 | let text;
80 |
81 | switch (action.actionType) {
82 | case todoConstants.CREATE:
83 | text = action.text.trim();
84 | if (text !== '') {
85 | create(text);
86 | this.emitChange();
87 | }
88 | break;
89 |
90 | case todoConstants.UPDATE_TEXT:
91 | text = action.text.trim();
92 | if (text !== '') {
93 | update(action.id, { text: text });
94 | this.emitChange();
95 | }
96 | break;
97 |
98 | case todoConstants.COMPLETE:
99 | update(action.id, { complete: true });
100 | this.emitChange();
101 | break;
102 |
103 | case todoConstants.UNDO_COMPLETE:
104 | update(action.id, { complete: false });
105 | this.emitChange();
106 | break;
107 |
108 | case todoConstants.TOGGLE_COMPLETE_ALL:
109 | if (this.areAllComplete()) {
110 | updateAll({ complete: false });
111 | } else {
112 | updateAll({ complete: true });
113 | }
114 | this.emitChange();
115 | break;
116 |
117 | case todoConstants.DESTROY:
118 | destroy(action.id);
119 | this.emitChange();
120 | break;
121 |
122 | case todoConstants.DESTROY_COMPLETED:
123 | destroyCompleted();
124 | this.emitChange();
125 | break;
126 |
127 | case todoConstants.FETCH_TODOS:
128 | case todoConstants.SYNC_TODOS:
129 | _todos = action.todos
130 | this.emitChange();
131 | break;
132 |
133 | default:
134 | }
135 | }
136 | }
137 |
138 | const todoStore = new TodoStore();
139 |
140 | export default todoStore;
141 |
--------------------------------------------------------------------------------
/src/js/utils/todoApi.js:
--------------------------------------------------------------------------------
1 | import todoConstants from '../constants/todo';
2 | import todoActions from '../actions/todoActions';
3 |
4 | function _fetchTodos() {
5 | return new Promise((resolve, reject) => {
6 | let todos;
7 | try {
8 | todos = localStorage.getItem('todos');
9 |
10 | if (!todos) {
11 | todos = {};
12 | } else if (todos === 'undefined') {
13 | todos = {};
14 | } else {
15 | todos = JSON.parse(todos);
16 | }
17 |
18 | resolve(todos);
19 | } catch (e) {
20 | reject(e);
21 | }
22 | });
23 | }
24 |
25 | function _createTodo(text) {
26 | return new Promise((resolve, reject) => {
27 | let id = (Date.now() + (Math.random() * 999999 | 0)).toString(36);
28 | let newTodo = {
29 | 'id': id,
30 | text: text,
31 | complete: false
32 | };
33 |
34 | _fetchTodos()
35 | .then((rawTodos) => {
36 | rawTodos[id] = newTodo;
37 |
38 | _saveTodos(rawTodos)
39 | .then((createdTodos) => {
40 | resolve({
41 | todo: newTodo,
42 | todos: createdTodos
43 | });
44 | }, _onError);
45 |
46 | }, _onError);
47 | })
48 | .catch(_onError);
49 | }
50 |
51 | function _saveTodos(todos) {
52 | return new Promise((resolve, reject) => {
53 | try {
54 | localStorage.setItem('todos', JSON.stringify(todos));
55 | resolve(todos);
56 | } catch (e) {
57 | reject(e);
58 | }
59 | });
60 | }
61 |
62 | function _updateTodo(todo) {
63 | return new Promise((resolve, reject) => {
64 |
65 | if (!todo || !todo.id) {
66 | return reject(new Error('todo.id is empty on _updateTodo()'));
67 | }
68 |
69 | _fetchTodos()
70 | .then((todos) => {
71 | todos[todo.id] = Object.assign({}, todos[todo.id], {
72 | id: todo.id,
73 | text: todo.text,
74 | complete: todo.complete ? true : false
75 | });
76 |
77 | _saveTodos(todos)
78 | .then((savedTodos) => {
79 | resolve(savedTodos);
80 | }, _onError);
81 | })
82 | .catch(_onError);
83 | });
84 | }
85 |
86 | function _toggleCompleteTodos() {
87 | return new Promise((resolve, reject) => {
88 | _fetchTodos()
89 | .then((todos) => {
90 | let isAllComplete = true;
91 | for (let id in todos) {
92 | if (!todos[id].complete) {
93 | isAllComplete = false;
94 | break;
95 | }
96 | }
97 |
98 | for (let id in todos) {
99 | todos[id] = Object.assign({}, todos[id], {
100 | complete: isAllComplete ? false : true
101 | });
102 | }
103 |
104 | _saveTodos(todos)
105 | .then((savedTodos) => {
106 | resolve(savedTodos);
107 | }, _onError);
108 | })
109 | .catch(_onError);
110 | });
111 | }
112 |
113 | function _destroyTodo(id) {
114 | return new Promise((resolve, reject) => {
115 |
116 | if (!id) {
117 | return reject(new Error('id is empty on _destroy()'))
118 | }
119 |
120 | _fetchTodos()
121 | .then((todos) => {
122 | delete todos[id];
123 |
124 | _saveTodos(todos)
125 | .then((savedTodos) => {
126 | resolve(savedTodos);
127 | }, _onError);
128 | })
129 | .catch(_onError);
130 | });
131 | }
132 |
133 | function _destroyCompletedTodos() {
134 | return new Promise((resolve, reject) => {
135 | _fetchTodos()
136 | .then((todos) => {
137 | for (let id in todos) {
138 | if (todos[id] && todos[id].complete) {
139 | delete todos[id];
140 | }
141 | }
142 |
143 | _saveTodos(todos)
144 | .then((savedTodos) => {
145 | resolve(savedTodos);
146 | }, _onError);
147 | })
148 | .catch(_onError);
149 | });
150 | }
151 |
152 | function _onError(e) {
153 | alert(e);
154 | }
155 |
156 | export default {
157 |
158 | fetch: (callback) => {
159 | _fetchTodos()
160 | .then((todos) => {
161 | callback(todos);
162 | })
163 | .catch(_onError);
164 | },
165 |
166 | save: (text) => {
167 | _createTodo(text)
168 | .then((res) => {
169 | todoActions.syncTodos(res.todos);
170 | })
171 | .catch(_onError);
172 | },
173 |
174 | update: (params) => {
175 | _updateTodo(params)
176 | .then((todos) => {
177 | todoActions.syncTodos(todos);
178 | })
179 | .catch(_onError);
180 | },
181 |
182 | updateAll: (params) => {
183 | _fetchTodos()
184 | .then((rawTodos) => {
185 | for (let id in rawTodos) {
186 | rawTodos[id] = Object.assign({}, params, rawTodos[id]);
187 | }
188 | _saveTodos(rawTodos)
189 | .then((todos) => {
190 | todoActions.syncTodos(todos);
191 | }, _onError);
192 | })
193 | .catch(_onError);
194 | },
195 |
196 | toggleComplete: () => {
197 | _toggleCompleteTodos()
198 | .then((todos) => {
199 | todoActions.syncTodos(todos);
200 | })
201 | .catch(_onError);
202 | },
203 |
204 | destroy: (id) => {
205 | _destroyTodo(id)
206 | .then((todos) => {
207 | todoActions.syncTodos(todos);
208 | })
209 | .catch(_onError);
210 | },
211 |
212 | destroyCompleted: () => {
213 | _destroyCompletedTodos()
214 | .then((todos) => {
215 | todoActions.syncTodos(todos);
216 | })
217 | .catch(_onError);
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/src/styl/app.styl:
--------------------------------------------------------------------------------
1 | @import 'base/_reset'
2 | @import 'base/_core'
3 |
4 | #todo-list
5 | li
6 | .edit
7 | display inline
8 |
--------------------------------------------------------------------------------
/src/styl/base/_core.styl:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | color: inherit;
16 | -webkit-appearance: none;
17 | -ms-appearance: none;
18 | -o-appearance: none;
19 | appearance: none;
20 | }
21 |
22 | body {
23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
24 | line-height: 1.4em;
25 | background: #eaeaea url('/img/bg.png');
26 | color: #4d4d4d;
27 | width: 550px;
28 | margin: 0 auto;
29 | -webkit-font-smoothing: antialiased;
30 | -moz-font-smoothing: antialiased;
31 | -ms-font-smoothing: antialiased;
32 | -o-font-smoothing: antialiased;
33 | font-smoothing: antialiased;
34 | }
35 |
36 | button,
37 | input[type="checkbox"] {
38 | outline: none;
39 | }
40 |
41 | #todoapp {
42 | background: #fff;
43 | background: rgba(255, 255, 255, 0.9);
44 | margin: 130px 0 40px 0;
45 | border: 1px solid #ccc;
46 | position: relative;
47 | border-top-left-radius: 2px;
48 | border-top-right-radius: 2px;
49 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
50 | 0 25px 50px 0 rgba(0, 0, 0, 0.15);
51 | }
52 |
53 | #todoapp:before {
54 | content: '';
55 | border-left: 1px solid #f5d6d6;
56 | border-right: 1px solid #f5d6d6;
57 | width: 2px;
58 | position: absolute;
59 | top: 0;
60 | left: 40px;
61 | height: 100%;
62 | }
63 |
64 | #todoapp input::-webkit-input-placeholder {
65 | font-style: italic;
66 | }
67 |
68 | #todoapp input::-moz-placeholder {
69 | font-style: italic;
70 | color: #a9a9a9;
71 | }
72 |
73 | #todoapp h1 {
74 | position: absolute;
75 | top: -60px;
76 | width: 100%;
77 | font-size: 70px;
78 | font-weight: bold;
79 | text-align: center;
80 | color: #b3b3b3;
81 | color: rgba(255, 255, 255, 0.3);
82 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
83 | -webkit-text-rendering: optimizeLegibility;
84 | -moz-text-rendering: optimizeLegibility;
85 | -ms-text-rendering: optimizeLegibility;
86 | -o-text-rendering: optimizeLegibility;
87 | text-rendering: optimizeLegibility;
88 | }
89 |
90 | #header {
91 | padding-top: 15px;
92 | border-radius: inherit;
93 | }
94 |
95 | #header:before {
96 | content: '';
97 | position: absolute;
98 | top: 0;
99 | right: 0;
100 | left: 0;
101 | height: 15px;
102 | z-index: 2;
103 | border-bottom: 1px solid #6c615c;
104 | background: #8d7d77;
105 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
106 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
107 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
108 | // filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
109 | border-top-left-radius: 1px;
110 | border-top-right-radius: 1px;
111 | }
112 |
113 | #new-todo,
114 | .edit {
115 | position: relative;
116 | margin: 0;
117 | width: 100%;
118 | font-size: 24px;
119 | font-family: inherit;
120 | line-height: 1.4em;
121 | border: 0;
122 | outline: none;
123 | color: inherit;
124 | padding: 6px;
125 | border: 1px solid #999;
126 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
127 | -moz-box-sizing: border-box;
128 | -ms-box-sizing: border-box;
129 | -o-box-sizing: border-box;
130 | box-sizing: border-box;
131 | -webkit-font-smoothing: antialiased;
132 | -moz-font-smoothing: antialiased;
133 | -ms-font-smoothing: antialiased;
134 | -o-font-smoothing: antialiased;
135 | font-smoothing: antialiased;
136 | }
137 |
138 | #new-todo {
139 | padding: 16px 16px 16px 60px;
140 | border: none;
141 | background: rgba(0, 0, 0, 0.02);
142 | z-index: 2;
143 | box-shadow: none;
144 | }
145 |
146 | #main {
147 | position: relative;
148 | z-index: 2;
149 | border-top: 1px dotted #adadad;
150 | }
151 |
152 | label[for='toggle-all'] {
153 | display: none;
154 | }
155 |
156 | #toggle-all {
157 | position: absolute;
158 | top: -42px;
159 | left: -4px;
160 | width: 40px;
161 | text-align: center;
162 | /* Mobile Safari */
163 | border: none;
164 | }
165 |
166 | #toggle-all:before {
167 | content: '»';
168 | font-size: 28px;
169 | color: #d9d9d9;
170 | padding: 0 25px 7px;
171 | }
172 |
173 | #toggle-all:checked:before {
174 | color: #737373;
175 | }
176 |
177 | #todo-list {
178 | margin: 0;
179 | padding: 0;
180 | list-style: none;
181 | }
182 |
183 | #todo-list li {
184 | position: relative;
185 | font-size: 24px;
186 | border-bottom: 1px dotted #ccc;
187 | }
188 |
189 | #todo-list li:last-child {
190 | border-bottom: none;
191 | }
192 |
193 | #todo-list li.editing {
194 | border-bottom: none;
195 | padding: 0;
196 | }
197 |
198 | #todo-list li.editing .edit {
199 | display: block;
200 | width: 506px;
201 | padding: 13px 17px 12px 17px;
202 | margin: 0 0 0 43px;
203 | }
204 |
205 | #todo-list li.editing .view {
206 | display: none;
207 | }
208 |
209 | #todo-list li .toggle {
210 | text-align: center;
211 | width: 40px;
212 | /* auto, since non-WebKit browsers doesn't support input styling */
213 | height: auto;
214 | position: absolute;
215 | top: 0;
216 | bottom: 0;
217 | margin: auto 0;
218 | /* Mobile Safari */
219 | border: none;
220 | -webkit-appearance: none;
221 | -ms-appearance: none;
222 | -o-appearance: none;
223 | appearance: none;
224 | }
225 |
226 | #todo-list li .toggle:after {
227 | content: '✔';
228 | /* 40 + a couple of pixels visual adjustment */
229 | line-height: 43px;
230 | font-size: 20px;
231 | color: #d9d9d9;
232 | text-shadow: 0 -1px 0 #bfbfbf;
233 | }
234 |
235 | #todo-list li .toggle:checked:after {
236 | color: #85ada7;
237 | text-shadow: 0 1px 0 #669991;
238 | bottom: 1px;
239 | position: relative;
240 | }
241 |
242 | #todo-list li label {
243 | white-space: pre;
244 | word-break: break-word;
245 | padding: 15px 60px 15px 15px;
246 | margin-left: 45px;
247 | display: block;
248 | line-height: 1.2;
249 | -webkit-transition: color 0.4s;
250 | transition: color 0.4s;
251 | }
252 |
253 | #todo-list li.completed label {
254 | color: #a9a9a9;
255 | text-decoration: line-through;
256 | }
257 |
258 | #todo-list li .destroy {
259 | display: none;
260 | position: absolute;
261 | top: 0;
262 | right: 10px;
263 | bottom: 0;
264 | width: 40px;
265 | height: 40px;
266 | margin: auto 0;
267 | font-size: 22px;
268 | color: #a88a8a;
269 | -webkit-transition: all 0.2s;
270 | transition: all 0.2s;
271 | }
272 |
273 | #todo-list li .destroy:hover {
274 | text-shadow: 0 0 1px #000,
275 | 0 0 10px rgba(199, 107, 107, 0.8);
276 | -webkit-transform: scale(1.3);
277 | -ms-transform: scale(1.3);
278 | transform: scale(1.3);
279 | }
280 |
281 | #todo-list li .destroy:after {
282 | content: '✖';
283 | }
284 |
285 | #todo-list li:hover .destroy {
286 | display: block;
287 | }
288 |
289 | #todo-list li .edit {
290 | display: none;
291 | }
292 |
293 | #todo-list li.editing:last-child {
294 | margin-bottom: -1px;
295 | }
296 |
297 | #footer {
298 | color: #777;
299 | padding: 0 15px;
300 | position: absolute;
301 | right: 0;
302 | bottom: -31px;
303 | left: 0;
304 | height: 20px;
305 | z-index: 1;
306 | text-align: center;
307 | }
308 |
309 | #footer:before {
310 | content: '';
311 | position: absolute;
312 | right: 0;
313 | bottom: 31px;
314 | left: 0;
315 | height: 50px;
316 | z-index: -1;
317 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
318 | 0 6px 0 -3px rgba(255, 255, 255, 0.8),
319 | 0 7px 1px -3px rgba(0, 0, 0, 0.3),
320 | 0 43px 0 -6px rgba(255, 255, 255, 0.8),
321 | 0 44px 2px -6px rgba(0, 0, 0, 0.2);
322 | }
323 |
324 | #todo-count {
325 | float: left;
326 | text-align: left;
327 | }
328 |
329 | #filters {
330 | margin: 0;
331 | padding: 0;
332 | list-style: none;
333 | position: absolute;
334 | right: 0;
335 | left: 0;
336 | }
337 |
338 | #filters li {
339 | display: inline;
340 | }
341 |
342 | #filters li a {
343 | color: #83756f;
344 | margin: 2px;
345 | text-decoration: none;
346 | }
347 |
348 | #filters li a.selected {
349 | font-weight: bold;
350 | }
351 |
352 | #clear-completed {
353 | float: right;
354 | position: relative;
355 | line-height: 20px;
356 | text-decoration: none;
357 | background: rgba(0, 0, 0, 0.1);
358 | font-size: 11px;
359 | padding: 0 10px;
360 | border-radius: 3px;
361 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
362 | }
363 |
364 | #clear-completed:hover {
365 | background: rgba(0, 0, 0, 0.15);
366 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
367 | }
368 |
369 | #info {
370 | margin: 65px auto 0;
371 | color: #a6a6a6;
372 | font-size: 12px;
373 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
374 | text-align: center;
375 | }
376 |
377 | #info a {
378 | color: inherit;
379 | }
380 |
381 | /*
382 | Hack to remove background from Mobile Safari.
383 | Can't use it globally since it destroys checkboxes in Firefox and Opera
384 | */
385 |
386 | @media screen and (-webkit-min-device-pixel-ratio:0) {
387 | #toggle-all,
388 | #todo-list li .toggle {
389 | background: none;
390 | }
391 |
392 | #todo-list li .toggle {
393 | height: 40px;
394 | }
395 |
396 | #toggle-all {
397 | top: -56px;
398 | left: -15px;
399 | width: 65px;
400 | height: 41px;
401 | -webkit-transform: rotate(90deg);
402 | -ms-transform: rotate(90deg);
403 | transform: rotate(90deg);
404 | -webkit-appearance: none;
405 | appearance: none;
406 | }
407 | }
408 |
409 | .hidden {
410 | display: none;
411 | }
412 |
413 | hr {
414 | margin: 20px 0;
415 | border: 0;
416 | border-top: 1px dashed #C5C5C5;
417 | border-bottom: 1px dashed #F7F7F7;
418 | }
419 |
420 | .learn a {
421 | font-weight: normal;
422 | text-decoration: none;
423 | color: #b83f45;
424 | }
425 |
426 | .learn a:hover {
427 | text-decoration: underline;
428 | color: #787e7e;
429 | }
430 |
431 | .learn h3,
432 | .learn h4,
433 | .learn h5 {
434 | margin: 10px 0;
435 | font-weight: 500;
436 | line-height: 1.2;
437 | color: #000;
438 | }
439 |
440 | .learn h3 {
441 | font-size: 24px;
442 | }
443 |
444 | .learn h4 {
445 | font-size: 18px;
446 | }
447 |
448 | .learn h5 {
449 | margin-bottom: 0;
450 | font-size: 14px;
451 | }
452 |
453 | .learn ul {
454 | padding: 0;
455 | margin: 0 0 30px 25px;
456 | }
457 |
458 | .learn li {
459 | line-height: 20px;
460 | }
461 |
462 | .learn p {
463 | font-size: 15px;
464 | font-weight: 300;
465 | line-height: 1.3;
466 | margin-top: 0;
467 | margin-bottom: 0;
468 | }
469 |
470 | .quote {
471 | border: none;
472 | margin: 20px 0 60px 0;
473 | }
474 |
475 | .quote p {
476 | font-style: italic;
477 | }
478 |
479 | .quote p:before {
480 | content: '“';
481 | font-size: 50px;
482 | opacity: .15;
483 | position: absolute;
484 | top: -20px;
485 | left: 3px;
486 | }
487 |
488 | .quote p:after {
489 | content: '”';
490 | font-size: 50px;
491 | opacity: .15;
492 | position: absolute;
493 | bottom: -42px;
494 | right: 3px;
495 | }
496 |
497 | .quote footer {
498 | position: absolute;
499 | bottom: -40px;
500 | right: 0;
501 | }
502 |
503 | .quote footer img {
504 | border-radius: 3px;
505 | }
506 |
507 | .quote footer a {
508 | margin-left: 5px;
509 | vertical-align: middle;
510 | }
511 |
512 | .speech-bubble {
513 | position: relative;
514 | padding: 10px;
515 | background: rgba(0, 0, 0, .04);
516 | border-radius: 5px;
517 | }
518 |
519 | .speech-bubble:after {
520 | content: '';
521 | position: absolute;
522 | top: 100%;
523 | right: 30px;
524 | border: 13px solid transparent;
525 | border-top-color: rgba(0, 0, 0, .04);
526 | }
527 |
528 | .learn-bar > .learn {
529 | position: absolute;
530 | width: 272px;
531 | top: 8px;
532 | left: -300px;
533 | padding: 10px;
534 | border-radius: 5px;
535 | background-color: rgba(255, 255, 255, .6);
536 | -webkit-transition-property: left;
537 | transition-property: left;
538 | -webkit-transition-duration: 500ms;
539 | transition-duration: 500ms;
540 | }
541 |
542 | @media (min-width: 899px) {
543 | .learn-bar {
544 | width: auto;
545 | margin: 0 0 0 300px;
546 | }
547 |
548 | .learn-bar > .learn {
549 | left: 8px;
550 | }
551 |
552 | .learn-bar #todoapp {
553 | width: 550px;
554 | margin: 130px auto 40px auto;
555 | }
556 | }
--------------------------------------------------------------------------------
/src/styl/base/_reset.styl:
--------------------------------------------------------------------------------
1 | *
2 | margin 0
3 | padding 0
--------------------------------------------------------------------------------
/src/www/img/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sskyu/react-flux-todomvc-example/3de1cb57689a896248af1992dfe0896af05444a6/src/www/img/bg.png
--------------------------------------------------------------------------------
/src/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | react-flux-todomvc-example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | const src = './src';
2 | const dest = './build';
3 |
4 | module.exports = {
5 | entry: `${src}/js/app.js`,
6 | output: {
7 | path: `${dest}/js/`,
8 | filename: 'bundle.js'
9 | },
10 | resolve: {
11 | extensions: ['', '.js']
12 | },
13 | module: {
14 | loaders: [
15 | {
16 | test: /\.js$/,
17 | exclude: /node_modules/,
18 | loader: 'babel'
19 | }
20 | ]
21 | },
22 | devtool: 'eval'
23 | };
24 |
--------------------------------------------------------------------------------