├── jsconfig.json ├── src ├── styles │ ├── Header.module.css │ ├── TodoItem.module.css │ └── app.css ├── components │ ├── TodoApp.jsx │ ├── Header.jsx │ ├── TodosList.jsx │ ├── Modal.jsx │ ├── Navbar.jsx │ ├── InputTodo.jsx │ ├── TodoItem.jsx │ └── TodosLogic.jsx ├── main.jsx ├── useOnClickOutside.jsx └── assets │ └── react.svg ├── vite.config.js ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── package.json └── public └── vite.svg /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/Header.module.css: -------------------------------------------------------------------------------- 1 | .header h1 { 2 | font-size: 3rem; 3 | font-weight: 600; 4 | margin-bottom: 1rem; 5 | line-height: 1em; 6 | text-transform: lowercase; 7 | text-align: center; 8 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | '@': path.resolve(__dirname, './src'), 10 | }, 11 | }, 12 | plugins: [react()], 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/TodoApp.jsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/Header'; 2 | import TodosLogic from '@/components/TodosLogic'; 3 | 4 | const TodoApp = () => { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 |
12 | ); 13 | }; 14 | export default TodoApp; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:react/recommended', 6 | 'plugin:react/jsx-runtime', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | settings: { react: { version: '18.2' } }, 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': 'warn', 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import styles from '@/styles/Header.module.css'; 2 | 3 | const Header = () => { 4 | const headerStyle = { 5 | padding: '20px 0', 6 | lineHeight: '1.5em', 7 | color: '#aeadad', 8 | textAlign: 'center', 9 | }; 10 | return ( 11 |
12 |

todos

13 |

Items will persist in the browser local storage

14 |
15 | ); 16 | }; 17 | export default Header; 18 | -------------------------------------------------------------------------------- /src/components/TodosList.jsx: -------------------------------------------------------------------------------- 1 | import TodoItem from '@/components/TodoItem'; 2 | const TodosList = ({ todosProps, handleChange, delTodo, setUpdate }) => { 3 | return ( 4 | 15 | ); 16 | }; 17 | export default TodosList; 18 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import TodoApp from '@/components/TodoApp'; 4 | import Navbar from '@/components/Navbar'; 5 | // import Modal from './components/Modal'; 6 | import '@/styles/app.css'; 7 | 8 | const domContainer = document.getElementById('root'); 9 | const root = ReactDOM.createRoot(domContainer); 10 | root.render( 11 | 12 | 13 | 14 | {/* */} 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/useOnClickOutside.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | export const useOnClickOutside = (ref, currentState, updater) => { 3 | useEffect(() => { 4 | const handler = (event) => { 5 | if (currentState && ref.current && !ref.current.contains(event.target)) { 6 | updater(); 7 | } 8 | }; 9 | document.addEventListener('mousedown', handler); 10 | return () => { 11 | // Cleanup the event listener 12 | document.removeEventListener('mousedown', handler); 13 | }; 14 | }, [ref, currentState, updater]); 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { useOnClickOutside } from '../useOnClickOutside'; 3 | const Modal = () => { 4 | const [openModal, setOpenModal] = useState(false); 5 | const ref = useRef(); 6 | 7 | useOnClickOutside(ref, openModal, () => setOpenModal(false)); 8 | return ( 9 |
10 | 11 | {openModal && ( 12 |
13 | setOpenModal(false)}>X 14 |
Modal content here
15 |
16 | )} 17 |
18 | ); 19 | }; 20 | export default Modal; 21 | -------------------------------------------------------------------------------- /src/styles/TodoItem.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | font-size: 1rem; 3 | list-style-type: none; 4 | padding: 14px 25px; 5 | border-bottom: 1px solid #fff; 6 | background: #f0efef; 7 | } 8 | 9 | .content { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | gap: 1rem; 14 | } 15 | 16 | .content span { 17 | flex: 1; 18 | order: 1; 19 | } 20 | 21 | .content button { 22 | cursor: pointer; 23 | background: transparent; 24 | order: 2; 25 | outline: none; 26 | border: none; 27 | margin-left: 7px; 28 | } 29 | 30 | .textInput { 31 | width: 100%; 32 | padding: 10px; 33 | border: 1px solid #dfdfdf; 34 | outline: none; 35 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-todo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-icons": "^4.9.0", 16 | "uuid": "^9.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.37", 20 | "@types/react-dom": "^18.0.11", 21 | "@vitejs/plugin-react": "^4.0.0", 22 | "eslint": "^8.38.0", 23 | "eslint-plugin-react": "^7.32.2", 24 | "eslint-plugin-react-hooks": "^4.6.0", 25 | "eslint-plugin-react-refresh": "^0.3.4", 26 | "vite": "^4.3.9" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { useOnClickOutside } from '../useOnClickOutside'; 3 | const Navbar = () => { 4 | const [dropdown, setDropdown] = useState(false); 5 | const ref = useRef(); 6 | 7 | useOnClickOutside(ref, dropdown, () => setDropdown(false)); 8 | 9 | return ( 10 | 27 | ); 28 | }; 29 | export default Navbar; 30 | -------------------------------------------------------------------------------- /src/components/InputTodo.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { FaPlusCircle } from 'react-icons/fa'; 3 | 4 | const InputTodo = ({ addTodoItem }) => { 5 | const [title, setTitle] = useState(''); 6 | const [message, setMessage] = useState(''); 7 | 8 | const handleChange = (e) => { 9 | setTitle(e.target.value); 10 | }; 11 | 12 | const handleSubmit = (e) => { 13 | e.preventDefault(); 14 | if (title.trim()) { 15 | addTodoItem(title); 16 | setTitle(''); 17 | setMessage(''); 18 | } else { 19 | setMessage('Please add item.'); 20 | } 21 | }; 22 | 23 | const handleUpdatedDone = (event) => { 24 | if (event.key === 'Enter') { 25 | setEditing(false); 26 | } 27 | }; 28 | 29 | return ( 30 | <> 31 |
32 | 39 | 42 |
43 | {message} 44 | 45 | ); 46 | }; 47 | export default InputTodo; 48 | -------------------------------------------------------------------------------- /src/styles/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font-family: "Segoe UI", sans-serif; 9 | line-height: 1.4; 10 | color: #000; 11 | background: #fff; 12 | height: 100vh; 13 | font-weight: 400; 14 | } 15 | 16 | .wrapper { 17 | width: 100%; 18 | padding: 8rem 20px 3rem; 19 | } 20 | 21 | .todos { 22 | max-width: 580px; 23 | margin: 0 auto; 24 | } 25 | 26 | .form-container { 27 | display: flex; 28 | border-radius: calc(0.1 * 100px); 29 | box-shadow: 0px 4px 14px 0px rgba(70, 70, 70, 0.38); 30 | justify-content: space-around; 31 | background: #fff; 32 | font-size: 1rem; 33 | } 34 | 35 | .input-text { 36 | width: 85%; 37 | padding-right: 5px; 38 | padding-left: 10px; 39 | border-radius: calc(0.5 * 100px); 40 | font-size: 1rem; 41 | } 42 | 43 | .input-text::placeholder { 44 | color: #000; 45 | } 46 | 47 | .input-submit { 48 | background: transparent; 49 | color: #5b5b5b; 50 | text-transform: capitalize; 51 | cursor: pointer; 52 | font-weight: 600; 53 | margin-right: 10px; 54 | } 55 | 56 | .input-text, 57 | .input-submit { 58 | height: 45px; 59 | outline: none; 60 | border: none; 61 | } 62 | 63 | .submit-warning { 64 | font-size: 13px; 65 | color: red; 66 | margin-top: 5px; 67 | display: block; 68 | } -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import styles from '@/styles/TodoItem.module.css'; 2 | import { useState, useRef } from 'react'; 3 | import { FaTrash } from 'react-icons/fa'; 4 | import { AiFillEdit } from 'react-icons/ai'; 5 | 6 | const TodoItem = ({ itemProp, handleChange, delTodo, setUpdate }) => { 7 | const editInputRef = useRef(null); 8 | const [editing, setEditing] = useState(false); 9 | const completedStyle = { 10 | fontStyle: 'italic', 11 | color: '#595959', 12 | opacity: 0.4, 13 | textDecoration: 'line-through', 14 | }; 15 | 16 | const handleEditing = () => { 17 | setEditing(true); 18 | }; 19 | 20 | let viewMode = {}; 21 | let editMode = {}; 22 | if (editing) { 23 | viewMode.display = 'none'; 24 | } else { 25 | editMode.display = 'none'; 26 | } 27 | 28 | const handleUpdatedDone = (event) => { 29 | if (event.key === 'Enter') { 30 | setUpdate(editInputRef.current.value, itemProp.id); 31 | setEditing(false); 32 | } 33 | }; 34 | 35 | return ( 36 |
  • 37 |
    38 | handleChange(itemProp.id)} 42 | /> 43 | 46 | 49 | 50 | {itemProp.title} 51 | 52 |
    53 | 61 |
  • 62 | ); 63 | }; 64 | export default TodoItem; 65 | -------------------------------------------------------------------------------- /src/components/TodosLogic.jsx: -------------------------------------------------------------------------------- 1 | import InputTodo from '@/components/InputTodo'; 2 | import TodosList from '@/components/TodosList'; 3 | import { useState, useEffect } from 'react'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | 6 | const TodosLogic = () => { 7 | const [todos, setTodos] = useState(getInitialTodos()); 8 | 9 | const handleChange = (id) => { 10 | setTodos((prevState) => 11 | prevState.map((todo) => { 12 | if (todo.id === id) { 13 | return { 14 | ...todo, 15 | completed: !todo.completed, 16 | }; 17 | } 18 | return todo; 19 | }) 20 | ); 21 | }; 22 | 23 | const delTodo = (id) => { 24 | setTodos([ 25 | ...todos.filter((todo) => { 26 | return todo.id !== id; 27 | }), 28 | ]); 29 | }; 30 | 31 | const addTodoItem = (title) => { 32 | const newTodo = { 33 | id: uuidv4(), 34 | title: title, 35 | completed: false, 36 | }; 37 | setTodos([...todos, newTodo]); 38 | }; 39 | 40 | const setUpdate = (updatedTitle, id) => { 41 | setTodos( 42 | todos.map((todo) => { 43 | if (todo.id === id) { 44 | todo.title = updatedTitle; 45 | } 46 | return todo; 47 | }) 48 | ); 49 | }; 50 | 51 | useEffect(() => { 52 | // storing todos items 53 | const temp = JSON.stringify(todos); 54 | localStorage.setItem('todos', temp); 55 | }, [todos]); 56 | 57 | function getInitialTodos() { 58 | // getting stored items 59 | const temp = localStorage.getItem('todos'); 60 | const savedTodos = JSON.parse(temp); 61 | return savedTodos || []; 62 | } 63 | 64 | return ( 65 | <> 66 | 67 | 73 | 74 | ); 75 | }; 76 | export default TodosLogic; 77 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------