├── .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 | ![Version](https://img.shields.io/badge/version-1.0.0-blue.svg?cacheSeconds=2592000) 4 | ![Prerequisite](https://img.shields.io/badge/node-%3E%3D11.0.0-blue.svg) 5 | ![Prerequisite](https://img.shields.io/badge/npm-%3E%3D6.0-blue.svg) 6 | ![Prerequisite](https://img.shields.io/badge/yarn-%3E%3D1.16.0-blue.svg) 7 | 8 | [![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://github.com/alexdevero/react-hooks-todo-list-app-ts#readme) 9 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/alexdevero/react-hooks-todo-list-app-ts/graphs/commit-activity) 10 | [![License: MIT](https://img.shields.io/github/license/alexdevero/react-hooks-todo-list-app-ts)](https://github.com/alexdevero/react-hooks-todo-list-app-ts/blob/master/LICENSE) 11 | [![Twitter: alexdevero](https://img.shields.io/twitter/follow/alexdevero.svg?style=social)](https://twitter.com/alexdevero) 12 | 13 | Paypal 14 | Patreon 15 | buymeacoffee 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 | [![support us](https://img.shields.io/badge/become-a patreon%20us-orange.svg?cacheSeconds=2592000)](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 | --------------------------------------------------------------------------------