From 942b37ec529dc30c8ed18ebb7967a95f8d0db524 Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Thu, 27 Feb 2025 02:36:30 -0300 Subject: [PATCH 01/11] Fix: correct filter condition (#1) --- src/components/TaskManager.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/TaskManager.tsx b/src/components/TaskManager.tsx index 7b280e8..cd74a91 100644 --- a/src/components/TaskManager.tsx +++ b/src/components/TaskManager.tsx @@ -10,10 +10,9 @@ const TaskManager = () => { 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; + if (filter === "completed") return task.completed === true; + if (filter === "pending") return task.completed === false; return true; }); From 6a617cc00f07d5e0478a2a9d70ed20b86c8c724b Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Thu, 27 Feb 2025 03:17:57 -0300 Subject: [PATCH 02/11] Fix deletion button not updating the UI immediately (#2) --- src/components/TaskManager.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/TaskManager.tsx b/src/components/TaskManager.tsx index cd74a91..98c9d44 100644 --- a/src/components/TaskManager.tsx +++ b/src/components/TaskManager.tsx @@ -28,13 +28,8 @@ const TaskManager = () => { 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); - } + setTasks(tasks.filter((task) => task.id !== id)); }; const toggleTaskCompletion = (id: number) => { From b230ef8fefee86eda2507d6eacba4b737627db30 Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Fri, 28 Feb 2025 02:28:46 -0300 Subject: [PATCH 03/11] Fix style inconsistencies (#3) * fix: use tailwind styles for delete button * fix: rounded button borders --- src/components/TaskItem.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index 6c2a176..b74d1ad 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -14,12 +14,7 @@ const TaskItem = ({ task, onDelete, onToggle }: any) => { From edc9a0c9998b1e6e6a54128ceb9026f269eb7963 Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Fri, 28 Feb 2025 03:45:38 -0300 Subject: [PATCH 04/11] Ensure robust type safety and address UI issues (#4) * feat: ensure robust types, fix naming in object keys and rename state variables * fix: invert task completed condition --- src/components/TaskItem.tsx | 11 +++++++++-- src/components/TaskManager.tsx | 30 +++++++++++++++++------------- src/types/task.ts | 5 +++++ 3 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/types/task.ts diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index b74d1ad..0405473 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -1,12 +1,19 @@ import React from "react"; +import { Task } from "../types/task"; -const TaskItem = ({ task, onDelete, onToggle }: any) => { +type Props = { + task: Task; + onDelete: (id: number) => void; + onToggle: (id: number) => void; +}; + +const TaskItem = ({ task, onDelete, onToggle }: Props) => { return (
  • onToggle(task.id)} className={`cursor-pointer ${ - task.isCompleted ? "text-black" : "line-through text-green-500" + task.completed ? "line-through text-green-500" : "text-black" }`} > {task.title} diff --git a/src/components/TaskManager.tsx b/src/components/TaskManager.tsx index 98c9d44..3cc731e 100644 --- a/src/components/TaskManager.tsx +++ b/src/components/TaskManager.tsx @@ -1,14 +1,17 @@ import React, { useState } from "react"; import TaskItem from "./TaskItem"; +import { Task } from "../types/task"; + +type Filter = "all" | "completed" | "pending"; const TaskManager = () => { - const [tasks, setTasks] = useState([ + 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(); + const [filter, setFilter] = useState("all"); + const [newTaskTitle, setNewTaskTitle] = useState(""); const filteredTasks = tasks.filter((task) => { if (filter === "completed") return task.completed === true; @@ -18,14 +21,14 @@ const TaskManager = () => { const handleAddTask = (e: React.FormEvent) => { e.preventDefault(); - if (newTask!.trim() === "") return; - const newTaskObj = { + if (newTaskTitle.trim() === "") return; + const newTask: Task = { id: tasks.length + 1, - name: newTask, + title: newTaskTitle, completed: false, }; - setTasks([...tasks, newTaskObj]); - setNewTask(""); + setTasks([...tasks, newTask]); + setNewTaskTitle(""); }; const handleDeleteTask = (id: number) => { @@ -33,9 +36,10 @@ const TaskManager = () => { }; const toggleTaskCompletion = (id: number) => { - const task = tasks.find((task) => task.id === id); - - task.isCompleted = !task.isCompleted; + const updatedTasks = tasks.map((task) => + task.id === id ? { ...task, completed: !task.completed } : task + ); + setTasks(updatedTasks); }; return ( @@ -44,8 +48,8 @@ const TaskManager = () => { setNewTask(e.target.value)} + value={newTaskTitle} + onChange={(e) => setNewTaskTitle(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/TaskFilter.tsx b/src/components/TaskManager/TaskFilter.tsx new file mode 100644 index 0000000..f02dfd0 --- /dev/null +++ b/src/components/TaskManager/TaskFilter.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Filter } from "../../types/filter"; + +type Props = { + currentFilter: Filter; + setFilter: (currentFilter: Filter) => void; +}; + +const TaskFilter = ({ currentFilter, setFilter }: Props) => { + 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..3269a5a --- /dev/null +++ b/src/components/TaskManager/TaskForm.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +type Props = { + newTaskTitle: string; + setNewTaskTitle: (title: string) => void; + addTask: (e: React.FormEvent) => void; +}; + +const TaskForm = ({ newTaskTitle, setNewTaskTitle, addTask }: Props) => ( +
    + setNewTaskTitle(e.target.value)} + className="flex-grow border rounded-l py-2 px-3" + /> + +
    +); + +export default TaskForm; diff --git a/src/components/TaskItem.tsx b/src/components/TaskManager/TaskList/TaskItem.tsx similarity index 93% rename from src/components/TaskItem.tsx rename to src/components/TaskManager/TaskList/TaskItem.tsx index 0405473..daf0f43 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskManager/TaskList/TaskItem.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Task } from "../types/task"; +import { Task } from "../../../types/task"; type Props = { task: Task; diff --git a/src/components/TaskManager/TaskList/index.tsx b/src/components/TaskManager/TaskList/index.tsx new file mode 100644 index 0000000..631afa5 --- /dev/null +++ b/src/components/TaskManager/TaskList/index.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { Task } from "../../../types/task"; +import TaskItem from "./TaskItem"; + +type Props = { + tasks: Task[]; + onDelete: (id: number) => void; + onToggle: (id: number) => void; +}; + +const TaskList = ({ tasks, onDelete, onToggle }: Props) => ( +
      + {tasks.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..bb9856d --- /dev/null +++ b/src/components/TaskManager/index.tsx @@ -0,0 +1,62 @@ +import React, { useState } from "react"; +import { Task } from "../../types/task"; +import { Filter } from "../../types/filter"; +import TaskList from "./TaskList"; +import TaskForm from "./TaskForm"; +import TaskFilter from "./TaskFilter"; + +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 [newTaskTitle, setNewTaskTitle] = useState(""); + + const filteredTasks = tasks.filter((task) => { + if (filter === "completed") return task.completed === true; + if (filter === "pending") return task.completed === false; + return true; + }); + + const handleAddTask = (e: React.FormEvent) => { + e.preventDefault(); + if (newTaskTitle.trim() === "") return; + const newTask: Task = { + id: tasks.length + 1, + title: newTaskTitle, + completed: false, + }; + setTasks([...tasks, newTask]); + setNewTaskTitle(""); + }; + + const handleDeleteTask = (id: number) => { + setTasks(tasks.filter((task) => task.id !== id)); + }; + + const toggleTaskCompletion = (id: number) => { + const updatedTasks = tasks.map((task) => + task.id === id ? { ...task, completed: !task.completed } : task + ); + setTasks(updatedTasks); + }; + + return ( +
    + + + +
    + ); +}; + +export default TaskManager; 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"; From b0216fe26669d8fa97f34c21c981ec6e6e51a598 Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Fri, 28 Feb 2025 05:20:42 -0300 Subject: [PATCH 06/11] Zustand implementation (#6) * feat: add zustand store for current filter * feat: use zustand for data * fix: avoid creating empty tasks * fix: use state variable for new task title --- package.json | 11 ++-- pnpm-lock.yaml | 26 ++++++++ src/components/TaskManager/TaskFilter.tsx | 10 ++- src/components/TaskManager/TaskForm.tsx | 47 ++++++++------ .../TaskManager/TaskList/TaskItem.tsx | 14 ++-- src/components/TaskManager/TaskList/index.tsx | 36 +++++------ src/components/TaskManager/index.tsx | 64 +++---------------- src/store/taskStore.ts | 38 +++++++++++ 8 files changed, 136 insertions(+), 110 deletions(-) create mode 100644 src/store/taskStore.ts 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/TaskManager/TaskFilter.tsx b/src/components/TaskManager/TaskFilter.tsx index f02dfd0..4876afb 100644 --- a/src/components/TaskManager/TaskFilter.tsx +++ b/src/components/TaskManager/TaskFilter.tsx @@ -1,12 +1,10 @@ import React from "react"; import { Filter } from "../../types/filter"; +import useTaskStore from "../../store/taskStore"; -type Props = { - currentFilter: Filter; - setFilter: (currentFilter: Filter) => void; -}; - -const TaskFilter = ({ currentFilter, setFilter }: Props) => { +const TaskFilter = () => { + const currentFilter = useTaskStore((state) => state.currentFilter); + const setFilter = useTaskStore((state) => state.setFilter); const filters: Filter[] = ["all", "completed", "pending"]; return ( diff --git a/src/components/TaskManager/TaskForm.tsx b/src/components/TaskManager/TaskForm.tsx index 3269a5a..7ca21b7 100644 --- a/src/components/TaskManager/TaskForm.tsx +++ b/src/components/TaskManager/TaskForm.tsx @@ -1,24 +1,31 @@ -import React from "react"; +import React, { useState } from "react"; +import useTaskStore from "../../store/taskStore"; -type Props = { - newTaskTitle: string; - setNewTaskTitle: (title: string) => void; - addTask: (e: React.FormEvent) => void; -}; +const TaskForm = () => { + const addTask = useTaskStore((state) => state.addTask); + const [newTaskTitle, setNewTaskTitle] = useState(""); + + const handleAddTask = (e: React.FormEvent) => { + e.preventDefault(); + if (newTaskTitle.trim() === "") return; + addTask(newTaskTitle); + setNewTaskTitle(""); + }; -const TaskForm = ({ newTaskTitle, setNewTaskTitle, addTask }: Props) => ( -
    - setNewTaskTitle(e.target.value)} - className="flex-grow border rounded-l py-2 px-3" - /> - -
    -); + 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 index daf0f43..32be035 100644 --- a/src/components/TaskManager/TaskList/TaskItem.tsx +++ b/src/components/TaskManager/TaskList/TaskItem.tsx @@ -1,17 +1,21 @@ import React from "react"; import { Task } from "../../../types/task"; +import useTaskStore from "../../../store/taskStore"; type Props = { task: Task; - onDelete: (id: number) => void; - onToggle: (id: number) => void; }; -const TaskItem = ({ task, onDelete, onToggle }: Props) => { +const TaskItem = ({ task }: Props) => { + const toggleTaskCompletion = useTaskStore( + (state) => state.toggleTaskCompletion + ); + const deleteTask = useTaskStore((state) => state.deleteTask); + return (
  • onToggle(task.id)} + onClick={() => toggleTaskCompletion(task.id)} className={`cursor-pointer ${ task.completed ? "line-through text-green-500" : "text-black" }`} @@ -20,7 +24,7 @@ const TaskItem = ({ task, onDelete, onToggle }: Props) => { + + + + + ); +}; + +export default DeleteTaskModal; diff --git a/src/components/TaskManager/TaskList/TaskItem.tsx b/src/components/TaskManager/TaskList/TaskItem.tsx index 32be035..d84c7ba 100644 --- a/src/components/TaskManager/TaskList/TaskItem.tsx +++ b/src/components/TaskManager/TaskList/TaskItem.tsx @@ -2,34 +2,32 @@ import React from "react"; import { Task } from "../../../types/task"; import useTaskStore from "../../../store/taskStore"; -type Props = { - task: Task; -}; - -const TaskItem = ({ task }: Props) => { +const TaskItem = ({ task }: { task: Task }) => { const toggleTaskCompletion = useTaskStore( (state) => state.toggleTaskCompletion ); - const deleteTask = useTaskStore((state) => state.deleteTask); + const setDeletingTask = useTaskStore((state) => state.setDeletingTask); return ( -
  • - toggleTaskCompletion(task.id)} - className={`cursor-pointer ${ - task.completed ? "line-through text-green-500" : "text-black" - }`} - > - {task.title} - + <> +
  • + toggleTaskCompletion(task.id)} + className={`cursor-pointer ${ + task.completed ? "line-through text-green-500" : "text-black" + }`} + > + {task.title} + - -
  • + + + ); }; diff --git a/src/components/TaskManager/index.tsx b/src/components/TaskManager/index.tsx index 33d3978..e1ee55b 100644 --- a/src/components/TaskManager/index.tsx +++ b/src/components/TaskManager/index.tsx @@ -2,12 +2,14 @@ import React from "react"; import TaskList from "./TaskList"; import TaskForm from "./TaskForm"; import TaskFilter from "./TaskFilter"; +import DeleteTaskModal from "./DeleteTaskModal"; const TaskManager = () => (
    +
    ); diff --git a/src/store/taskStore.ts b/src/store/taskStore.ts index ce32dcb..5cadeac 100644 --- a/src/store/taskStore.ts +++ b/src/store/taskStore.ts @@ -5,10 +5,12 @@ 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) => ({ @@ -17,7 +19,8 @@ const useTaskStore = create()((set) => ({ { id: 2, title: "Clean the house", completed: true }, ], currentFilter: "all", - addTask: (title: string) => + deletingTask: null, + addTask: (title) => set((state) => { const updatedTasks = [ ...state.tasks, @@ -28,13 +31,13 @@ const useTaskStore = create()((set) => ({ tasks: updatedTasks, }; }), - deleteTask: (id: number) => + deleteTask: (id) => set((state) => { const updatedTasks = state.tasks.filter((task) => task.id !== id); localStorage.setItem("tasks", JSON.stringify(updatedTasks)); - return { tasks: updatedTasks }; + return { tasks: updatedTasks, deletingTask: null }; }), - toggleTaskCompletion: (id: number) => + toggleTaskCompletion: (id) => set((state) => { const updatedTasks = state.tasks.map((task) => task.id === id ? { ...task, completed: !task.completed } : task @@ -44,7 +47,8 @@ const useTaskStore = create()((set) => ({ tasks: updatedTasks, }; }), - setFilter: (filter: Filter) => set(() => ({ currentFilter: filter })), + setFilter: (filter) => set(() => ({ currentFilter: filter })), + setDeletingTask: (task) => set(() => ({ deletingTask: task })), })); export default useTaskStore; From 7606c03a294336f0622dd8531ec3cc6434489e89 Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Fri, 28 Feb 2025 06:29:13 -0300 Subject: [PATCH 09/11] Add deployment workflow for Vercel (#9) --- .github/workflows/production.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/production.yaml diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml new file mode 100644 index 0000000..727b98b --- /dev/null +++ b/.github/workflows/production.yaml @@ -0,0 +1,21 @@ +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: 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 }} From af5f39abe364fb3a64bc7dab8a67add702ee4dd1 Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Fri, 28 Feb 2025 06:52:43 -0300 Subject: [PATCH 10/11] Fix deploy (#10) --- .github/workflows/production.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/production.yaml b/.github/workflows/production.yaml index 727b98b..c7eb41f 100644 --- a/.github/workflows/production.yaml +++ b/.github/workflows/production.yaml @@ -10,6 +10,9 @@ 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 From 4344727afb3faada517bbbabfefd3e585c311000 Mon Sep 17 00:00:00 2001 From: Leandro Cotti <52415035+Lefcott@users.noreply.github.com> Date: Fri, 28 Feb 2025 07:13:16 -0300 Subject: [PATCH 11/11] Improve UI (#11) * feat: add select-none in order to not select the text of the task when clicking multiple times * feat: avoid creating repeated tasks * feat: add some background to the tabs * feat: add transition duration to tabs * feat: add little :active animation to tasks --- src/components/TaskManager/TaskFilter.tsx | 4 ++-- src/components/TaskManager/TaskForm.tsx | 6 ++++++ src/components/TaskManager/TaskList/TaskItem.tsx | 2 +- src/store/taskStore.ts | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/TaskManager/TaskFilter.tsx b/src/components/TaskManager/TaskFilter.tsx index 4876afb..1b65af1 100644 --- a/src/components/TaskManager/TaskFilter.tsx +++ b/src/components/TaskManager/TaskFilter.tsx @@ -13,8 +13,8 @@ const TaskFilter = () => {