├── 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 |
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 |
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 |
5 | {todosProps.map((todo) => (
6 |
13 | ))}
14 |
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 |
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 |
--------------------------------------------------------------------------------