From 9dbfafe444291b85d78d728e24357e2828feb138 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 17:08:54 -0600 Subject: [PATCH 01/18] generated package files --- package-lock.json | 9 +++++---- package.json | 14 +++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1d49e1..65fd822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", - "@types/react": "^18.2.66", + "@types/react": "^18.3.24", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", @@ -1568,10 +1568,11 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.23", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", - "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "version": "18.3.24", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", + "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" diff --git a/package.json b/package.json index 1a66568..13a93b3 100644 --- a/package.json +++ b/package.json @@ -15,24 +15,24 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.2.66", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.3.24", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/ui": "^1.4.0", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "jsdom": "^24.0.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.3", "typescript": "^5.2.2", "vite": "^5.2.0", - "vitest": "^1.4.0", - "@vitest/ui": "^1.4.0", - "@testing-library/react": "^14.2.1", - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/user-event": "^14.5.2", - "jsdom": "^24.0.0" + "vitest": "^1.4.0" } } From d5d0e4d0410bda01c0defddb035aab10fece97fc Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 17:09:14 -0600 Subject: [PATCH 02/18] Basic component structure and rendering columns --- src/ChallengeComponent.tsx | 38 ++++++++++++++++++++++++++----- src/components/CreateTaskForm.tsx | 6 +++++ src/components/TaskCard.tsx | 10 ++++++++ src/components/TaskColumn.tsx | 24 +++++++++++++++++++ src/services/todoService.tsx | 1 + src/types.ts | 9 ++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/components/CreateTaskForm.tsx create mode 100644 src/components/TaskCard.tsx create mode 100644 src/components/TaskColumn.tsx create mode 100644 src/services/todoService.tsx create mode 100644 src/types.ts diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 2344883..2fbdbbd 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,10 +1,36 @@ +import { TaskColumn } from "./components/TaskColumn"; +import { TodoStatus, TodoStatusArray, Task } from "./types"; + +const COLUMN_IDS: TodoStatusArray = ["todo", "inProgress", "done"]; +const COLUMN_LABELS: Record = { + todo: "To Do", + inProgress: "In Progress", + done: "Done", +}; + export function ChallengeComponent() { return ( - <> - {/* Delete this h2, and add your own code here. */} -

- Your code goes here -

- +
+ {COLUMN_IDS.map((column_id) => ( + + ))} +
); } + +// TODO: +// Categories: iterate and render on field +// TaskCards: iterate on Categories +// TaskCard update +// const testTasks = [ +// { +// id: "1", +// title: "Do laundry", +// description: "Don't forget to run the dryer this time", +// status: "todo", +// }, +// ] as Task[]; diff --git a/src/components/CreateTaskForm.tsx b/src/components/CreateTaskForm.tsx new file mode 100644 index 0000000..94b6fde --- /dev/null +++ b/src/components/CreateTaskForm.tsx @@ -0,0 +1,6 @@ +// interface CreateTaskFormProps { +// } + +// export function TaskCard({}: CreateTaskFormProps) { +// return; +// } diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx new file mode 100644 index 0000000..a490949 --- /dev/null +++ b/src/components/TaskCard.tsx @@ -0,0 +1,10 @@ +// import { Task } from "../types"; + +// interface TaskColumnProps { +// category: string; +// task: Task; +// } + +// export function TaskCard({ category, task }: TaskColumnProps) { +// return; +// } diff --git a/src/components/TaskColumn.tsx b/src/components/TaskColumn.tsx new file mode 100644 index 0000000..03f069e --- /dev/null +++ b/src/components/TaskColumn.tsx @@ -0,0 +1,24 @@ +import { Task } from "../types"; + +interface TaskColumnProps { + title: string; + tasks: Task[]; +} + +export function TaskColumn({ title, tasks }: TaskColumnProps) { + return ( +
+

{title}

+
    + {tasks.map((task) => ( +
  • +

    {task.description}

    +
  • + ))} +
