├── .gitignore
├── .eslintrc.json
├── public
├── favicon.ico
└── index.html
├── src
├── Types.js
├── img
│ └── destroy.png
├── TodoList.css
├── index.js
├── Body.css
├── Footer.css
├── TodoListItem.css
├── App.css
├── Footer.js
├── TodoList.js
├── Body.js
├── TodoListItem.js
└── App.js
├── .flowconfig
├── prettier.config.js
├── README.md
├── LICENSE.md
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app"
3 | }
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssorallen/react-todos/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/Types.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | export type Todo = { done: boolean, title: string };
4 |
--------------------------------------------------------------------------------
/src/img/destroy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ssorallen/react-todos/HEAD/src/img/destroy.png
--------------------------------------------------------------------------------
/src/TodoList.css:
--------------------------------------------------------------------------------
1 | #todo-list {
2 | margin: 10px 0;
3 | padding: 0;
4 | list-style: none;
5 | }
6 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [lints]
8 |
9 | [options]
10 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | jsxBracketSameLine: true,
3 | printWidth: 100,
4 | singleQuote: true,
5 | trailingComma: "es5"
6 | };
7 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import App from './App';
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 |
7 | const todoappRoot = document.getElementById('todoapp');
8 | if (todoappRoot == null) throw new Error('App element #todoapp not found');
9 |
10 | ReactDOM.render(, todoappRoot);
11 |
--------------------------------------------------------------------------------
/src/Body.css:
--------------------------------------------------------------------------------
1 | #todoapp input[type="text"] {
2 | width: 466px;
3 | font-size: 24px;
4 | font-family: inherit;
5 | line-height: 1.4em;
6 | border: 0;
7 | outline: none;
8 | padding: 6px;
9 | border: 1px solid #999999;
10 | box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
11 | }
12 |
13 | #todoapp input::-webkit-input-placeholder {
14 | font-style: italic;
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Todos
2 |
3 | Backbone's example [TODO app](http://backbonejs.org/docs/todos.html) with React JS
4 | Components for views in place of `Backbone.View`.
5 |
6 | ## Demo
7 |
8 | https://ssorallen.github.io/react-todos/
9 |
10 | The demo uses local storage to save todos in the browser. The views are all rendered
11 | with React, which means there is no dependency on another DOM manipulation library
12 | like jQuery.
13 |
14 | ## Running Locally
15 |
16 | 1. Clone this repository
17 | 2. Install dependencies
18 |
19 | $ yarn install
20 | 3. Run the local server
21 |
22 | $ yarn start
23 | 4. See the running app at http://localhost:3000
24 |
25 | ### Favicon
26 |
27 | The great little todo list favicon comes from [Typicons](http://typicons.com/).
28 |
--------------------------------------------------------------------------------
/src/Footer.css:
--------------------------------------------------------------------------------
1 | #todoapp footer {
2 | margin: 0 -20px -20px -20px;
3 | overflow: hidden;
4 | color: #555555;
5 | background: #f4fce8;
6 | border-top: 1px solid #ededed;
7 | padding: 0 20px;
8 | line-height: 37px;
9 | border-radius: 0 0 5px 5px;
10 | }
11 |
12 | #clear-completed {
13 | float: right;
14 | line-height: 20px;
15 | text-decoration: none;
16 | background: rgba(0, 0, 0, 0.1);
17 | color: #555555;
18 | font-size: 11px;
19 | margin-top: 8px;
20 | margin-bottom: 8px;
21 | padding: 0 10px 1px;
22 | cursor: pointer;
23 | border-radius: 12px;
24 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
25 | }
26 |
27 | #clear-completed:hover {
28 | background: rgba(0, 0, 0, 0.15);
29 | box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
30 | }
31 |
32 | #clear-completed:active {
33 | position: relative;
34 | top: 1px;
35 | }
36 |
37 | #todo-count span {
38 | font-weight: bold;
39 | }
40 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React.js Todos
7 |
9 |
10 |
11 |
12 |
13 |
14 | Double-click to edit a todo.
15 |
16 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/TodoListItem.css:
--------------------------------------------------------------------------------
1 | #todo-list li {
2 | padding: 18px 20px 18px 0;
3 | position: relative;
4 | font-size: 24px;
5 | border-bottom: 1px solid #cccccc;
6 | }
7 |
8 | #todo-list li:last-child {
9 | border-bottom: none;
10 | }
11 |
12 | #todo-list li.done label {
13 | color: #777777;
14 | text-decoration: line-through;
15 | }
16 |
17 | #todo-list .destroy {
18 | position: absolute;
19 | right: 5px;
20 | top: 20px;
21 | display: none;
22 | cursor: pointer;
23 | width: 20px;
24 | height: 20px;
25 | background: url(img/destroy.png) no-repeat;
26 | text-indent: -9999px;
27 | }
28 |
29 | #todo-list li:hover .destroy {
30 | display: block;
31 | }
32 |
33 | #todo-list .destroy:hover {
34 | background-position: 0 -20px;
35 | }
36 |
37 | #todo-list li.editing {
38 | border-bottom: none;
39 | margin-top: -1px;
40 | padding: 0;
41 | }
42 |
43 | #todo-list li.editing:last-child {
44 | margin-bottom: -1px;
45 | }
46 |
47 | #todo-list li .view label {
48 | word-break: break-word;
49 | }
50 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --text-secondary: #777777;
3 | }
4 |
5 | html,
6 | body {
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | body {
12 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
13 | line-height: 1.4em;
14 | background: #eeeeee;
15 | color: #333333;
16 | width: 520px;
17 | margin: 0 auto;
18 | -webkit-font-smoothing: antialiased;
19 | }
20 |
21 | #todoapp {
22 | background: #fff;
23 | padding: 20px;
24 | margin-bottom: 40px;
25 | box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
26 | border-radius: 0 0 5px 5px;
27 | }
28 |
29 | #todoapp h1 {
30 | font-size: 36px;
31 | font-weight: bold;
32 | text-align: center;
33 | padding: 0 0 10px 0;
34 | }
35 |
36 | #instructions {
37 | margin: 10px auto;
38 | color: var(--text-secondary);
39 | text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
40 | text-align: center;
41 | }
42 |
43 | #credits {
44 | margin: 30px auto;
45 | color: var(--text-secondary);
46 | text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
47 | text-align: center;
48 | }
49 |
50 | #credits a {
51 | color: var(--text-secondary);
52 | }
53 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Ross Allen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "react-todos",
4 | "version": "2.0.0",
5 | "description": "Backbone's example TODO app with React for views",
6 | "main": "todos.js",
7 | "directories": {
8 | "doc": "docs"
9 | },
10 | "scripts": {
11 | "build": "react-scripts build",
12 | "deploy": "gh-pages -d build",
13 | "eject": "react-scripts eject",
14 | "flow": "flow",
15 | "predeploy": "npm run build",
16 | "start": "react-scripts start",
17 | "test": "react-scripts test --env=jsdom"
18 | },
19 | "repository": "ssorallen/react-todos",
20 | "keywords": [
21 | "backbone",
22 | "reactjs",
23 | "todos"
24 | ],
25 | "author": "Ross Allen ",
26 | "license": "Apache",
27 | "bugs": {
28 | "url": "https://github.com/ssorallen/react-todos/issues"
29 | },
30 | "homepage": "http://www.ssorallen.com/react-todos/",
31 | "dependencies": {
32 | "react": "^16.8.6",
33 | "react-dom": "^16.8.6"
34 | },
35 | "devDependencies": {
36 | "flow-bin": "^0.95.1",
37 | "gh-pages": "^1.0.0",
38 | "prettier": "^1.12.1",
39 | "react-scripts": "^1.0.14"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Footer.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import './Footer.css';
4 | import * as React from 'react';
5 |
6 | // Footer Component
7 | // ----------------
8 |
9 | type Props = {
10 | clearCompletedItems: (event: SyntheticMouseEvent) => void,
11 | itemsDoneCount: number,
12 | itemsRemainingCount: number,
13 | };
14 |
15 | // The footer shows the total number of todos and how many are completed.
16 | export default function Footer(props: Props) {
17 | let clearCompletedButton;
18 |
19 | // Show the "Clear X completed items" button only if there are completed
20 | // items.
21 | if (props.itemsDoneCount > 0) {
22 | clearCompletedButton = (
23 |
24 | Clear {props.itemsDoneCount} completed
25 | {1 === props.itemsDoneCount ? ' item' : ' items'}
26 |
27 | );
28 | }
29 |
30 | // Clicking the "Clear X completed items" button calls the
31 | // "clearCompletedItems" function passed in on `props`.
32 | return (
33 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/TodoList.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import './TodoList.css';
4 | import * as React from 'react';
5 | import type { Todo } from './Types';
6 | import TodoListItem from './TodoListItem';
7 |
8 | // Todo List Component
9 | // -------------------
10 |
11 | type Props = {
12 | onDestroyTodo: (Todo: Todo) => void,
13 | onSetTodoTitle: (Todo: Todo, title: string) => void,
14 | onToggleTodoCompleted: (Todo: Todo, done: boolean) => void,
15 | todos: Array,
16 | };
17 |
18 | export default function TodoList(props: Props) {
19 | const [editingTodo, setEditingTodo] = React.useState(null);
20 |
21 | return (
22 |
23 | {props.todos.map((todo, index) => (
24 | {
28 | props.onDestroyTodo(todo);
29 | }}
30 | onSetTitle={title => {
31 | props.onSetTodoTitle(todo, title);
32 | }}
33 | onStartEditing={() => {
34 | setEditingTodo(todo);
35 | }}
36 | onStopEditing={() => {
37 | if (todo === editingTodo) {
38 | setEditingTodo(null);
39 | }
40 | }}
41 | onToggleCompleted={() => {
42 | props.onToggleTodoCompleted(todo, !todo.done);
43 | }}
44 | todo={todo}
45 | />
46 | ))}
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/Body.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import './Body.css';
4 | import * as React from 'react';
5 | import Footer from './Footer';
6 | import type { Todo } from './Types';
7 | import TodoList from './TodoList';
8 |
9 | // Main Component
10 | // --------------
11 |
12 | interface Props {
13 | onClearCompletedTodos: () => void;
14 | onDestroyTodo: (Todo: Todo) => void;
15 | onSetTodoTitle: (Todo: Todo, title: string) => void;
16 | onToggleTodoCompleted: (Todo: Todo, done: boolean) => void;
17 | onToggleAllTodosCompleted: (toggle: boolean) => void;
18 | todos: Array;
19 | }
20 |
21 | // The main component contains the list of todos and the footer.
22 | export default function Body(props: Props) {
23 | // Tell the **App** to toggle the *done* state of all **Todo** items.
24 | const toggleAllTodosCompleted = (event: SyntheticInputEvent) => {
25 | props.onToggleAllTodosCompleted(event.target.checked);
26 | };
27 |
28 | return (
29 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/TodoListItem.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import './TodoListItem.css';
4 | import * as React from 'react';
5 | import type { Todo } from './Types';
6 |
7 | // Todo List Item Component
8 | // ------------------------
9 |
10 | type Props = {
11 | editing: boolean,
12 | onDestroy: () => void,
13 | onSetTitle: (title: string) => void,
14 | onStartEditing: () => void,
15 | onStopEditing: () => void,
16 | onToggleCompleted: (done: boolean) => void,
17 | todo: Todo,
18 | };
19 |
20 | export default function TodoListItem(props: Props) {
21 | const inputEl = React.createRef();
22 |
23 | function setTitleAndStopEditing() {
24 | if (inputEl.current != null) props.onSetTitle(inputEl.current.value);
25 | props.onStopEditing();
26 | }
27 |
28 | // // Stop editing if the input gets an "Enter" keypress.
29 | function handleEditKeyPress(event: SyntheticKeyboardEvent) {
30 | if (event.key === 'Enter') {
31 | setTitleAndStopEditing();
32 | }
33 | }
34 |
35 | return (
36 |
37 | {props.editing ? (
38 |
47 | ) : (
48 |
60 | )}
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | import './App.css';
4 | import * as React from 'react';
5 | import Body from './Body';
6 | import type { Todo } from './Types';
7 |
8 | type State = {
9 | todos: Array,
10 | };
11 |
12 | const initialState = {
13 | todos: [],
14 | };
15 |
16 | function reducer(state: State, action) {
17 | switch (action.type) {
18 | case 'clear-completed-todos':
19 | return {
20 | ...state,
21 | todos: state.todos.filter(t => !t.done),
22 | };
23 | case 'create-todo':
24 | return {
25 | ...state,
26 | todos: [
27 | ...state.todos,
28 | {
29 | done: false,
30 | title: action.title,
31 | },
32 | ],
33 | };
34 | case 'destroy-todo': {
35 | const nextTodos = state.todos.slice();
36 | nextTodos.splice(nextTodos.indexOf(action.todo), 1);
37 | return {
38 | ...state,
39 | todos: nextTodos,
40 | };
41 | }
42 | case 'set-todo-title': {
43 | const nextTodos = state.todos.slice();
44 | nextTodos.splice(nextTodos.indexOf(action.todo), 1, { ...action.todo, title: action.title });
45 | return {
46 | ...state,
47 | todos: nextTodos,
48 | };
49 | }
50 | case 'toggle-todo-completed': {
51 | const nextTodos = state.todos.slice();
52 | nextTodos.splice(nextTodos.indexOf(action.todo), 1, { ...action.todo, done: action.done });
53 | return {
54 | ...state,
55 | todos: nextTodos,
56 | };
57 | }
58 | case 'toggle-all-todos-completed':
59 | return {
60 | ...state,
61 | todos: state.todos.map(t => ({ ...t, done: action.done })),
62 | };
63 | default:
64 | return state;
65 | }
66 | }
67 |
68 | export default function App() {
69 | // Store the app's todos in local storage
70 | const [state, dispatch] = React.useReducer(
71 | reducer,
72 | JSON.parse(window.localStorage.getItem('react-todos')) || initialState
73 | );
74 | React.useEffect(
75 | () => {
76 | window.localStorage.setItem('react-todos', JSON.stringify(state));
77 | },
78 | [state]
79 | );
80 | const formEl = React.useRef(null);
81 |
82 | // If "Enter" is pressed in the main input field, it will submit the form.
83 | // Create a new **Todo** and reset the title.
84 | const handleTitleFormSubmit = (event: SyntheticEvent) => {
85 | event.preventDefault();
86 | const titleEl = event.currentTarget.elements['title'];
87 | if (!(titleEl instanceof HTMLInputElement)) return;
88 | const title = titleEl.value;
89 | if (title === '') return;
90 | dispatch({ title, type: 'create-todo' });
91 | if (formEl.current != null) formEl.current.reset();
92 | };
93 |
94 | return (
95 |
96 |
102 | {/* Don't display the "Mark all as complete" button and the footer if there
103 | are no **Todo** items. */}
104 | {state.todos.length === 0 ? null : (
105 | {
107 | dispatch({ type: 'clear-completed-todos' });
108 | }}
109 | onDestroyTodo={todo => {
110 | dispatch({ todo, type: 'destroy-todo' });
111 | }}
112 | onSetTodoTitle={(todo, title) => {
113 | dispatch({ title, todo, type: 'set-todo-title' });
114 | }}
115 | onToggleTodoCompleted={(todo, done) => {
116 | dispatch({
117 | done,
118 | todo,
119 | type: 'toggle-todo-completed',
120 | });
121 | }}
122 | onToggleAllTodosCompleted={done => {
123 | dispatch({ done, type: 'toggle-all-todos-completed' });
124 | }}
125 | todos={state.todos}
126 | />
127 | )}
128 |
129 | );
130 | }
131 |
--------------------------------------------------------------------------------