diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml
new file mode 100644
index 0000000..c7eb41f
--- /dev/null
+++ b/.github/workflows/production.yaml
@@ -0,0 +1,24 @@
+name: Vercel Production Deployment
+env:
+ VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
+ VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
+on:
+ push:
+ branches:
+ - main
+jobs:
+ Deploy-Production:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10
+ - uses: actions/checkout@v2
+ - name: Install Vercel CLI
+ run: npm install --global vercel@latest
+ - name: Pull Vercel Environment Information
+ run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Build Project Artifacts
+ run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
+ - name: Deploy Project Artifacts to Vercel
+ run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
diff --git a/package.json b/package.json
index 72d99a3..958874d 100644
--- a/package.json
+++ b/package.json
@@ -9,16 +9,17 @@
},
"dependencies": {
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "zustand": "^5.0.3"
},
"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..f67acf9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
+ zustand:
+ specifier: ^5.0.3
+ version: 5.0.3(@types/react@18.3.18)(react@18.3.1)
devDependencies:
'@types/react':
specifier: ^18.0.0
@@ -845,6 +848,24 @@ packages:
engines: {node: '>= 14'}
hasBin: true
+ zustand@5.0.3:
+ resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ '@types/react': '>=18.0.0'
+ immer: '>=9.0.6'
+ react: '>=18.0.0'
+ use-sync-external-store: '>=1.2.0'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -1578,3 +1599,8 @@ snapshots:
yallist@3.1.1: {}
yaml@2.7.0: {}
+
+ zustand@5.0.3(@types/react@18.3.18)(react@18.3.1):
+ optionalDependencies:
+ '@types/react': 18.3.18
+ react: 18.3.1
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/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 (
-
-
-
-
-
-
-
-
- {filteredTasks.map((task) => (
-
- ))}
-
-
- );
-};
-
-export default TaskManager;
diff --git a/src/components/TaskManager/DeleteTaskModal.tsx b/src/components/TaskManager/DeleteTaskModal.tsx
new file mode 100644
index 0000000..2c57d03
--- /dev/null
+++ b/src/components/TaskManager/DeleteTaskModal.tsx
@@ -0,0 +1,47 @@
+import React from "react";
+import useTaskStore from "../../store/taskStore";
+
+const DeleteTaskModal = () => {
+ const deleteTask = useTaskStore((state) => state.deleteTask);
+ const deletingTask = useTaskStore((state) => state.deletingTask);
+ const setDeletingTask = useTaskStore((state) => state.setDeletingTask);
+
+ const handleCloseModal = () => {
+ setDeletingTask(null);
+ };
+
+ if (!deletingTask) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+
Are you sure?
+
+ Do you really want to delete the task {deletingTask.title}?
+
+
+
+
+
+
+
+ );
+};
+
+export default DeleteTaskModal;
diff --git a/src/components/TaskManager/TaskFilter.tsx b/src/components/TaskManager/TaskFilter.tsx
new file mode 100644
index 0000000..1b65af1
--- /dev/null
+++ b/src/components/TaskManager/TaskFilter.tsx
@@ -0,0 +1,27 @@
+import React from "react";
+import { Filter } from "../../types/filter";
+import useTaskStore from "../../store/taskStore";
+
+const TaskFilter = () => {
+ const currentFilter = useTaskStore((state) => state.currentFilter);
+ const setFilter = useTaskStore((state) => state.setFilter);
+ const filters: Filter[] = ["all", "completed", "pending"];
+
+ return (
+
+ {filters.map((filter) => (
+
+ ))}
+
+ );
+};
+
+export default TaskFilter;
diff --git a/src/components/TaskManager/TaskForm.tsx b/src/components/TaskManager/TaskForm.tsx
new file mode 100644
index 0000000..06eb7e4
--- /dev/null
+++ b/src/components/TaskManager/TaskForm.tsx
@@ -0,0 +1,37 @@
+import React, { useState } from "react";
+import useTaskStore from "../../store/taskStore";
+
+const TaskForm = () => {
+ const tasks = useTaskStore((state) => state.tasks);
+ const addTask = useTaskStore((state) => state.addTask);
+ const [newTaskTitle, setNewTaskTitle] = useState("");
+
+ const handleAddTask = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (newTaskTitle.trim() === "") return;
+
+ const existingTask = tasks.find((task) => task.title === newTaskTitle);
+ if (existingTask) return;
+
+ addTask(newTaskTitle);
+ setNewTaskTitle("");
+ };
+
+ return (
+
+ );
+};
+
+export default TaskForm;
diff --git a/src/components/TaskManager/TaskList/TaskItem.tsx b/src/components/TaskManager/TaskList/TaskItem.tsx
new file mode 100644
index 0000000..335a64e
--- /dev/null
+++ b/src/components/TaskManager/TaskList/TaskItem.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { Task } from "../../../types/task";
+import useTaskStore from "../../../store/taskStore";
+
+const TaskItem = ({ task }: { task: Task }) => {
+ const toggleTaskCompletion = useTaskStore(
+ (state) => state.toggleTaskCompletion
+ );
+ const setDeletingTask = useTaskStore((state) => state.setDeletingTask);
+
+ return (
+ <>
+
+ toggleTaskCompletion(task.id)}
+ className={`cursor-pointer select-none duration-200 active:scale-110 ${
+ task.completed ? "line-through text-green-500" : "text-black"
+ }`}
+ >
+ {task.title}
+
+
+
+
+ >
+ );
+};
+
+export default TaskItem;
diff --git a/src/components/TaskManager/TaskList/index.tsx b/src/components/TaskManager/TaskList/index.tsx
new file mode 100644
index 0000000..7f1446b
--- /dev/null
+++ b/src/components/TaskManager/TaskList/index.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import TaskItem from "./TaskItem";
+import useTaskStore from "../../../store/taskStore";
+
+const TaskList = () => {
+ const tasks = useTaskStore((state) => state.tasks);
+ const currentFilter = useTaskStore((state) => state.currentFilter);
+
+ const filteredTasks = tasks.filter((task) => {
+ if (currentFilter === "completed") return task.completed === true;
+ if (currentFilter === "pending") return task.completed === false;
+ return true;
+ });
+
+ return (
+
+ {filteredTasks.map((task) => (
+
+ ))}
+
+ );
+};
+
+export default TaskList;
diff --git a/src/components/TaskManager/index.tsx b/src/components/TaskManager/index.tsx
new file mode 100644
index 0000000..e1ee55b
--- /dev/null
+++ b/src/components/TaskManager/index.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import TaskList from "./TaskList";
+import TaskForm from "./TaskForm";
+import TaskFilter from "./TaskFilter";
+import DeleteTaskModal from "./DeleteTaskModal";
+
+const TaskManager = () => (
+
+
+
+
+
+
+);
+
+export default TaskManager;
diff --git a/src/store/taskStore.ts b/src/store/taskStore.ts
new file mode 100644
index 0000000..dc35188
--- /dev/null
+++ b/src/store/taskStore.ts
@@ -0,0 +1,55 @@
+import { create } from "zustand";
+import { Filter } from "../types/filter";
+import { Task } from "../types/task";
+
+type TaskStore = {
+ tasks: Task[];
+ currentFilter: Filter;
+ deletingTask: Task | null;
+ addTask: (title: string) => void;
+ deleteTask: (id: number) => void;
+ toggleTaskCompletion: (id: number) => void;
+ setFilter: (filter: Filter) => void;
+ setDeletingTask: (id: Task | null) => void;
+};
+
+const useTaskStore = create()((set) => ({
+ tasks: JSON.parse(localStorage.getItem("tasks") || "null") || [
+ { id: 1, title: "Buy groceries", completed: false },
+ { id: 2, title: "Clean the house", completed: true },
+ ],
+ currentFilter: "all",
+ deletingTask: null,
+ addTask: (title) =>
+ set((state) => {
+ const updatedTasks = [
+ ...state.tasks,
+ { id: state.tasks.length + 1, title, completed: false },
+ ];
+ localStorage.setItem("tasks", JSON.stringify(updatedTasks));
+
+ return {
+ tasks: updatedTasks,
+ };
+ }),
+ deleteTask: (id) =>
+ set((state) => {
+ const updatedTasks = state.tasks.filter((task) => task.id !== id);
+ localStorage.setItem("tasks", JSON.stringify(updatedTasks));
+ return { tasks: updatedTasks, deletingTask: null };
+ }),
+ toggleTaskCompletion: (id) =>
+ set((state) => {
+ const updatedTasks = state.tasks.map((task) =>
+ task.id === id ? { ...task, completed: !task.completed } : task
+ );
+ localStorage.setItem("tasks", JSON.stringify(updatedTasks));
+ return {
+ tasks: updatedTasks,
+ };
+ }),
+ setFilter: (filter) => set(() => ({ currentFilter: filter })),
+ setDeletingTask: (task) => set(() => ({ deletingTask: task })),
+}));
+
+export default useTaskStore;
diff --git a/src/types/filter.ts b/src/types/filter.ts
new file mode 100644
index 0000000..3e6cdcf
--- /dev/null
+++ b/src/types/filter.ts
@@ -0,0 +1 @@
+export type Filter = "all" | "completed" | "pending";
diff --git a/src/types/task.ts b/src/types/task.ts
new file mode 100644
index 0000000..66e58f7
--- /dev/null
+++ b/src/types/task.ts
@@ -0,0 +1,5 @@
+export type Task = {
+ id: number;
+ title: string;
+ completed: boolean;
+};