+
+ ); +} diff --git a/src/services/todoService.tsx b/src/services/todoService.tsx new file mode 100644 index 0000000..6a999fc --- /dev/null +++ b/src/services/todoService.tsx @@ -0,0 +1 @@ +// TODO: add connection to "api" here diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9f5a55e --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export type TodoStatus = "todo" | "inProgress" | "done"; +export type TodoStatusArray = TodoStatus[]; + +export interface Task { + id: string; + title: string; + description: string; + status: TodoStatus; +} From 33d2b2981be3c6b60d6e9058ee5e7b80a2d9f937 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 17:52:51 -0600 Subject: [PATCH 03/18] task filtering and test config --- src/helpers/tasks.ts | 8 ++++++++ test/helpers/tasks.test.tsx | 33 +++++++++++++++++++++++++++++++++ tsconfig.json | 8 ++++++-- vite.config.ts | 6 ++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/helpers/tasks.ts create mode 100644 test/helpers/tasks.test.tsx diff --git a/src/helpers/tasks.ts b/src/helpers/tasks.ts new file mode 100644 index 0000000..04ab5e7 --- /dev/null +++ b/src/helpers/tasks.ts @@ -0,0 +1,8 @@ +import { Task, TaskStatus } from "../types"; + +export const filterByStatus = ( + allTasks: Task[], + status: TaskStatus +): Task[] => { + return allTasks.filter((task) => task.status === status); +}; diff --git a/test/helpers/tasks.test.tsx b/test/helpers/tasks.test.tsx new file mode 100644 index 0000000..8246939 --- /dev/null +++ b/test/helpers/tasks.test.tsx @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { filterByStatus } from "@/helpers/tasks"; +import { Task, TaskStatus } from "@/types"; + +describe("filterByStatus", () => { + const sampleTasks: Task[] = [ + { id: "1", title: "Task 1", description: "Do the thing", status: "todo" }, + { + id: "2", + title: "Task 2", + description: "Do the thing", + status: "inProgress", + }, + { id: "3", title: "Task 3", description: "Do the thing", status: "done" }, + { id: "4", title: "Task 4", description: "Do the thing", status: "todo" }, + ]; + + it.each([ + ["todo", ["1", "4"]], + ["inProgress", ["2"]], + ["done", ["3"]], + ] as [TaskStatus, string[]][])( + "returns only tasks with corredsponding status", + (status, expectedIds) => { + const result = filterByStatus(sampleTasks, status); + expect(result.map((t) => t.id)).toEqual(expectedIds); + } + ); + + it("returns an empty array when given an empty task list", () => { + expect(filterByStatus([], "todo")).toEqual([]); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..6b44549 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,8 +18,12 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, - "include": ["src"], + "include": ["src", "test"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/vite.config.ts b/vite.config.ts index 279c5d2..4b31cad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import path from "path"; // https://vitejs.dev/config/ export default defineConfig({ @@ -8,6 +9,11 @@ export default defineConfig({ port: 3000, open: true, }, + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, test: { globals: true, environment: "jsdom", From 43354b9b15b79b4c7355113cb9029e8b9d34760a Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 17:53:30 -0600 Subject: [PATCH 04/18] Basic task rendering --- src/ChallengeComponent.tsx | 26 ++++++++++++++++++++------ src/components/TaskCard.tsx | 25 +++++++++++++++++-------- src/components/TaskColumn.tsx | 13 +++++++++---- src/types.ts | 6 +++--- 4 files changed, 49 insertions(+), 21 deletions(-) diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 2fbdbbd..98fcf75 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,8 +1,8 @@ import { TaskColumn } from "./components/TaskColumn"; -import { TodoStatus, TodoStatusArray, Task } from "./types"; +import { Task, TaskStatus, TaskStatusArray } from "./types"; -const COLUMN_IDS: TodoStatusArray = ["todo", "inProgress", "done"]; -const COLUMN_LABELS: Record = { +const COLUMN_IDS: TaskStatusArray = ["todo", "inProgress", "done"]; +const COLUMN_LABELS: Record = { todo: "To Do", inProgress: "In Progress", done: "Done", @@ -11,10 +11,11 @@ const COLUMN_LABELS: Record = { export function ChallengeComponent() { return (
- {COLUMN_IDS.map((column_id) => ( + {COLUMN_IDS.map((columnId) => ( ))} @@ -26,6 +27,7 @@ export function ChallengeComponent() { // Categories: iterate and render on field // TaskCards: iterate on Categories // TaskCard update + // const testTasks = [ // { // id: "1", @@ -33,4 +35,16 @@ export function ChallengeComponent() { // description: "Don't forget to run the dryer this time", // status: "todo", // }, +// { +// id: "2", +// title: "Do dishes", +// description: "Handwash the skillet", +// status: "inProgress", +// }, +// { +// id: "3", +// title: "Nap", +// description: "Need I say more?", +// status: "done", +// }, // ] as Task[]; diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx index a490949..558f62f 100644 --- a/src/components/TaskCard.tsx +++ b/src/components/TaskCard.tsx @@ -1,10 +1,19 @@ -// import { Task } from "../types"; +import { Task, TaskStatus } from "../types"; -// interface TaskColumnProps { -// category: string; -// task: Task; -// } +interface TaskColumnProps { + task: Task; +} -// export function TaskCard({ category, task }: TaskColumnProps) { -// return; -// } +export function TaskCard({ task }: TaskColumnProps) { + const { title, description, status } = task; + + return ( +
+
+

{title}

+

{status}

+
+

{description}

+
+ ); +} diff --git a/src/components/TaskColumn.tsx b/src/components/TaskColumn.tsx index 03f069e..c57ede7 100644 --- a/src/components/TaskColumn.tsx +++ b/src/components/TaskColumn.tsx @@ -1,21 +1,26 @@ -import { Task } from "../types"; +import { Task, TaskStatus } from "../types"; +import { TaskCard } from "./TaskCard"; +import { filterByStatus } from "../helpers/tasks"; interface TaskColumnProps { + columnStatus: TaskStatus; title: string; tasks: Task[]; } -export function TaskColumn({ title, tasks }: TaskColumnProps) { +export function TaskColumn({ title, tasks, columnStatus }: TaskColumnProps) { + const filteredTasks = filterByStatus(tasks, columnStatus); + return (

{title}

    - {tasks.map((task) => ( + {filteredTasks.map((task) => (
  • -

    {task.description}

    +
  • ))}
diff --git a/src/types.ts b/src/types.ts index 9f5a55e..9dbd4f3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,9 @@ -export type TodoStatus = "todo" | "inProgress" | "done"; -export type TodoStatusArray = TodoStatus[]; +export type TaskStatus = "todo" | "inProgress" | "done"; +export type TaskStatusArray = TaskStatus[]; export interface Task { id: string; title: string; description: string; - status: TodoStatus; + status: TaskStatus; } From cf5a52e5b804503d241ac9bd5120255cbeb6ec77 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 17:53:54 -0600 Subject: [PATCH 05/18] Package file bump --- package-lock.json | 18 ++++++++++++++++++ package.json | 1 + 2 files changed, 19 insertions(+) diff --git a/package-lock.json b/package-lock.json index 65fd822..257fef1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", + "@types/node": "^24.5.2", "@types/react": "^18.3.24", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", @@ -1561,6 +1562,16 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -5980,6 +5991,13 @@ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "dev": true }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", diff --git a/package.json b/package.json index 13a93b3..ce19b9c 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", + "@types/node": "^24.5.2", "@types/react": "^18.3.24", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", From 4f166a3226c5fc974fbd3b1b5f73dd698e453eb1 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 18:16:07 -0600 Subject: [PATCH 06/18] clean up task filtering and create mock tasks service --- src/ChallengeComponent.tsx | 30 +++++++----------------------- src/components/TaskColumn.tsx | 7 ++----- src/services/tasksService.tsx | 25 +++++++++++++++++++++++++ src/services/todoService.tsx | 1 - 4 files changed, 34 insertions(+), 29 deletions(-) create mode 100644 src/services/tasksService.tsx delete mode 100644 src/services/todoService.tsx diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 98fcf75..df03b10 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,5 +1,8 @@ +import { useState } from "react"; import { TaskColumn } from "./components/TaskColumn"; import { Task, TaskStatus, TaskStatusArray } from "./types"; +import { getTasks } from "./services/tasksService"; +import { filterByStatus } from "./helpers/tasks"; const COLUMN_IDS: TaskStatusArray = ["todo", "inProgress", "done"]; const COLUMN_LABELS: Record = { @@ -9,14 +12,16 @@ const COLUMN_LABELS: Record = { }; export function ChallengeComponent() { + const mockTasks = getTasks(); + const [tasks, setTasks] = useState(mockTasks); + return (
{COLUMN_IDS.map((columnId) => ( ))}
@@ -27,24 +32,3 @@ export function ChallengeComponent() { // Categories: iterate and render on field // TaskCards: iterate on Categories // TaskCard update - -// const testTasks = [ -// { -// id: "1", -// title: "Do laundry", -// description: "Don't forget to run the dryer this time", -// status: "todo", -// }, -// { -// id: "2", -// title: "Do dishes", -// description: "Handwash the skillet", -// status: "inProgress", -// }, -// { -// id: "3", -// title: "Nap", -// description: "Need I say more?", -// status: "done", -// }, -// ] as Task[]; diff --git a/src/components/TaskColumn.tsx b/src/components/TaskColumn.tsx index c57ede7..eb373d7 100644 --- a/src/components/TaskColumn.tsx +++ b/src/components/TaskColumn.tsx @@ -3,19 +3,16 @@ import { TaskCard } from "./TaskCard"; import { filterByStatus } from "../helpers/tasks"; interface TaskColumnProps { - columnStatus: TaskStatus; title: string; tasks: Task[]; } -export function TaskColumn({ title, tasks, columnStatus }: TaskColumnProps) { - const filteredTasks = filterByStatus(tasks, columnStatus); - +export function TaskColumn({ title, tasks }: TaskColumnProps) { return (

{title}

    - {filteredTasks.map((task) => ( + {tasks.map((task) => (
  • { + // TODO: add connection to "api" here + return [ + { + id: "1", + title: "Do laundry", + description: "Don't forget to run the dryer this time", + status: "todo", + }, + { + id: "2", + title: "Do dishes", + description: "Handwash the skillet", + status: "inProgress", + }, + { + id: "3", + title: "Nap", + description: "Need I say more?", + status: "done", + }, + ] as Task[]; +}; diff --git a/src/services/todoService.tsx b/src/services/todoService.tsx deleted file mode 100644 index 6a999fc..0000000 --- a/src/services/todoService.tsx +++ /dev/null @@ -1 +0,0 @@ -// TODO: add connection to "api" here From 14e39b47b6a19e8007d969aebd000c57c8407222 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 18:29:51 -0600 Subject: [PATCH 07/18] State management for changes and create task functionality --- src/ChallengeComponent.tsx | 20 ++++++++---- src/components/CreateTaskForm.tsx | 51 ++++++++++++++++++++++++++++--- src/components/TaskCard.tsx | 2 +- src/components/TaskColumn.tsx | 1 - 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index df03b10..fc48082 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,10 +1,11 @@ import { useState } from "react"; import { TaskColumn } from "./components/TaskColumn"; -import { Task, TaskStatus, TaskStatusArray } from "./types"; +import { TaskStatus, TaskStatusArray } from "./types"; import { getTasks } from "./services/tasksService"; import { filterByStatus } from "./helpers/tasks"; +import { CreateTaskForm } from "./components/CreateTaskForm"; -const COLUMN_IDS: TaskStatusArray = ["todo", "inProgress", "done"]; +const TASK_STATUS_IDS: TaskStatusArray = ["todo", "inProgress", "done"]; const COLUMN_LABELS: Record = { todo: "To Do", inProgress: "In Progress", @@ -17,11 +18,18 @@ export function ChallengeComponent() { return (
    - {COLUMN_IDS.map((columnId) => ( +
    + { + setTasks((prev) => [...prev, { id: crypto.randomUUID(), ...task }]); + }} + /> +
    + {TASK_STATUS_IDS.map((taskStatusId) => ( ))}
    diff --git a/src/components/CreateTaskForm.tsx b/src/components/CreateTaskForm.tsx index 94b6fde..12d0b19 100644 --- a/src/components/CreateTaskForm.tsx +++ b/src/components/CreateTaskForm.tsx @@ -1,6 +1,47 @@ -// interface CreateTaskFormProps { -// } +import { useState, FormEvent } from "react"; +import { Task } from "../types"; -// export function TaskCard({}: CreateTaskFormProps) { -// return; -// } +interface CreateTaskFormProps { + onCreate: (task: Omit) => void; +} + +export function CreateTaskForm({ onCreate }: CreateTaskFormProps) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + onCreate({ + title: title.trim(), + description: description.trim(), + status: "todo", + }); + }; + + return ( +
    + setTitle(e.target.value)} + placeholder="Task title" + aria-label="Task title" + className="border rounded px-2 py-1" + /> + setDescription(e.target.value)} + placeholder="Task description" + aria-label="Task description" + className="border rounded px-2 py-1" + /> + +
    + ); +} diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx index 558f62f..83ba9a4 100644 --- a/src/components/TaskCard.tsx +++ b/src/components/TaskCard.tsx @@ -1,4 +1,4 @@ -import { Task, TaskStatus } from "../types"; +import { Task } from "../types"; interface TaskColumnProps { task: Task; diff --git a/src/components/TaskColumn.tsx b/src/components/TaskColumn.tsx index eb373d7..b81b181 100644 --- a/src/components/TaskColumn.tsx +++ b/src/components/TaskColumn.tsx @@ -1,6 +1,5 @@ import { Task, TaskStatus } from "../types"; import { TaskCard } from "./TaskCard"; -import { filterByStatus } from "../helpers/tasks"; interface TaskColumnProps { title: string; From 2e95c65afcb59724173b6a0fe55245d0cf876bb2 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 19:00:05 -0600 Subject: [PATCH 08/18] Implement task moving method --- src/ChallengeComponent.tsx | 21 ++++++++++++++++++++- src/components/CreateTaskForm.tsx | 4 ++++ src/components/TaskCard.tsx | 28 ++++++++++++++++++++++++---- src/components/TaskColumn.tsx | 9 +++++++-- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index fc48082..8370489 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { TaskColumn } from "./components/TaskColumn"; -import { TaskStatus, TaskStatusArray } from "./types"; +import { Task, TaskStatus, TaskStatusArray } from "./types"; import { getTasks } from "./services/tasksService"; import { filterByStatus } from "./helpers/tasks"; import { CreateTaskForm } from "./components/CreateTaskForm"; @@ -16,6 +16,24 @@ export function ChallengeComponent() { const mockTasks = getTasks(); const [tasks, setTasks] = useState(mockTasks); + const moveTask = (id: string, direction: "previous" | "next") => { + setTasks((prev: Task[]) => + prev.map((task) => { + if (task.id !== id) return task; + + const order: TaskStatus[] = ["todo", "inProgress", "done"]; + let taskStatusIndex = order.indexOf(task.status); + + if (direction === "previous") + taskStatusIndex = Math.max(0, taskStatusIndex - 1); + if (direction === "next") + taskStatusIndex = Math.min(order.length - 1, taskStatusIndex + 1); + + return { ...task, status: order[taskStatusIndex] }; + }) + ); + }; + return (
    @@ -30,6 +48,7 @@ export function ChallengeComponent() { key={taskStatusId} title={COLUMN_LABELS[taskStatusId]} tasks={filterByStatus(tasks, taskStatusId)} + moveTask={moveTask} /> ))}
    diff --git a/src/components/CreateTaskForm.tsx b/src/components/CreateTaskForm.tsx index 12d0b19..5957e0e 100644 --- a/src/components/CreateTaskForm.tsx +++ b/src/components/CreateTaskForm.tsx @@ -16,6 +16,10 @@ export function CreateTaskForm({ onCreate }: CreateTaskFormProps) { description: description.trim(), status: "todo", }); + // Note: if this were to be handled by an HTTP request, we would only reset the + // inputs onSuccess. + setTitle(""); + setDescription(""); }; return ( diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx index 83ba9a4..1713dd4 100644 --- a/src/components/TaskCard.tsx +++ b/src/components/TaskCard.tsx @@ -1,11 +1,16 @@ -import { Task } from "../types"; +import { Task, TaskStatus } from "../types"; -interface TaskColumnProps { +interface TaskCardProps { task: Task; + onMovePrevious?: (taskId: string) => void; + onMoveNext?: (taskId: string) => void; } -export function TaskCard({ task }: TaskColumnProps) { - const { title, description, status } = task; +export function TaskCard({ task, onMovePrevious, onMoveNext }: TaskCardProps) { + const { id, title, description, status } = task; + + const statusOrder: TaskStatus[] = ["todo", "inProgress", "done"]; + const currentIndex = statusOrder.indexOf(status); return (
    @@ -14,6 +19,21 @@ export function TaskCard({ task }: TaskColumnProps) {

    {status}

    {description}

    +
    + + +
    ); } diff --git a/src/components/TaskColumn.tsx b/src/components/TaskColumn.tsx index b81b181..b6630cc 100644 --- a/src/components/TaskColumn.tsx +++ b/src/components/TaskColumn.tsx @@ -4,9 +4,10 @@ import { TaskCard } from "./TaskCard"; interface TaskColumnProps { title: string; tasks: Task[]; + moveTask: (id: string, direction: "previous" | "next") => void; } -export function TaskColumn({ title, tasks }: TaskColumnProps) { +export function TaskColumn({ title, tasks, moveTask }: TaskColumnProps) { return (

    {title}

    @@ -16,7 +17,11 @@ export function TaskColumn({ title, tasks }: TaskColumnProps) { key={task.id} className="flex justify-between items-center p-2 bg-gray-100 rounded" > - + moveTask(id, "previous")} + onMoveNext={(id) => moveTask(id, "next")} + />
  • ))}
From 23b042227101f24dd9d725d96e057678ee8e38a6 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Sun, 28 Sep 2025 19:06:51 -0600 Subject: [PATCH 09/18] constants cleanup and styling --- src/ChallengeComponent.tsx | 15 ++------------- src/components/TaskCard.tsx | 25 ++++++++++++++++++------- src/components/TaskColumn.tsx | 13 ++++++------- src/utils/constants.ts | 8 ++++++++ 4 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 src/utils/constants.ts diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 8370489..abc0e00 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,16 +1,10 @@ import { useState } from "react"; import { TaskColumn } from "./components/TaskColumn"; -import { Task, TaskStatus, TaskStatusArray } from "./types"; +import { Task, TaskStatus } from "./types"; import { getTasks } from "./services/tasksService"; import { filterByStatus } from "./helpers/tasks"; import { CreateTaskForm } from "./components/CreateTaskForm"; - -const TASK_STATUS_IDS: TaskStatusArray = ["todo", "inProgress", "done"]; -const COLUMN_LABELS: Record = { - todo: "To Do", - inProgress: "In Progress", - done: "Done", -}; +import { COLUMN_LABELS, TASK_STATUS_IDS } from "./utils/constants"; export function ChallengeComponent() { const mockTasks = getTasks(); @@ -54,8 +48,3 @@ export function ChallengeComponent() {
); } - -// TODO: -// Categories: iterate and render on field -// TaskCards: iterate on Categories -// TaskCard update diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx index 1713dd4..a921e7d 100644 --- a/src/components/TaskCard.tsx +++ b/src/components/TaskCard.tsx @@ -1,3 +1,4 @@ +import { COLUMN_LABELS } from "@/utils/constants"; import { Task, TaskStatus } from "../types"; interface TaskCardProps { @@ -12,22 +13,32 @@ export function TaskCard({ task, onMovePrevious, onMoveNext }: TaskCardProps) { const statusOrder: TaskStatus[] = ["todo", "inProgress", "done"]; const currentIndex = statusOrder.indexOf(status); + // Map status to background colors + const statusColors: Record = { + todo: "bg-blue-100", + inProgress: "bg-yellow-100", + done: "bg-green-100", + }; + return ( -
-
-

{title}

-

{status}

+
+
+

{title}

+

{COLUMN_LABELS[status]}

-

{description}

-
+

{description}

+
diff --git a/src/services/tasksService.tsx b/src/services/tasksService.tsx index abd9905..aad02b3 100644 --- a/src/services/tasksService.tsx +++ b/src/services/tasksService.tsx @@ -1,7 +1,7 @@ import { Task } from "@/types"; export const getTasks = () => { - // TODO: add connection to "api" here + // TODO: add connection to "api" here if necessary return [ { id: "1", @@ -18,7 +18,7 @@ export const getTasks = () => { { id: "3", title: "Nap", - description: "Need I say more?", + description: "Try to keep it under 3 hours", status: "done", }, ] as Task[]; From 8c8b836888d65b4821136ebba0f23846343ca22b Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Tue, 30 Sep 2025 17:44:14 -0600 Subject: [PATCH 12/18] localstorage task service --- src/ChallengeComponent.tsx | 57 ++++++++++++++---------- src/services/taskService.ts | 81 +++++++++++++++++++++++++++++++++++ src/services/tasksService.tsx | 25 ----------- 3 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 src/services/taskService.ts delete mode 100644 src/services/tasksService.tsx diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index abc0e00..98a681f 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,41 +1,54 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { TaskColumn } from "./components/TaskColumn"; import { Task, TaskStatus } from "./types"; -import { getTasks } from "./services/tasksService"; +import { taskService } from "./services/taskService"; import { filterByStatus } from "./helpers/tasks"; import { CreateTaskForm } from "./components/CreateTaskForm"; import { COLUMN_LABELS, TASK_STATUS_IDS } from "./utils/constants"; export function ChallengeComponent() { - const mockTasks = getTasks(); - const [tasks, setTasks] = useState(mockTasks); + const [tasks, setTasks] = useState([]); - const moveTask = (id: string, direction: "previous" | "next") => { - setTasks((prev: Task[]) => - prev.map((task) => { - if (task.id !== id) return task; + // Load tasks on mount (seed if empty) + useEffect(() => { + (async () => { + let allTasks = await taskService.fetchAll(); + if (allTasks.length === 0) { + allTasks = await taskService.seed(); + } + setTasks(allTasks); + })(); + }, []); - const order: TaskStatus[] = ["todo", "inProgress", "done"]; - let taskStatusIndex = order.indexOf(task.status); + const moveTask = async (id: string, direction: "previous" | "next") => { + const order: TaskStatus[] = ["todo", "inProgress", "done"]; + const task = tasks.find((t) => t.id === id); + if (!task) return; - if (direction === "previous") - taskStatusIndex = Math.max(0, taskStatusIndex - 1); - if (direction === "next") - taskStatusIndex = Math.min(order.length - 1, taskStatusIndex + 1); + let taskStatusIndex = order.indexOf(task.status); + if (direction === "previous") + taskStatusIndex = Math.max(0, taskStatusIndex - 1); + if (direction === "next") + taskStatusIndex = Math.min(order.length - 1, taskStatusIndex + 1); - return { ...task, status: order[taskStatusIndex] }; - }) - ); + const updated = await taskService.update(id, { + status: order[taskStatusIndex], + }); + + if (updated) { + setTasks((prev) => prev.map((t) => (t.id === id ? updated : t))); + } + }; + + const handleCreate = async (task: Omit) => { + const newTask = await taskService.create(task); + setTasks((prev) => [...prev, newTask]); }; return (
- { - setTasks((prev) => [...prev, { id: crypto.randomUUID(), ...task }]); - }} - /> +
{TASK_STATUS_IDS.map((taskStatusId) => ( { + const data = localStorage.getItem(STORAGE_KEY); + return data ? (JSON.parse(data) as Task[]) : []; +}; + +const save = (tasks: Task[]) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)); +}; + +export const taskService = { + seed: () => { + const sampleTasks: Task[] = [ + { + id: "1", + title: "Do laundry", + description: "Don't forget to run the dryer this time", + status: "todo", + }, + { + id: "2", + title: "Do dishes", + description: "Handwash the skillet", + status: "inProgress", + }, + { + id: "3", + title: "Nap", + description: "Try to keep it under 3 hours", + status: "done", + }, + ]; + save(sampleTasks); + return sampleTasks; + }, + + fetchAll: (): Task[] => { + return load(); + }, + + create: (task: Omit): Task => { + const tasks = load(); + const newTask: Task = { id: crypto.randomUUID(), ...task }; + + tasks.push(newTask); + + save(tasks); + return newTask; + }, + + update: (id: string, updates: Partial>): Task | null => { + const tasks = load(); + const index = tasks.findIndex((task) => task.id === id); + + if (index === -1) return null; + + const updatedTask = { ...tasks[index], ...updates }; + + tasks[index] = updatedTask; + save(tasks); + + return updatedTask; + }, + + delete: (id: string): boolean => { + const tasks = load(); + const newTasks = tasks.filter((task) => task.id !== id); + + if (newTasks.length === tasks.length) return false; + save(newTasks); + + return true; + }, + + clear: (): void => { + localStorage.removeItem(STORAGE_KEY); + }, +}; diff --git a/src/services/tasksService.tsx b/src/services/tasksService.tsx deleted file mode 100644 index aad02b3..0000000 --- a/src/services/tasksService.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Task } from "@/types"; - -export const getTasks = () => { - // TODO: add connection to "api" here if necessary - return [ - { - id: "1", - title: "Do laundry", - description: "Don't forget to run the dryer this time", - status: "todo", - }, - { - id: "2", - title: "Do dishes", - description: "Handwash the skillet", - status: "inProgress", - }, - { - id: "3", - title: "Nap", - description: "Try to keep it under 3 hours", - status: "done", - }, - ] as Task[]; -}; From 6c89735bb8669bc394baf6f3c6105a8e3eb05d6b Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Tue, 30 Sep 2025 18:10:29 -0600 Subject: [PATCH 13/18] Add delete and clear functionality with tweaks --- src/ChallengeComponent.tsx | 59 +++++++++++++++++++++++++++-------- src/components/TaskCard.tsx | 48 +++++++++++++++++----------- src/components/TaskColumn.tsx | 9 +++++- 3 files changed, 84 insertions(+), 32 deletions(-) diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 98a681f..8d44293 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -9,9 +9,9 @@ import { COLUMN_LABELS, TASK_STATUS_IDS } from "./utils/constants"; export function ChallengeComponent() { const [tasks, setTasks] = useState([]); - // Load tasks on mount (seed if empty) useEffect(() => { (async () => { + // Currently fetching a seed list of tasks if the task list is empty for demo purposes. let allTasks = await taskService.fetchAll(); if (allTasks.length === 0) { allTasks = await taskService.seed(); @@ -22,7 +22,8 @@ export function ChallengeComponent() { const moveTask = async (id: string, direction: "previous" | "next") => { const order: TaskStatus[] = ["todo", "inProgress", "done"]; - const task = tasks.find((t) => t.id === id); + const task = tasks.find((task) => task.id === id); + if (!task) return; let taskStatusIndex = order.indexOf(task.status); @@ -36,7 +37,7 @@ export function ChallengeComponent() { }); if (updated) { - setTasks((prev) => prev.map((t) => (t.id === id ? updated : t))); + setTasks((prev) => prev.map((task) => (task.id === id ? updated : task))); } }; @@ -45,19 +46,51 @@ export function ChallengeComponent() { setTasks((prev) => [...prev, newTask]); }; + const handleDelete = async (id: string) => { + await taskService.delete(id); + setTasks((prevTasks) => prevTasks.filter((task) => task.id !== id)); + }; + + const handleClear = async () => { + await taskService.clear(); + setTasks([]); + }; + + const handleSeed = async () => { + const seeded = await taskService.seed(); + setTasks(seeded); + }; + return ( -
-
+
+
+ {TASK_STATUS_IDS.map((taskStatusId) => ( + + ))} +
+
+
+ + +
- {TASK_STATUS_IDS.map((taskStatusId) => ( - - ))}
); } diff --git a/src/components/TaskCard.tsx b/src/components/TaskCard.tsx index e833c0c..ec73aa8 100644 --- a/src/components/TaskCard.tsx +++ b/src/components/TaskCard.tsx @@ -5,18 +5,21 @@ interface TaskCardProps { task: Task; onMovePrevious?: (taskId: string) => void; onMoveNext?: (taskId: string) => void; + onDelete?: (taskId: string) => void; } -export function TaskCard({ task, onMovePrevious, onMoveNext }: TaskCardProps) { +export function TaskCard({ + task, + onMovePrevious, + onMoveNext, + onDelete, +}: TaskCardProps) { const { id, title, description, status } = task; const statusOrder: TaskStatus[] = ["todo", "inProgress", "done"]; const currentIndex = statusOrder.indexOf(status); - const isLastStatus = currentIndex === statusOrder.length - 1; - const isFirstStatus = currentIndex === 0; - - const customStatusColors: Record = { + const statusColors: Record = { todo: "bg-blue-100", inProgress: "bg-yellow-100", done: "bg-green-100", @@ -24,28 +27,37 @@ export function TaskCard({ task, onMovePrevious, onMoveNext }: TaskCardProps) { return (

{title}

{COLUMN_LABELS[status]}

{description}

-
- + +
+
+ + +
); diff --git a/src/components/TaskColumn.tsx b/src/components/TaskColumn.tsx index 52cad67..4b6f02e 100644 --- a/src/components/TaskColumn.tsx +++ b/src/components/TaskColumn.tsx @@ -5,9 +5,15 @@ interface TaskColumnProps { title: string; tasks: Task[]; moveTask: (id: string, direction: "previous" | "next") => void; + onDelete: (id: string) => void; } -export function TaskColumn({ title, tasks, moveTask }: TaskColumnProps) { +export function TaskColumn({ + title, + tasks, + moveTask, + onDelete, +}: TaskColumnProps) { return (

@@ -20,6 +26,7 @@ export function TaskColumn({ title, tasks, moveTask }: TaskColumnProps) { task={task} onMovePrevious={(id) => moveTask(id, "previous")} onMoveNext={(id) => moveTask(id, "next")} + onDelete={onDelete} /> ))} From 7ab2a72af05f1f805708142c46ad400fe31013ae Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Tue, 30 Sep 2025 18:13:35 -0600 Subject: [PATCH 14/18] Empty state for columns --- src/components/TaskColumn.tsx | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/TaskColumn.tsx b/src/components/TaskColumn.tsx index 4b6f02e..fdcffc3 100644 --- a/src/components/TaskColumn.tsx +++ b/src/components/TaskColumn.tsx @@ -15,21 +15,25 @@ export function TaskColumn({ onDelete, }: TaskColumnProps) { return ( -
+

{title}

    - {tasks.map((task) => ( -
  • - moveTask(id, "previous")} - onMoveNext={(id) => moveTask(id, "next")} - onDelete={onDelete} - /> -
  • - ))} + {tasks.length === 0 ? ( +
  • No tasks
  • + ) : ( + tasks.map((task) => ( +
  • + moveTask(id, "previous")} + onMoveNext={(id) => moveTask(id, "next")} + onDelete={onDelete} + /> +
  • + )) + )}
); From 9b7f6ff8d54acf069aa2325b5d5bfdaba3685d41 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Tue, 30 Sep 2025 18:30:01 -0600 Subject: [PATCH 15/18] Add tests for task service --- test/services/taskService.test.ts | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 test/services/taskService.test.ts diff --git a/test/services/taskService.test.ts b/test/services/taskService.test.ts new file mode 100644 index 0000000..3e562cc --- /dev/null +++ b/test/services/taskService.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { taskService } from "@/services/taskService"; + +describe("taskService", () => { + const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value.toString(); + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + }, + }; + })(); + + beforeEach(() => { + // @ts-ignore + global.localStorage = localStorageMock; + localStorage.clear(); + }); + + it("seeds sample tasks", () => { + const seededTasks = taskService.seed(); + + expect(seededTasks.length).toBe(3); + + const tasks = JSON.parse(localStorage.getItem("tasks")!); + + expect(tasks.length).toBe(3); + }); + + it("fetchs all tasks", () => { + taskService.seed(); + const tasks = taskService.fetchAll(); + + expect(tasks.length).toBe(3); + }); + + it("creates a new task", () => { + taskService.seed(); + const newTask = taskService.create({ + title: "Test task", + description: "Test description", + status: "todo", + }); + + expect(newTask.id).toBeDefined(); + expect(newTask.title).toBe("Test task"); + + const tasks = taskService.fetchAll(); + + expect(tasks.length).toBe(4); + }); + + it("updates an existing task", () => { + const tasks = taskService.seed(); + const taskToUpdate = tasks[0]; + + const updated = taskService.update(taskToUpdate.id, { + title: "Updated title", + status: "done", + }); + + expect(updated).not.toBeNull(); + expect(updated?.title).toBe("Updated title"); + expect(updated?.status).toBe("done"); + + const stored = taskService.fetchAll().find((t) => t.id === taskToUpdate.id); + + expect(stored?.title).toBe("Updated title"); + }); + + it("returns null when updating non-existent task", () => { + const result = taskService.update("non-existent-id", { + title: "This does not exist", + }); + + expect(result).toBeNull(); + }); + + it("deletes a task", () => { + const tasks = taskService.seed(); + const taskToDelete = tasks[0]; + const result = taskService.delete(taskToDelete.id); + + expect(result).toBe(true); + + const remainingTasks = taskService.fetchAll(); + + expect(remainingTasks.length).toBe(2); + expect( + remainingTasks.find((t) => t.id === taskToDelete.id) + ).toBeUndefined(); + }); + + it("returns false when deleting a non-existent task", () => { + taskService.seed(); + const result = taskService.delete("non-existent-id"); + + expect(result).toBe(false); + }); + + it("clears all tasks", () => { + taskService.seed(); + taskService.clear(); + + const tasks = taskService.fetchAll(); + + expect(tasks.length).toBe(0); + }); +}); From 59e5c0f4aefefcd67095f1cb6261e18e4435ecb8 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Tue, 30 Sep 2025 18:31:35 -0600 Subject: [PATCH 16/18] Cleanup --- {src => test}/App.test.tsx | 2 +- test/helpers/{tasks.test.tsx => tasks.test.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {src => test}/App.test.tsx (91%) rename test/helpers/{tasks.test.tsx => tasks.test.ts} (100%) diff --git a/src/App.test.tsx b/test/App.test.tsx similarity index 91% rename from src/App.test.tsx rename to test/App.test.tsx index 3f4e706..fce4f8b 100644 --- a/src/App.test.tsx +++ b/test/App.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect } from "vitest"; -import App from "./App"; +import App from "../src/App"; describe("App", () => { it("renders welcome message", () => { diff --git a/test/helpers/tasks.test.tsx b/test/helpers/tasks.test.ts similarity index 100% rename from test/helpers/tasks.test.tsx rename to test/helpers/tasks.test.ts From ae4ae49094a189afc4d9314f0bfd185feac5bcd7 Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Tue, 30 Sep 2025 18:41:59 -0600 Subject: [PATCH 17/18] more testing for components --- test/components/TaskColumn.test.tsx | 104 ++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test/components/TaskColumn.test.tsx diff --git a/test/components/TaskColumn.test.tsx b/test/components/TaskColumn.test.tsx new file mode 100644 index 0000000..e6723f9 --- /dev/null +++ b/test/components/TaskColumn.test.tsx @@ -0,0 +1,104 @@ +// TaskColumn.test.tsx +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import { TaskColumn } from "@/components/TaskColumn"; +import { Task } from "@/types"; + +describe("TaskColumn", () => { + const tasks: Task[] = [ + { + id: "1", + title: "Task 1", + description: "Do the first thing.", + status: "todo", + }, + { + id: "2", + title: "Task 2", + description: "Now do the second thing.", + status: "inProgress", + }, + ]; + + it("renders the column title", () => { + render( + {}} + onDelete={() => {}} + /> + ); + + expect(screen.getByText("Todo")).toBeDefined(); + }); + + it("renders tasks when present", () => { + render( + {}} + onDelete={() => {}} + /> + ); + + tasks.forEach((task) => { + expect(screen.getByText(task.title)).toBeDefined(); + expect(screen.getByText(task.description)).toBeDefined(); + }); + }); + + it("shows empty state when there are no tasks", () => { + render( + {}} + onDelete={() => {}} + /> + ); + + expect(screen.getByText("No tasks")).toBeDefined(); + }); + + it("calls moveTask on Previous / Next button clicks", () => { + const moveTaskMock = vi.fn(); + + render( + {}} + /> + ); + + const prevButtons = screen.getAllByText("Previous"); + const nextButtons = screen.getAllByText("Next"); + + fireEvent.click(nextButtons[0]); + expect(moveTaskMock).toHaveBeenCalledWith("1", "next"); + + fireEvent.click(prevButtons[1]); + expect(moveTaskMock).toHaveBeenCalledWith("2", "previous"); + }); + + it("calls onDelete when Delete button is clicked", () => { + const onDeleteMock = vi.fn(); + + render( + {}} + onDelete={onDeleteMock} + /> + ); + + const deleteButtons = screen.getAllByText("Delete"); + + fireEvent.click(deleteButtons[0]); + expect(onDeleteMock).toHaveBeenCalledWith("1"); + }); +}); From 7db11dfa0ee552adf3d8cc029a4dc3acbd1bb6af Mon Sep 17 00:00:00 2001 From: Matt Ha Date: Tue, 30 Sep 2025 18:47:52 -0600 Subject: [PATCH 18/18] Comment tweaks --- src/ChallengeComponent.tsx | 2 +- test/components/CreateTaskForm.test.tsx | 1 - test/components/TaskCard.test.tsx | 1 - test/components/TaskColumn.test.tsx | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 8d44293..19fddaf 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -11,7 +11,7 @@ export function ChallengeComponent() { useEffect(() => { (async () => { - // Currently fetching a seed list of tasks if the task list is empty for demo purposes. + // Seed a list of tasks if the task list is empty for demo purposes. let allTasks = await taskService.fetchAll(); if (allTasks.length === 0) { allTasks = await taskService.seed(); diff --git a/test/components/CreateTaskForm.test.tsx b/test/components/CreateTaskForm.test.tsx index ccdd82e..7565d24 100644 --- a/test/components/CreateTaskForm.test.tsx +++ b/test/components/CreateTaskForm.test.tsx @@ -1,4 +1,3 @@ -// CreateTaskForm.test.tsx import { render, screen, fireEvent } from "@testing-library/react"; import { CreateTaskForm } from "@/components/CreateTaskForm"; import { describe, it, expect, vi } from "vitest"; diff --git a/test/components/TaskCard.test.tsx b/test/components/TaskCard.test.tsx index ff3e7ab..d8e5ba9 100644 --- a/test/components/TaskCard.test.tsx +++ b/test/components/TaskCard.test.tsx @@ -1,4 +1,3 @@ -// TaskCard.test.tsx import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { TaskCard } from "@/components/TaskCard"; diff --git a/test/components/TaskColumn.test.tsx b/test/components/TaskColumn.test.tsx index e6723f9..97c269a 100644 --- a/test/components/TaskColumn.test.tsx +++ b/test/components/TaskColumn.test.tsx @@ -1,4 +1,3 @@ -// TaskColumn.test.tsx import { render, screen, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { TaskColumn } from "@/components/TaskColumn";