diff --git a/.gitignore b/.gitignore index 76add87..5232681 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -dist \ No newline at end of file +dist +.vscode diff --git a/package.json b/package.json index 72d99a3..756eb03 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,18 @@ "preview": "vite preview" }, "dependencies": { + "@headlessui/react": "^2.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", - "typescript": "^4.6.0", - "vite": "^4.0.0", "@vitejs/plugin-react": "^3.0.0", - "tailwindcss": "^3.0.0", + "autoprefixer": "^10.0.0", "postcss": "^8.0.0", - "autoprefixer": "^10.0.0" + "tailwindcss": "^3.0.0", + "typescript": "^4.6.0", + "vite": "^4.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fe8063..2f37adf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@headlessui/react': + specifier: ^2.2.0 + version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -261,6 +264,34 @@ packages: cpu: [x64] os: [win32] + '@floating-ui/core@1.6.9': + resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==} + + '@floating-ui/dom@1.6.13': + resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + + '@headlessui/react@2.2.0': + resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -299,6 +330,52 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@react-aria/focus@3.19.1': + resolution: {integrity: sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.23.0': + resolution: {integrity: sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.9.7': + resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.27.0': + resolution: {integrity: sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/utils@3.10.5': + resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.27.0': + resolution: {integrity: sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tanstack/react-virtual@3.13.2': + resolution: {integrity: sha512-LceSUgABBKF6HSsHK2ZqHzQ37IKV/jlaWbHm+NyTa3/WNb/JZVcThDuTainf+PixltOOcFCYXwxbLpOX9sCx+g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/virtual-core@3.13.2': + resolution: {integrity: sha512-Qzz4EgzMbO5gKrmqUondCjiHcuu4B1ftHb0pjCut661lXZdGoHeze9f/M8iwsK1t5LGR6aNuNGU7mxkowaW6RQ==} + '@types/prop-types@15.7.14': resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==} @@ -379,6 +456,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -763,6 +844,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tailwindcss@3.4.17: resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} engines: {node: '>=14.0.0'} @@ -782,6 +866,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} @@ -1030,6 +1117,40 @@ snapshots: '@esbuild/win32-x64@0.18.20': optional: true + '@floating-ui/core@1.6.9': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.6.13': + dependencies: + '@floating-ui/core': 1.6.9 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.13 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/utils': 0.2.9 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tabbable: 6.2.0 + + '@floating-ui/utils@0.2.9': {} + + '@headlessui/react@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/focus': 3.19.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/interactions': 3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-virtual': 3.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -1071,6 +1192,61 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@react-aria/focus@3.19.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/interactions': 3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/utils': 3.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.27.0(react@18.3.1) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/interactions@3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.7(react@18.3.1) + '@react-aria/utils': 3.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-types/shared': 3.27.0(react@18.3.1) + '@swc/helpers': 0.5.15 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-aria/ssr@3.9.7(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.15 + react: 18.3.1 + + '@react-aria/utils@3.27.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-aria/ssr': 3.9.7(react@18.3.1) + '@react-stately/utils': 3.10.5(react@18.3.1) + '@react-types/shared': 3.27.0(react@18.3.1) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@react-stately/utils@3.10.5(react@18.3.1)': + dependencies: + '@swc/helpers': 0.5.15 + react: 18.3.1 + + '@react-types/shared@3.27.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tanstack/react-virtual@3.13.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/virtual-core@3.13.2': {} + '@types/prop-types@15.7.14': {} '@types/react-dom@18.3.5(@types/react@18.3.18)': @@ -1157,6 +1333,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -1500,6 +1678,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tabbable@6.2.0: {} + tailwindcss@3.4.17: dependencies: '@alloc/quick-lru': 5.2.0 @@ -1541,6 +1721,8 @@ snapshots: ts-interface-checker@0.1.13: {} + tslib@2.8.1: {} + typescript@4.9.5: {} update-browserslist-db@1.1.2(browserslist@4.24.4): diff --git a/src/App.tsx b/src/App.tsx index fcd2850..6002c16 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import React from "react"; -import TaskManager from "./components/TaskManager"; +import { TaskManager } from "./components/TaskManager"; function App() { return ( diff --git a/src/components/ConfirmationModal/ConfirmationModal.tsx b/src/components/ConfirmationModal/ConfirmationModal.tsx new file mode 100644 index 0000000..89d86b8 --- /dev/null +++ b/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -0,0 +1,57 @@ +import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from "@headlessui/react"; +import React from "react"; + +import { ConfirmationModalProps } from "./ConfirmationModal.types"; + +export const ConfirmationModal = ({ open, onOpen, onDelete }: ConfirmationModalProps) => { + return ( + + + +
+
+ +
+
+
+ + Delete task + +
+

+ Are you sure you want to delete this task?
+ This action cannot be undone. +

+
+
+
+
+
+ + +
+
+
+
+
+ ); +}; diff --git a/src/components/ConfirmationModal/ConfirmationModal.types.ts b/src/components/ConfirmationModal/ConfirmationModal.types.ts new file mode 100644 index 0000000..e1b5c3c --- /dev/null +++ b/src/components/ConfirmationModal/ConfirmationModal.types.ts @@ -0,0 +1,5 @@ +export interface ConfirmationModalProps { + open: boolean; + onOpen: (open: boolean) => void; + onDelete: () => void; +} diff --git a/src/components/ConfirmationModal/index.ts b/src/components/ConfirmationModal/index.ts new file mode 100644 index 0000000..828239d --- /dev/null +++ b/src/components/ConfirmationModal/index.ts @@ -0,0 +1 @@ +export * from './ConfirmationModal'; diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx new file mode 100644 index 0000000..8657f86 --- /dev/null +++ b/src/components/Filter/Filter.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import { FilterProps } from "./Filter.types"; + +export const Filter = ({ statuses, onFilter, currentStatus }: FilterProps) => { + return ( +
+ {statuses.map((status) => ( + + ))} +
+ ); +}; diff --git a/src/components/Filter/Filter.types.ts b/src/components/Filter/Filter.types.ts new file mode 100644 index 0000000..9b9876f --- /dev/null +++ b/src/components/Filter/Filter.types.ts @@ -0,0 +1,5 @@ +export interface FilterProps { + currentStatus: string; + statuses: string[]; + onFilter: (filter: string) => void; +} diff --git a/src/components/Filter/index.ts b/src/components/Filter/index.ts new file mode 100644 index 0000000..0eea779 --- /dev/null +++ b/src/components/Filter/index.ts @@ -0,0 +1 @@ +export * from './Filter'; diff --git a/src/components/TaskForm/TaskForm.tsx b/src/components/TaskForm/TaskForm.tsx new file mode 100644 index 0000000..cf613c8 --- /dev/null +++ b/src/components/TaskForm/TaskForm.tsx @@ -0,0 +1,31 @@ +import React, { FormEvent, useState } from "react"; + +import { TaskFormProps } from "./TaskForm.types"; + +export const TaskForm = ({ onSubmit }: TaskFormProps) => { + const [newTask, setNewTask] = useState(""); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + + if (newTask.trim() === "") return; + + onSubmit(newTask); + setNewTask(""); + }; + + return ( +
+ setNewTask(event.target.value)} + className="flex-grow border rounded-l py-2 px-3" + /> + +
+ ); +}; diff --git a/src/components/TaskForm/TaskForm.types.ts b/src/components/TaskForm/TaskForm.types.ts new file mode 100644 index 0000000..e432baf --- /dev/null +++ b/src/components/TaskForm/TaskForm.types.ts @@ -0,0 +1,3 @@ +export interface TaskFormProps { + onSubmit: (task: string) => void; +} diff --git a/src/components/TaskForm/index.ts b/src/components/TaskForm/index.ts new file mode 100644 index 0000000..c46c0dc --- /dev/null +++ b/src/components/TaskForm/index.ts @@ -0,0 +1 @@ +export * from './TaskForm'; diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx deleted file mode 100644 index 6c2a176..0000000 --- a/src/components/TaskItem.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; - -const TaskItem = ({ task, onDelete, onToggle }: any) => { - return ( -
  • - onToggle(task.id)} - className={`cursor-pointer ${ - task.isCompleted ? "text-black" : "line-through text-green-500" - }`} - > - {task.title} - - - -
  • - ); -}; - -export default TaskItem; diff --git a/src/components/TaskItem/TaskItem.tsx b/src/components/TaskItem/TaskItem.tsx new file mode 100644 index 0000000..9622815 --- /dev/null +++ b/src/components/TaskItem/TaskItem.tsx @@ -0,0 +1,34 @@ +import React, { useState } from "react"; + +import { ConfirmationModal } from "../ConfirmationModal"; +import { TaskItemProps } from "./TaskItem.types"; + +export const TaskItem = ({ task, onDelete, onToggle }: TaskItemProps) => { + const [open, setOpen] = useState(false); + + const handleDelete = () => { + setOpen(true); + }; + + const handleConfirmDelete = () => { + onDelete(task.id); + setOpen(false); + }; + + return ( +
  • + + + + + +
  • + ); +}; diff --git a/src/components/TaskItem/TaskItem.types.ts b/src/components/TaskItem/TaskItem.types.ts new file mode 100644 index 0000000..de496f1 --- /dev/null +++ b/src/components/TaskItem/TaskItem.types.ts @@ -0,0 +1,8 @@ + +import { ITask } from "../TaskManager/TaskManager.types"; + +export interface TaskItemProps { + task: ITask; + onToggle: (id: number) => void; + onDelete: (id: number) => void; +} diff --git a/src/components/TaskItem/index.ts b/src/components/TaskItem/index.ts new file mode 100644 index 0000000..3d23b46 --- /dev/null +++ b/src/components/TaskItem/index.ts @@ -0,0 +1 @@ +export * from './TaskItem'; diff --git a/src/components/TaskManager.tsx b/src/components/TaskManager.tsx deleted file mode 100644 index 7b280e8..0000000 --- a/src/components/TaskManager.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useState } from "react"; - -import TaskItem from "./TaskItem"; - -const TaskManager = () => { - const [tasks, setTasks] = useState([ - { id: 1, title: "Buy groceries", completed: false }, - { id: 2, title: "Clean the house", completed: true }, - ]); - const [filter, setFilter] = useState("all"); - const [newTask, setNewTask] = useState(); - - // Intentional bug: The filter conditions are reversed. - const filteredTasks = tasks.filter((task) => { - if (filter === "completed") return task.completed === false; - if (filter === "pending") return task.completed === true; - return true; - }); - - const handleAddTask = (e: React.FormEvent) => { - e.preventDefault(); - if (newTask!.trim() === "") return; - const newTaskObj = { - id: tasks.length + 1, - name: newTask, - completed: false, - }; - setTasks([...tasks, newTaskObj]); - setNewTask(""); - }; - - // Intentional bug: Directly mutating the tasks array when deleting. - const handleDeleteTask = (id: number) => { - const index = tasks.findIndex((task) => task.id === id); - if (index !== -1) { - tasks.splice(index, 1); - setTasks(tasks); - } - }; - - const toggleTaskCompletion = (id: number) => { - const task = tasks.find((task) => task.id === id); - - task.isCompleted = !task.isCompleted; - }; - - return ( -
    -
    - setNewTask(e.target.value)} - className="flex-grow border rounded-l py-2 px-3" - /> - -
    -
    - - - -
    -
      - {filteredTasks.map((task) => ( - - ))} -
    -
    - ); -}; - -export default TaskManager; diff --git a/src/components/TaskManager/TaskManager.tsx b/src/components/TaskManager/TaskManager.tsx new file mode 100644 index 0000000..b15577f --- /dev/null +++ b/src/components/TaskManager/TaskManager.tsx @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from "react"; + +import { Filter } from "../Filter"; +import { TaskItem } from "../TaskItem"; + +import { TaskForm } from "../TaskForm"; +import { ITask } from "./TaskManager.types"; + +export const TaskManager = () => { + const [tasks, setTasks] = useState([]); + const [filter, setFilter] = useState("all"); + + const statuses = ["all", "completed", "pending"]; + + useEffect(() => { + const localTasks = localStorage.getItem("tasks"); + const parsedTasks = localTasks ? JSON.parse(localTasks) : []; + + setTasks(parsedTasks); + }, []); + + const filteredTasks = tasks.filter((task) => { + if (filter === "completed") return task.completed; + if (filter === "pending") return !task.completed; + return true; + }); + + const handleAddTask = (title: string) => { + const newTaskObj = { + id: tasks.length + 1, + title, + completed: false, + }; + + setTasks([...tasks, newTaskObj]); + + const serializedTasks = JSON.stringify([...tasks, newTaskObj]); + + localStorage.setItem("tasks", serializedTasks); + }; + + const handleDeleteTask = (id: number) => { + const newTasks = tasks.filter((task) => task.id !== id); + + setTasks(newTasks); + + const serializedTasks = JSON.stringify([...newTasks]); + + localStorage.setItem("tasks", serializedTasks); + }; + + const toggleTaskCompletion = (id: number) => { + const newTasks = tasks.map((task) => { + if (task.id === id) { + task.completed = !task.completed; + } + + return task; + }); + + setTasks(newTasks); + + const serializedTasks = JSON.stringify([...newTasks]); + + localStorage.setItem("tasks", serializedTasks); + }; + + return ( +
    + + + + +
      + {filteredTasks.length === 0 &&

      No tasks found

      } + + {filteredTasks.map((task) => ( + + ))} +
    +
    + ); +}; diff --git a/src/components/TaskManager/TaskManager.types.ts b/src/components/TaskManager/TaskManager.types.ts new file mode 100644 index 0000000..f8138c7 --- /dev/null +++ b/src/components/TaskManager/TaskManager.types.ts @@ -0,0 +1,5 @@ +export interface ITask { + id: number; + title: string; + completed: boolean; +} diff --git a/src/components/TaskManager/index.ts b/src/components/TaskManager/index.ts new file mode 100644 index 0000000..c08b640 --- /dev/null +++ b/src/components/TaskManager/index.ts @@ -0,0 +1 @@ +export * from './TaskManager';