├── .editorconfig
├── .env.development
├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── manifest.json
└── robots.txt
├── src
├── components
│ ├── todo-form.tsx
│ ├── todo-item.tsx
│ └── todo-list.tsx
├── index.tsx
├── interfaces.ts
├── react-app-env.d.ts
└── styles
│ └── styles.css
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = crlf
5 | charset = utf-8
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | BROWSER=none
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Development folders and files #
2 | #################################
3 | builds/
4 | dist/
5 | node_modules/
6 |
7 | # Folder config file #
8 | ######################
9 | Desktop.ini
10 |
11 | # Folders for inspiration & notes #
12 | ###################################
13 | __ignore__/
14 |
15 | # Log files & folders #
16 | #######################
17 | logs/
18 | *.log
19 | npm-debug.log*
20 | .npm
21 |
22 | # Packages #
23 | ############
24 | # it's better to unpack these files and commit the raw source
25 | # git has its own built in compression methods
26 | *.7z
27 | *.dmg
28 | *.gz
29 | *.iso
30 | *.jar
31 | *.rar
32 | *.tar
33 | *.zip
34 |
35 | # Photoshop & Illustrator files #
36 | #################################
37 | *.ai
38 | *.eps
39 | *.psd
40 |
41 | # Windows & Mac file caches #
42 | #############################
43 | .DS_Store
44 | Thumbs.db
45 | thumbs.db
46 |
47 | # Windows shortcuts #
48 | #####################
49 | *.lnk
50 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Alex Devero (alexdevero.com)
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to React Todo list app (TS) 👋
2 |
3 | 
4 | 
5 | 
6 | 
7 |
8 | [](https://github.com/alexdevero/react-hooks-todo-list-app-ts#readme)
9 | [](https://github.com/alexdevero/react-hooks-todo-list-app-ts/graphs/commit-activity)
10 | [](https://github.com/alexdevero/react-hooks-todo-list-app-ts/blob/master/LICENSE)
11 | [](https://twitter.com/alexdevero)
12 |
13 |
14 |
15 |
16 |
17 |
18 | > Simple Todo list app built with React hooks and TypeScript.
19 |
20 | ### 🏠 [Homepage](https://github.com/alexdevero/react-hooks-todo-list-app-ts#readme)
21 |
22 | ## Prerequisites
23 |
24 | - node >=11.0.0
25 | - npm >=6.0
26 | - yarn >=1.16.0
27 |
28 | ## Install
29 |
30 | ```sh
31 | npm install
32 | ```
33 |
34 | ## Usage
35 |
36 | ```sh
37 | npm run start
38 | ```
39 |
40 | ## Run tests
41 |
42 | ```sh
43 | npm run test
44 | ```
45 |
46 | ## Author
47 |
48 | 👤 **Alex Devero**
49 |
50 | * Website: https://www.alexdevero.com
51 | * Twitter: [@alexdevero](https://twitter.com/alexdevero)
52 | * Github: [@alexdevero](https://github.com/alexdevero)
53 |
54 | ## 🤝 Contributing
55 |
56 | Contributions, issues and feature requests are welcome!
57 |
58 | Feel free to check [issues page](https://github.com/alexdevero/react-hooks-todo-list-app-ts/issues).
59 |
60 | ## Show your support
61 |
62 | Give a ⭐️ if this project helped you!
63 |
64 | [](https://www.patreon.com/alexdevero)
65 |
66 |
67 | ## 📝 License
68 |
69 | Copyright © 2019 [Alex Devero](https://github.com/alexdevero).
70 |
71 | This project is [MIT](https://github.com/alexdevero/react-hooks-todo-list-app-ts/blob/master/LICENSE) licensed.
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-todo-list-hooks-ts",
3 | "version": "1.0.0",
4 | "description": "Simple Todo list app built with React hooks and TypeScript.",
5 | "license": "MIT",
6 | "private": false,
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/alexdevero/react-hooks-todo-list-app-ts.git"
10 | },
11 | "homepage": "https://github.com/alexdevero/react-hooks-todo-list-app-ts#readme",
12 | "bugs": {
13 | "url": "https://github.com/alexdevero/react-hooks-todo-list-app-ts/issues"
14 | },
15 | "author": {
16 | "name": "Alex Devero",
17 | "url": "https://alexdevero.com/"
18 | },
19 | "keywords": [
20 | "app",
21 | "react",
22 | "reactjs",
23 | "todo",
24 | "typescript"
25 | ],
26 | "browserslist": [
27 | ">0.2%",
28 | "not dead",
29 | "not ie <= 11",
30 | "not op_mini all"
31 | ],
32 | "engines": {
33 | "node": ">=11.0.0",
34 | "npm": ">=6.0",
35 | "yarn": ">=1.16.0"
36 | },
37 | "main": "src/index.tsx",
38 | "scripts": {
39 | "start": "react-scripts start",
40 | "build": "react-scripts build",
41 | "test": "react-scripts test --env=jsdom",
42 | "eject": "react-scripts eject"
43 | },
44 | "dependencies": {
45 | "react": "16.11.0",
46 | "react-dom": "16.11.0",
47 | "shortid": "2.2.15"
48 | },
49 | "devDependencies": {
50 | "@types/react": "16.9.11",
51 | "@types/react-dom": "16.9.4",
52 | "@types/shortid": "^0.0.29",
53 | "react-scripts": "3.2.0",
54 | "typescript": "3.7.2"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdevero/react-hooks-todo-list-app-ts/4ba635eca4a5ecb01e6ea686c5adb520f2bc571a/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React Todo List
23 |
24 |
25 |
26 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-todo-list-hooks-ts",
3 | "name": "Simple Todo list app built with React hooks and TypeScript.",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow: /static/
4 |
--------------------------------------------------------------------------------
/src/components/todo-form.tsx:
--------------------------------------------------------------------------------
1 | // Import dependencies
2 | import * as React from 'react'
3 | import shortid from 'shortid'
4 |
5 | // Import interfaces
6 | import {TodoInterface, TodoFormInterface} from './../interfaces'
7 |
8 | // Todo form component
9 | const TodoForm = (props: TodoFormInterface) => {
10 | // Create ref for form input
11 | const inputRef = React.useRef(null)
12 |
13 | // Create new form state
14 | const [formState, setFormState] = React.useState('')
15 |
16 | // Handle todo input change
17 | function handleInputChange(event: React.ChangeEvent) {
18 | // Update form state with the text from input
19 | setFormState(event.target.value)
20 | }
21 |
22 | // Handle 'Enter' in todo input
23 | function handleInputEnter(event: React.KeyboardEvent) {
24 | // Check for 'Enter' key
25 | if (event.key === 'Enter') {
26 | // Prepare new todo object
27 | const newTodo: TodoInterface = {
28 | id: shortid.generate(),
29 | text: formState,
30 | isCompleted: false
31 | }
32 |
33 | // Create new todo item
34 | props.handleTodoCreate(newTodo)
35 |
36 | // Reset the input field
37 | if (inputRef && inputRef.current) {
38 | inputRef.current.value = ''
39 | }
40 | }
41 | }
42 |
43 | return (
44 |
45 | handleInputChange(event)}
50 | onKeyPress={event => handleInputEnter(event)}
51 | />
52 |
53 | )
54 | }
55 |
56 | export default TodoForm
57 |
--------------------------------------------------------------------------------
/src/components/todo-item.tsx:
--------------------------------------------------------------------------------
1 | // Import dependencies
2 | import * as React from 'react'
3 |
4 | // Import interfaces
5 | import { TodoItemInterface } from './../interfaces'
6 |
7 | // TodoItem component
8 | const TodoItem = (props: TodoItemInterface) => {
9 | return (
10 |
11 |
props.handleTodoComplete(props.todo.id)}>
12 | {props.todo.isCompleted ? (
13 | ✔
14 | ) : (
15 |
16 | )}
17 |
18 |
19 |
20 | ) => props.handleTodoUpdate(event, props.todo.id)}
24 | />
25 |
26 |
27 |
props.handleTodoRemove(props.todo.id)}>
28 | ⨯
29 |
30 |
31 | )
32 | }
33 |
34 | export default TodoItem
35 |
--------------------------------------------------------------------------------
/src/components/todo-list.tsx:
--------------------------------------------------------------------------------
1 | // Import dependencies
2 | import * as React from 'react'
3 |
4 | // Import TodoItem
5 | import TodoItem from './todo-item'
6 |
7 | // Import interfaces
8 | import { TodoListInterface } from './../interfaces'
9 |
10 | // TodoList component
11 | const TodoList = (props: TodoListInterface) => {
12 | return (
13 |
14 |
15 | {props.todos.map((todo) => (
16 | -
17 |
24 |
25 | ))}
26 |
27 |
28 | )
29 | }
30 |
31 | export default TodoList
32 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | // Import dependencies
2 | import * as React from 'react'
3 | import { render } from 'react-dom'
4 |
5 | // Import components
6 | import TodoForm from './components/todo-form'
7 | import TodoList from './components/todo-list'
8 |
9 | // Import interfaces
10 | import { TodoInterface } from './interfaces'
11 |
12 | // Import styles
13 | import './styles/styles.css'
14 |
15 | // TodoListApp component
16 | const TodoListApp = () => {
17 | const [todos, setTodos] = React.useState([])
18 |
19 | // Creating new todo item
20 | function handleTodoCreate(todo: TodoInterface) {
21 | // Prepare new todos state
22 | const newTodosState: TodoInterface[] = [...todos]
23 |
24 | // Update new todos state
25 | newTodosState.push(todo)
26 |
27 | // Update todos state
28 | setTodos(newTodosState)
29 | }
30 |
31 | // Update existing todo item
32 | function handleTodoUpdate(event: React.ChangeEvent, id: string) {
33 | // Prepare new todos state
34 | const newTodosState: TodoInterface[] = [...todos]
35 |
36 | // Find correct todo item to update
37 | newTodosState.find((todo: TodoInterface) => todo.id === id)!.text = event.target.value
38 |
39 | // Update todos state
40 | setTodos(newTodosState)
41 | }
42 |
43 | // Remove existing todo item
44 | function handleTodoRemove(id: string) {
45 | // Prepare new todos state
46 | const newTodosState: TodoInterface[] = todos.filter((todo: TodoInterface) => todo.id !== id)
47 |
48 | // Update todos state
49 | setTodos(newTodosState)
50 | }
51 |
52 | // Check existing todo item as completed
53 | function handleTodoComplete(id: string) {
54 | // Copy current todos state
55 | const newTodosState: TodoInterface[] = [...todos]
56 |
57 | // Find the correct todo item and update its 'isCompleted' key
58 | newTodosState.find((todo: TodoInterface) => todo.id === id)!.isCompleted = !newTodosState.find((todo: TodoInterface) => todo.id === id)!.isCompleted
59 |
60 | // Update todos state
61 | setTodos(newTodosState)
62 | }
63 |
64 | // Check if todo item has title
65 | function handleTodoBlur(event: React.ChangeEvent) {
66 | if (event.target.value.length === 0) {
67 | event.target.classList.add('todo-input-error')
68 | } else {
69 | event.target.classList.remove('todo-input-error')
70 | }
71 | }
72 |
73 | return (
74 |
75 |
79 |
80 |
87 |
88 | )
89 | }
90 |
91 | const rootElement = document.getElementById('root')
92 | render(, rootElement)
93 |
--------------------------------------------------------------------------------
/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | // Todo interface
2 | export interface TodoInterface {
3 | id: string;
4 | text: string;
5 | isCompleted: boolean;
6 | }
7 |
8 | // Todo form interface
9 | export interface TodoFormInterface {
10 | todos: TodoInterface[];
11 | handleTodoCreate: (todo: TodoInterface) => void;
12 | }
13 |
14 | // Todo list interface
15 | export interface TodoListInterface {
16 | handleTodoUpdate: (event: React.ChangeEvent, id: string) => void;
17 | handleTodoRemove: (id: string) => void;
18 | handleTodoComplete: (id: string) => void;
19 | handleTodoBlur: (event: React.ChangeEvent) => void;
20 | todos: TodoInterface[];
21 | }
22 |
23 | // Todo item interface
24 | export interface TodoItemInterface {
25 | handleTodoUpdate: (event: React.ChangeEvent, id: string) => void;
26 | handleTodoRemove: (id: string) => void;
27 | handleTodoComplete: (id: string) => void;
28 | handleTodoBlur: (event: React.ChangeEvent) => void;
29 | todo: TodoInterface;
30 | }
31 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/styles/styles.css:
--------------------------------------------------------------------------------
1 | /* Default styles*/
2 | html {
3 | box-sizing: border-box;
4 | }
5 |
6 | *,
7 | *::before,
8 | *::after {
9 | box-sizing: inherit;
10 | }
11 |
12 | #root,
13 | body {
14 | min-height: 100vh;
15 | }
16 |
17 | body {
18 | margin: 0;
19 | }
20 |
21 | #root,
22 | .todo-list-app {
23 | display: flex;
24 | flex-flow: column nowrap;
25 | }
26 |
27 | #root {
28 | align-items: center;
29 | width: 100%;
30 | }
31 |
32 | /* Todo list app styles */
33 | .todo-list-app {
34 | padding-top: 32px;
35 | width: 100%;
36 | max-width: 480px;
37 | }
38 |
39 | /* Todo form styles */
40 | .todo-form input,
41 | .todo-item {
42 | border: 1px solid #ececec;
43 | }
44 |
45 | .todo-form input {
46 | padding: 0 14px;
47 | width: 100%;
48 | height: 48px;
49 | transition: .25s border ease-in-out;
50 | }
51 |
52 | .todo-form input:focus {
53 | outline: 0;
54 | border: 1px solid #3498db;
55 | }
56 |
57 | /* Todo list styles */
58 | .todo-list ul {
59 | padding: 0;
60 | margin: 0;
61 | }
62 |
63 | .todo-list li {
64 | list-style-type: none;
65 | }
66 |
67 | /* Todo item styles */
68 | .todo-item {
69 | display: flex;
70 | flex-flow: row nowrap;
71 | align-items: center;
72 | padding: 8px;
73 | }
74 |
75 | .todo-form + .todo-list ul .todo-item {
76 | border-top: 0;
77 | }
78 |
79 | .todo-item-input-wrapper {
80 | flex-grow: 1;
81 | padding: 0 16px;
82 | }
83 |
84 | .todo-item input {
85 | width: 100%;
86 | border: 0;
87 | border-bottom: 1px solid transparent;
88 | transition: .25s border-bottom ease-in-out;
89 | }
90 |
91 | .todo-item input:focus {
92 | outline: 0;
93 | border-bottom: 1px solid #3498db;
94 | }
95 |
96 | .todo-item .todo-input-error {
97 | border-bottom: 1px solid #e74c3c;
98 | }
99 |
100 | .todo-item span {
101 | display: flex;
102 | align-items: center;
103 | justify-content: center;
104 | width: 32px;
105 | height: 32px;
106 | border-radius: 50%;
107 | border: 1px solid #ececec;
108 | transition: .25s all ease-in-out;
109 | }
110 |
111 | .todo-item-unchecked:hover {
112 | background: hsla(168, 76%, 42%, .25);
113 | border: 1px solid hsl(168, 76%, 42%, .25);
114 | }
115 |
116 | .todo-item-checked {
117 | color: #fff;
118 | background: #1abc9c;
119 | border: 1px solid #1abc9c;
120 | }
121 |
122 | .item-remove {
123 | display: flex;
124 | padding-left: 8px;
125 | padding-right: 8px;
126 | font-size: 28px;
127 | cursor: pointer;
128 | line-height: 1;
129 | color: #ececec;
130 | transition: .25s color ease-in-out;
131 | }
132 |
133 | .item-remove:hover {
134 | color: #111;
135 | }
136 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "./src/*"
4 | ],
5 | "compilerOptions": {
6 | "lib": [
7 | "dom",
8 | "es2015"
9 | ],
10 | "jsx": "react",
11 | "target": "es5",
12 | "allowJs": true,
13 | "skipLibCheck": true,
14 | "esModuleInterop": true,
15 | "allowSyntheticDefaultImports": true,
16 | "strict": true,
17 | "forceConsistentCasingInFileNames": true,
18 | "module": "esnext",
19 | "moduleResolution": "node",
20 | "resolveJsonModule": true,
21 | "isolatedModules": true,
22 | "noEmit": true
23 | }
24 | }
25 |
--------------------------------------------------------------------------------