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 ( -
    -
    - 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/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 ( +
    + setNewTaskTitle(e.target.value)} + className="flex-grow border rounded-l py-2 px-3" + /> + +
    + ); +}; + +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; +};