├── todo.jpg ├── src ├── todo-image.png ├── index.js ├── Alert.js ├── ToDo.js ├── App.js └── App.css ├── .gitignore ├── package.json ├── README.md └── public └── index.html /todo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kateFrontend/react-to-do-app/HEAD/todo.jpg -------------------------------------------------------------------------------- /src/todo-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kateFrontend/react-to-do-app/HEAD/src/todo-image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | const root = ReactDOM.createRoot(document.getElementById('root')); 6 | root.render( 7 | 8 | 9 | 10 | ); 11 | 12 | // If you want to start measuring performance in your app, pass a function 13 | // to log results (for example: reportWebVitals(console.log)) 14 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 15 | 16 | -------------------------------------------------------------------------------- /src/Alert.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | function Alert({type, msg, removeAlert, list}) { // everytime when the props are going to change we will get a new set of timeout 4 | useEffect(() => { 5 | const timeout = setTimeout(() => { 6 | removeAlert() 7 | },2000) // after 2 seconds invoke removeAlert and then hide alert 8 | return () => clearTimeout(timeout) 9 | },[list]) 10 | return

{msg}

; // depending on the type it will be alert with the options danger or success 11 | } 12 | 13 | export default Alert; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "to-do-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-icons": "^4.6.0", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ToDo.js: -------------------------------------------------------------------------------- 1 | import { FaEdit, FaTrash } from "react-icons/fa"; 2 | 3 | function ToDo({ items, removeItem, editItem }) { 4 | // structure props from App.js in a function parameters 5 | return ( 6 |
7 | {items.map((item) => { 8 | const { id, title } = item; 9 | return ( 10 |
11 |

{title}

12 |
13 | 20 | 27 |
28 |
29 | ); 30 | })} 31 |
32 | ); 33 | } 34 | 35 | export default ToDo; 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![cover](./todo.jpg) 2 |
View Demo ToDo React App
3 | 4 | ## 🦉 Main information 5 | 6 | ToDo app is an app that allows users to add, edit, and delete tasks they want to work on. These actions touch upon the four aspects of a CRUD (Create, Read, Update, and Delete) application. 7 | 8 | This application integrate React useState and useEffect Hooks. React Hooks allow for functional components to have a state and use lifecycle methods, allowing you to avoid using class components and have more modular and readable code. 9 | 10 | ToDo app is an excellent app to practice hooks, state, style and data manipulation. 11 | 12 | Here you can find the sourse code with all the necessary and useful comments. 13 | 14 | ## Built With 15 | 16 | [React](https://reactjs.org/) / [JavaScript](https://www.w3schools.com/js/) 17 | 18 | ## ⏲️ Run project 19 | 20 | In the project directory, you can run: 21 | 22 | ### `npm start` 23 | 24 | Runs the app in the development mode.\ 25 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 26 | 27 | The page will reload when you make changes.\ 28 | You may also see any lint errors in the console. 29 | 30 | ## ⚡ Setup 31 | 32 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 33 | 34 | The following technologies and components were used in this project: 35 | 36 | ### Create React app 37 | 38 | ``` 39 | npx create-react-app my-app 40 | cd my-app 41 | npm start 42 | ``` 43 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 19 | 28 | To Do List 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import Alert from "./Alert"; 3 | import "./App.css"; 4 | import ToDo from "./ToDo"; 5 | import image from "./todo-image.png"; 6 | 7 | // We need to have a localStorage to not lose all values after refreshing the page 8 | 9 | const getLocalStorage = () => { 10 | let list = localStorage.getItem("list"); 11 | if (list) { 12 | return JSON.parse(localStorage.getItem("list")); 13 | } else { 14 | return []; 15 | } 16 | }; 17 | 18 | function App() { 19 | const [name, setName] = useState(""); // value that we'll use in our form / the empty value by default 20 | const [list, setList] = useState(getLocalStorage()); // empty array that we'll use for local storage // local storage list 21 | const [isEditing, setIsEditing] = useState(false); // a flag in a state whether is editing or not 22 | const [editID, setEditID] = useState(null); // this state will reflect which ite is actually editing 23 | const [alert, setAlert] = useState({ 24 | show: false, 25 | msg: "", 26 | type: "", 27 | }); // is an object from we can display alert information 28 | 29 | const handleSubmit = (e) => { 30 | e.preventDefault(); // the preventDefault() method cancels the event if it is cancelable, meaning that the default action that belongs to the event will not occur. // clicking on a "Submit" button, prevent it from submitting a form 31 | // console.log('hello'); 32 | if (!name) { 33 | // check if the value in input is empty and if is empty then display the alert 34 | showAlert(true, "danger", "please enter value"); // display alert 35 | } else if (name && isEditing) { 36 | // check if there's something in the value and if the editing is true 37 | setList( 38 | list.map((item) => { 39 | // we have our list and we're iterating over it 40 | if (item.id === editID) { 41 | // if the item Id matches to whatever we have in a state, the return all the propreties 42 | return { ...item, title: name }; // return Id and change the title to whatever is the state 43 | } 44 | return item; 45 | }) 46 | ); 47 | setName(""); 48 | setEditID(null); 49 | setIsEditing(false); 50 | showAlert(true, "success", "value changed"); 51 | } else { 52 | // show alert 53 | showAlert(true, "success", "new task added to the list"); 54 | const newItem = { 55 | // create a new item is equil to the object with an unique ID and a title tht will be equil to the name value that is coming from the state 56 | id: new Date().getTime().toString(), 57 | title: name, 58 | }; 59 | setList([...list, newItem]); // ... get me the previous values from the state add add a new one 60 | setName(""); 61 | } 62 | }; 63 | 64 | const showAlert = (show = false, type = "", msg = "") => { 65 | // parameters by default 66 | setAlert({ show, type, msg }); // if the property name matches to the variable name that holds the value then show and type an message 67 | }; 68 | 69 | const clearList = () => { 70 | showAlert(true, "danger", "empty list"); 71 | setList([]); 72 | }; 73 | 74 | const removeItem = (id) => { 75 | showAlert(true, "danger", "task removed"); 76 | setList(list.filter((item) => item.id !== id)); // list filter always return a new array / if item Id matches to whatever idea passed into remove item, then don't return it from thos filter function. If item Id doesn't match, then it's going to be added to the new array 77 | }; 78 | 79 | const editItem = (id) => { 80 | // get a specific item whose Id matches 81 | const specificItem = list.find((item) => item.id === id); // if the item Id matches, then return that item 82 | setIsEditing(true); 83 | setEditID(id); 84 | setName(specificItem.title); 85 | }; 86 | 87 | useEffect(() => { 88 | localStorage.setItem("list", JSON.stringify(list)); 89 | }, [list]); 90 | 91 | return ( 92 | <> 93 |
94 | {/*

To-do list

*/} 95 |
96 |
97 | {alert.show && ( 98 | 99 | )}{" "} 100 | {/* inside of alert component pass all the properties from state alert value */} 101 | {/* show some checking for the proprety of show more specific for the value and if that is the case - display it // you can check it if you change useState for alert to show:true // The logical AND (&&) operator for a set of boolean operands will be true if and only if all the operands are true. Otherwise it will be false. */} 102 |
103 | setName(e.target.value)} 109 | /> 110 | 114 |
115 | 116 | {list.length > 0 && ( 117 |
118 | {" "} 119 | {/* list as a prop into Todo component named 'items' */} 120 | 123 |
124 | )} 125 |
126 | 127 |
128 |
129 |
130 | 131 | ); 132 | } 133 | 134 | export default App; 135 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | =============== 3 | Variables 4 | =============== 5 | */ 6 | 7 | :root { 8 | /* dark shades of primary color*/ 9 | --main-yellow: #ffc727; 10 | --dark-yellow: #f4ae00; 11 | --main-red: red; 12 | --dark-red: rgb(125, 1, 1); 13 | --clr-black: #222; 14 | --clr-white: #fff; 15 | --clr-primary: hsl(205, 100%, 96%); 16 | --clr-grey-1: hsl(209, 61%, 16%); 17 | --clr-grey-5: hsl(210, 22%, 49%); 18 | --clr-grey-10: hsl(0, 0%, 94%); 19 | --transition: all 0.3s linear; 20 | --spacing: 0.1rem; 21 | --radius: 0.25rem; 22 | --light-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 23 | --dark-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); 24 | --max-width: 1170px; 25 | --fixed-width: 620px; 26 | } 27 | /* 28 | =============== 29 | Global Styles 30 | =============== 31 | */ 32 | 33 | *, 34 | ::after, 35 | ::before { 36 | margin: 0; 37 | padding: 0; 38 | box-sizing: border-box; 39 | } 40 | body { 41 | font-family: 'Poppins', sans-serif; 42 | background: var(--main-yellow); 43 | color: var(--clr-grey-1); 44 | line-height: 1.5; 45 | font-size: 0.875rem; 46 | } 47 | ul { 48 | list-style-type: none; 49 | } 50 | a { 51 | text-decoration: none; 52 | } 53 | h1, 54 | h2, 55 | h3, 56 | h4 { 57 | letter-spacing: var(--spacing); 58 | text-transform: capitalize; 59 | line-height: 1.25; 60 | margin-bottom: 0.75rem; 61 | } 62 | h1 { 63 | font-size: 3rem; 64 | } 65 | h2 { 66 | font-size: 2rem; 67 | } 68 | h3 { 69 | font-size: 1.25rem; 70 | } 71 | h4 { 72 | font-size: 0.875rem; 73 | } 74 | p { 75 | margin-bottom: 1.25rem; 76 | color: var(--clr-grey-5); 77 | } 78 | 79 | .img-container { 80 | display: flex; 81 | justify-content: center; 82 | } 83 | 84 | .image { 85 | width: 400px; 86 | margin: 5rem 0; 87 | } 88 | 89 | @media screen and (min-width: 800px) { 90 | h1 { 91 | font-size: 4rem; 92 | } 93 | h2 { 94 | font-size: 2.5rem; 95 | } 96 | h3 { 97 | font-size: 1.75rem; 98 | } 99 | h4 { 100 | font-size: 1rem; 101 | } 102 | body { 103 | font-size: 1rem; 104 | } 105 | h1, 106 | h2, 107 | h3, 108 | h4 { 109 | line-height: 1; 110 | } 111 | 112 | } 113 | .btn { 114 | text-transform: uppercase; 115 | color: var(--clr-black); 116 | padding: 0.375rem 0.75rem; 117 | letter-spacing: var(--spacing); 118 | transition: var(--transition); 119 | font-size: 0.875rem; 120 | cursor: pointer; 121 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 122 | border-radius: var(--radius); 123 | } 124 | .btn:hover { 125 | color: var(--clr-white); 126 | background: var(--clr-black); 127 | } 128 | /* section */ 129 | .section { 130 | padding: 5rem 0; 131 | } 132 | 133 | .section-center { 134 | width: 90vw; 135 | margin: 0 auto; 136 | max-width: 35rem; 137 | margin-top: 8rem; 138 | } 139 | 140 | main { 141 | min-height: 100vh; 142 | display: grid; 143 | place-items: center; 144 | } 145 | /* 146 | =============== 147 | Grocery List 148 | =============== 149 | */ 150 | .section-center { 151 | background: var(--clr-white); 152 | border-radius: var(--radius); 153 | box-shadow: var(--light-shadow); 154 | transition: var(--transition); 155 | padding: 3rem 1rem; 156 | box-shadow: var(--dark-shadow); 157 | } 158 | 159 | .alert { 160 | margin: 1rem 0 2rem 0; 161 | height: 2rem; 162 | display: grid; 163 | align-items: center; 164 | text-align: center; 165 | font-size: 1rem; 166 | border-radius: 0.25rem; 167 | letter-spacing: var(--spacing); 168 | text-transform: capitalize; 169 | } 170 | .alert-danger { 171 | color: var(--clr-white); 172 | background: var(--main-red); 173 | } 174 | .alert-success { 175 | color: var(--clr-black); 176 | background: var(--main-yellow); 177 | } 178 | 179 | .form-control { 180 | display: flex; 181 | justify-content: center; 182 | } 183 | .todo { 184 | padding: 0.5rem 1rem; 185 | background: var(--clr-grey-10); 186 | border-top-left-radius: var(--radius); 187 | border-bottom-left-radius: var(--radius); 188 | border-color: transparent; 189 | font-size: 1.2rem; 190 | flex: 1 0 auto; 191 | color: var(--clr-grey-5); 192 | border: 1px solid var(--dark-yellow) 193 | } 194 | .todo::placeholder { 195 | color: var(--clr-grey-5); 196 | } 197 | 198 | 199 | .todo-task { 200 | padding: 0.25rem; 201 | padding-left: 1rem; 202 | } 203 | 204 | .icons { 205 | width: 2rem; 206 | height: 1rem; 207 | } 208 | 209 | .submit-btn { 210 | background: var(--main-yellow); 211 | border-color: transparent; 212 | flex: 0 0 5rem; 213 | display: grid; 214 | align-items: center; 215 | padding: 0.25rem; 216 | text-transform: capitalize; 217 | letter-spacing: 2px; 218 | border-top-right-radius: var(--radius); 219 | border-bottom-right-radius: var(--radius); 220 | cursor: pointer; 221 | transition: var(--transition); 222 | font-size: 0.85rem; 223 | font-weight: bold; 224 | } 225 | .submit-btn:hover { 226 | background: var(--main-yellow); 227 | color: var(--clr-white); 228 | } 229 | 230 | .todo-container { 231 | margin-top: 2rem; 232 | } 233 | 234 | .todo-item { 235 | display: flex; 236 | align-items: center; 237 | justify-content: space-between; 238 | margin-bottom: 0.5rem; 239 | transition: var(--transition); 240 | padding: 0.25rem 1rem; 241 | border-radius: var(--radius); 242 | text-transform: capitalize; 243 | border-bottom: 1px solid var(--main-yellow); 244 | } 245 | .todo-item:hover { 246 | color: var(--clr-grey-5); 247 | background: var(--clr-grey-10); 248 | } 249 | .todo-item:hover .title { 250 | color: var(--clr-grey-5); 251 | } 252 | .title { 253 | margin-bottom: 0; 254 | color: var(--clr-grey-1); 255 | letter-spacing: 2px; 256 | transition: var(--transition); 257 | } 258 | 259 | .title-center { 260 | color: var(--clr-grey-1); 261 | letter-spacing: 2px; 262 | transition: var(--transition); 263 | display: flex; 264 | justify-content: center; 265 | margin-top: 10rem; 266 | } 267 | 268 | .edit-btn, 269 | .delete-btn { 270 | background: transparent; 271 | border-color: transparent; 272 | cursor: pointer; 273 | font-size: 0.7rem; 274 | margin: 0 0.15rem; 275 | transition: var(--transition); 276 | } 277 | .edit-btn { 278 | color:var(--dark-yellow); 279 | } 280 | .edit-btn:hover { 281 | color: var(--main-yellow); 282 | } 283 | .delete-btn { 284 | color: var(--main-red) 285 | } 286 | .delete-btn:hover { 287 | color: var(--dark-red); 288 | } 289 | .clear-btn { 290 | text-transform: capitalize; 291 | display: grid; 292 | align-items: center; 293 | background: #ffc727; 294 | border-radius: var(--radius); 295 | font-weight: bold; 296 | padding: 0.5rem 2rem; 297 | border-color: transparent; 298 | color: var(--dark-red); 299 | margin: 0 auto; 300 | font-size: 0.85rem; 301 | letter-spacing: var(--spacing); 302 | cursor: pointer; 303 | transition: var(--transition); 304 | margin-top: 1.5rem; 305 | 306 | } 307 | .clear-btn:hover { 308 | color: var(--clr-white); 309 | } 310 | 311 | @media all and (min-width: 992px) { 312 | .section-center { 313 | width: 95vw; 314 | 315 | } 316 | } 317 | 318 | @media all and (max-width: 500px) { 319 | .section-center { 320 | width: 100vw; 321 | } 322 | 323 | .todo { 324 | font-size: 1rem; 325 | } 326 | 327 | .icons { 328 | width: 1.5rem; 329 | } 330 | 331 | } --------------------------------------------------------------------------------