├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── robots.txt ├── readme ├── example.png └── lighthouse.png └── src ├── App.js ├── components ├── AddTask.js ├── DeleteTask.js ├── UpdateTask.js └── tasks.js ├── images └── empty.svg └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # dependencies 3 | /node_modules 4 | /.pnp 5 | .pnp.js 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | package-lock.json 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Todo List 2 | A simple to-do list app powered by React, Chakra UI, React icons. 3 | Save in Local Storage 4 | 5 | ##### [Live Demo](https://todo-app.erfjs.com/) on Vercel 6 | 7 | 8 | 9 | 10 |

11 | 12 |

13 | 14 |

Lighthouse Speed

15 |

16 | 17 |

18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-todo-list", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chakra-ui/react": "^1.8.5", 7 | "@emotion/react": "^11.8.1", 8 | "@emotion/styled": "^11.8.1", 9 | "@testing-library/jest-dom": "^5.16.2", 10 | "@testing-library/react": "^12.1.3", 11 | "@testing-library/user-event": "^13.5.0", 12 | "framer-motion": "^6.2.8", 13 | "nanoid": "^3.3.1", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2", 16 | "react-icons": "^4.3.1", 17 | "react-scripts": "5.0.0", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfjs/Todo-list/eb0e9f74a7b2fc43e04e7422bc1ab0c747d674c5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo list 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /readme/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfjs/Todo-list/eb0e9f74a7b2fc43e04e7422bc1ab0c747d674c5/readme/example.png -------------------------------------------------------------------------------- /readme/lighthouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erfjs/Todo-list/eb0e9f74a7b2fc43e04e7422bc1ab0c747d674c5/readme/lighthouse.png -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { 2 | Heading, 3 | IconButton, 4 | VStack, 5 | useColorMode, 6 | useToast, 7 | } from "@chakra-ui/react"; 8 | import TaskList from "./components/tasks"; 9 | import AddTask from "./components/AddTask"; 10 | import { FaSun, FaMoon } from "react-icons/fa"; 11 | import { useState, useEffect } from "react"; 12 | 13 | function App() { 14 | const toast = useToast(); 15 | const [tasks, setTasks] = useState( 16 | () => JSON.parse(localStorage.getItem("tasks")) || [] 17 | ); 18 | 19 | useEffect(() => { 20 | localStorage.setItem("tasks", JSON.stringify(tasks)); 21 | }, [tasks]); 22 | 23 | function deleteTask(id) { 24 | const newTasks = tasks.filter((task) => { 25 | return task.id !== id; 26 | }); 27 | setTasks(newTasks); 28 | } 29 | 30 | function deleteTaskAll() { 31 | setTasks([]); 32 | } 33 | 34 | function checkTask(id) { 35 | const newTasksCheck = tasks.map((task, index, array) => { 36 | if (task.id === id) { 37 | task.check = !task.check; 38 | } 39 | return task; 40 | }); 41 | setTasks(newTasksCheck); 42 | } 43 | 44 | function updateTask(id, body, onClose) { 45 | const info = body.trim(); 46 | 47 | if (!info) { 48 | toast({ 49 | title: "Enter your task", 50 | position: "top", 51 | status: "warning", 52 | duration: 2000, 53 | isClosable: true, 54 | }); 55 | 56 | return; 57 | } 58 | 59 | const newTasksUpdate = tasks.map((task, index, array) => { 60 | if (task.id === id) { 61 | task.body = body; 62 | task.check = false; 63 | } 64 | return task; 65 | }); 66 | 67 | setTasks(newTasksUpdate); 68 | 69 | onClose(); 70 | } 71 | 72 | function addTask(task) { 73 | setTasks([...tasks, task]); 74 | } 75 | 76 | const { colorMode, toggleColorMode } = useColorMode(); 77 | 78 | return ( 79 | 80 | : } 82 | isRound='true' 83 | size='md' 84 | alignSelf='flex-end' 85 | onClick={toggleColorMode} 86 | aria-label='toogle-dark-mode' 87 | /> 88 | 89 | 96 | Todo list 97 | 98 | 99 | 106 | 107 | ); 108 | } 109 | 110 | export default App; 111 | -------------------------------------------------------------------------------- /src/components/AddTask.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Button, HStack, Input, useToast } from "@chakra-ui/react"; 3 | import { nanoid } from "nanoid"; 4 | 5 | function AddTask({ addTask }) { 6 | const toast = useToast(); 7 | const [content, setContent] = useState(""); 8 | const [statusInput, setStatusInput] = useState(true); 9 | 10 | function handleSubmit(e) { 11 | e.preventDefault(); 12 | 13 | const taskText = content.trim(); 14 | 15 | if (!taskText) { 16 | toast({ 17 | title: "Enter your task", 18 | position: "top", 19 | status: "warning", 20 | duration: 2000, 21 | isClosable: true, 22 | }); 23 | setStatusInput(false); 24 | 25 | return setContent(""); 26 | } 27 | 28 | const task = { 29 | id: nanoid(), 30 | body: taskText, 31 | check: false, 32 | }; 33 | 34 | addTask(task); 35 | setContent(""); 36 | } 37 | 38 | if (content && !statusInput) { 39 | setStatusInput(true); 40 | } 41 | 42 | return ( 43 |
44 | 45 | setContent(e.target.value)} 52 | /> 53 | 63 | 64 |
65 | ); 66 | } 67 | 68 | export default AddTask; 69 | -------------------------------------------------------------------------------- /src/components/DeleteTask.js: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalOverlay, 4 | ModalContent, 5 | ModalHeader, 6 | ModalFooter, 7 | ModalBody, 8 | Button, 9 | Text, 10 | useDisclosure, 11 | IconButton, 12 | } from "@chakra-ui/react"; 13 | import React from "react"; 14 | import { FiTrash2 } from "react-icons/fi"; 15 | 16 | function DeleteAllTask({ deleteTaskAll }) { 17 | const { isOpen, onOpen, onClose } = useDisclosure(); 18 | return ( 19 | <> 20 | 30 | 31 | 32 | 33 | 34 | Do you really want to delete all tasks? 35 | 36 | 39 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | function DeleteTask({ task, deleteTask }) { 50 | const { isOpen, onOpen, onClose } = useDisclosure(); 51 | 52 | return ( 53 | <> 54 | } isRound='true' onClick={onOpen} /> 55 | 56 | 57 | 58 | 59 | Do you really want to delete the task? 60 | 61 | {task.body} 62 | 63 | 64 | 67 | 73 | 74 | 75 | 76 | 77 | ); 78 | } 79 | 80 | export { DeleteTask, DeleteAllTask }; 81 | -------------------------------------------------------------------------------- /src/components/UpdateTask.js: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalOverlay, 4 | ModalContent, 5 | ModalHeader, 6 | ModalFooter, 7 | ModalBody, 8 | ModalCloseButton, 9 | Button, 10 | Input, 11 | FormControl, 12 | useDisclosure, 13 | IconButton, 14 | } from "@chakra-ui/react"; 15 | import { useState } from "react"; 16 | import React from "react"; 17 | import { FiEdit } from "react-icons/fi"; 18 | 19 | function UpdateTask({ task, updateTask }) { 20 | const { isOpen, onOpen, onClose } = useDisclosure(); 21 | const [body, setBody] = useState(""); 22 | 23 | const initialRef = React.useRef(); 24 | 25 | return ( 26 | <> 27 | } isRound='true' onClick={onOpen} /> 28 | 34 | 35 | 36 | Update your task 37 | 38 | 39 | 40 | setBody(e.target.value)} 45 | onFocus={(e) => setBody(e.target.value)} 46 | /> 47 | 48 | 49 | 50 | 51 | 54 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default UpdateTask; 68 | -------------------------------------------------------------------------------- /src/components/tasks.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import UpdateTask from "./UpdateTask"; 3 | import { DeleteTask, DeleteAllTask } from "./DeleteTask"; 4 | import { 5 | HStack, 6 | Box, 7 | VStack, 8 | Flex, 9 | Text, 10 | StackDivider, 11 | } from "@chakra-ui/react"; 12 | import { Image } from "@chakra-ui/react"; 13 | import img from "../images/empty.svg"; 14 | 15 | function TaskList({ tasks, updateTask, deleteTask, deleteTaskAll, checkTask }) { 16 | if (!tasks.length) { 17 | return ( 18 | <> 19 | 20 | Your list is empty 27 | 28 | 29 | ); 30 | } 31 | return ( 32 | <> 33 | } 35 | borderColor='gray.100' 36 | borderWidth='2px' 37 | p='5' 38 | borderRadius='lg' 39 | w='100%' 40 | maxW={{ base: "90vw", sm: "80vw", lg: "50vw", xl: "30vw" }} 41 | alignItems='stretch' 42 | > 43 | {tasks.map((task) => ( 44 | 45 | checkTask(task.id)} 52 | > 53 | {task.body} 54 | 55 | 60 | 61 | 62 | ))} 63 | 64 | 65 | 66 | 67 | 68 | 69 | ); 70 | } 71 | 72 | export default TaskList; 73 | -------------------------------------------------------------------------------- /src/images/empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 39 | 52 | 53 | 54 | 55 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 69 | 70 | 71 | 73 | 74 | 76 | 78 | 80 | 81 | 83 | 84 | 86 | 88 | 90 | 91 | 92 | 93 | 94 | 95 | 97 | 98 | 100 | 102 | 104 | 105 | 107 | 108 | 110 | 112 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 124 | 125 | 126 | 127 | 128 | 129 | 131 | 132 | 133 | 134 | 135 | 137 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 147 | 149 | 150 | 151 | 153 | 154 | 155 | 156 | 157 | 159 | 160 | 161 | 162 | 164 | 165 | 166 | 168 | 169 | 170 | 172 | 173 | 174 | 175 | 176 | 178 | 179 | 180 | 182 | 183 | 184 | 186 | 187 | 188 | 189 | 190 | 192 | 193 | 194 | 196 | 197 | 198 | 200 | 201 | 202 | 203 | 204 | 206 | 207 | 208 | 210 | 211 | 212 | 214 | 215 | 216 | 217 | 218 | 220 | 221 | 222 | 224 | 225 | 226 | 228 | 229 | 230 | 231 | 234 | 235 | 236 | 237 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 252 | 253 | 254 | 255 | 256 | 257 | 260 | 265 | 267 | 269 | 272 | 273 | 274 | 276 | 277 | 278 | 279 | 280 | 281 | 285 | 286 | 287 | 288 | 289 | 290 | 293 | 295 | 297 | 299 | 301 | 303 | 306 | 308 | 310 | 312 | 314 | 316 | 317 | 318 | 320 | 321 | 322 | 323 | 324 | 325 | 327 | 329 | 330 | 331 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 342 | 344 | 345 | 346 | 347 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import { ChakraProvider, ColorModeScript, extendTheme } from "@chakra-ui/react"; 5 | 6 | const config = { 7 | initialColorMode: "dark", 8 | }; 9 | 10 | const theme = extendTheme({ 11 | config, 12 | }); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById("root") 22 | ); 23 | --------------------------------------------------------------------------------