From 06f931f33308487406c3d1f03d2194db9b9a369f Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 15:46:09 -0300 Subject: [PATCH 01/12] feat: created column and card component --- package-lock.json | 33 +++++++++++++++++++-- package.json | 15 +++++----- src/ChallengeComponent.tsx | 46 ++++++++++++++++++++++++----- src/components/TaskCard/index.tsx | 14 +++++++++ src/components/TaskColumn/index.tsx | 20 +++++++++++++ src/types.ts | 11 +++++++ vite.config.ts | 2 +- 7 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 src/components/TaskCard/index.tsx create mode 100644 src/components/TaskColumn/index.tsx create mode 100644 src/types.ts diff --git a/package-lock.json b/package-lock.json index f1d49e1..8623d40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.1.0", "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-feather": "^2.0.10" }, "devDependencies": { "@testing-library/jest-dom": "^6.4.2", @@ -4543,7 +4544,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5034,6 +5034,23 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -5104,6 +5121,18 @@ "react": "^18.3.1" } }, + "node_modules/react-feather": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/react-feather/-/react-feather-2.0.10.tgz", + "integrity": "sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 1a66568..c9fed80 100644 --- a/package.json +++ b/package.json @@ -12,27 +12,28 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-feather": "^2.0.10" }, "devDependencies": { + "@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-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" } } diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 2344883..79a5f0e 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,10 +1,42 @@ +import { TaskColumn } from "./components/TaskColumn"; + +const columns = [ + { + id: 'todo', + title: 'To Do', + order: 0, + }, + { + id: 'inProgress', + title: 'In Progress', + order: 1, + }, + { + id: 'done', + title: 'Done', + order: 2, + }, +] + +const mockedTasks = [ + {id: 'task-1', text: 'Mow The lawn', columnId: 'todo'}, + {id: 'task-2', text: 'Pull Weeds', columnId: 'inProgress'}, + {id: 'task-1', text: 'Rake the leaves', columnId: 'done'} +] + export function ChallengeComponent() { - return ( - <> - {/* Delete this h2, and add your own code here. */} -

- Your code goes here -

- + + const getTasksForColumn = (columnId: string) => { + return mockedTasks.filter(task => task.columnId === columnId) + } + + return ( +
+ { + columns.map((column) => ( + + )) + } +
); } diff --git a/src/components/TaskCard/index.tsx b/src/components/TaskCard/index.tsx new file mode 100644 index 0000000..bc68932 --- /dev/null +++ b/src/components/TaskCard/index.tsx @@ -0,0 +1,14 @@ +import { Task } from "../../types" + import { ArrowLeft, ArrowRight} from 'react-feather'; + +interface TaskCardProps { + task: Task +} + +export function TaskCard({task}: TaskCardProps) { + return
+ +

{task.text}

+ +
+} \ No newline at end of file diff --git a/src/components/TaskColumn/index.tsx b/src/components/TaskColumn/index.tsx new file mode 100644 index 0000000..3199d5a --- /dev/null +++ b/src/components/TaskColumn/index.tsx @@ -0,0 +1,20 @@ +import { Column, Task } from "../../types"; +import { TaskCard } from "../TaskCard"; + +interface TaskColumnProps { + column: Column; + tasks: Task[]; +} + +export function TaskColumn({ + column, + tasks +}: TaskColumnProps) { + return ( +
+

{column.title}

+ + {tasks.map((task) => )} +
+ ) +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2394b85 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,11 @@ +export interface Column { + id: string; + title: string; + order: number; +} + +export interface Task { + id: string; + text: string; + columnId: string; +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 279c5d2..0627e02 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,7 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], server: { - port: 3000, + port: 5173, open: true, }, test: { From c2980d6cf30c884b91cb0fdea702c66254903730 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 16:02:47 -0300 Subject: [PATCH 02/12] feat: add new task input --- src/ChallengeComponent.tsx | 17 ++++++++----- src/components/TaskForm/index.tsx | 40 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 src/components/TaskForm/index.tsx diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 79a5f0e..83276d8 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,4 +1,5 @@ import { TaskColumn } from "./components/TaskColumn"; +import { TaskForm } from "./components/TaskForm"; const columns = [ { @@ -31,12 +32,16 @@ export function ChallengeComponent() { } return ( -
- { - columns.map((column) => ( - - )) - } +
+
+ { + columns.map((column) => ( + + )) + } +
+ +
); } diff --git a/src/components/TaskForm/index.tsx b/src/components/TaskForm/index.tsx new file mode 100644 index 0000000..1296fd9 --- /dev/null +++ b/src/components/TaskForm/index.tsx @@ -0,0 +1,40 @@ + import { ChangeEvent, FormEvent, useState } from "react"; +import { Plus } from "react-feather"; + +export function TaskForm() { + const [newTaskText, setNewTaskText] = useState('') + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const trimText = newTaskText.trim(); + if(trimText) { + setNewTaskText(''); + } + } + + const handleChange = (e: ChangeEvent) => { + setNewTaskText(e.target.value); + } + + return ( +
+
+ + +
+
+ ) +} \ No newline at end of file From e6bc0c0e39c2d2aa75bd0ff3a6e5d8a974252ec3 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 16:35:40 -0300 Subject: [PATCH 03/12] feat: added useTask --- src/ChallengeComponent.tsx | 24 ++++++---- src/components/TaskCard/index.tsx | 20 ++++++-- src/components/TaskColumn/index.tsx | 15 ++++-- src/components/TaskForm/index.tsx | 7 ++- src/hooks/useTask.ts | 71 +++++++++++++++++++++++++++++ src/types.ts | 16 ++++++- 6 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 src/hooks/useTask.ts diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 83276d8..4e50cf4 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,5 +1,7 @@ import { TaskColumn } from "./components/TaskColumn"; import { TaskForm } from "./components/TaskForm"; +import { useTasks } from "./hooks/useTask"; +import { Board } from "./types"; const columns = [ { @@ -19,16 +21,17 @@ const columns = [ }, ] -const mockedTasks = [ - {id: 'task-1', text: 'Mow The lawn', columnId: 'todo'}, - {id: 'task-2', text: 'Pull Weeds', columnId: 'inProgress'}, - {id: 'task-1', text: 'Rake the leaves', columnId: 'done'} -] +const board: Board = { + id: '1', + name: 'interview', + columns: columns +} export function ChallengeComponent() { + const { tasks, addTask, moveTask } = useTasks({ board }) const getTasksForColumn = (columnId: string) => { - return mockedTasks.filter(task => task.columnId === columnId) + return tasks.filter(task => task.columnId === columnId) } return ( @@ -36,12 +39,17 @@ export function ChallengeComponent() {
{ columns.map((column) => ( - + )) }
- +
); } diff --git a/src/components/TaskCard/index.tsx b/src/components/TaskCard/index.tsx index bc68932..df994ba 100644 --- a/src/components/TaskCard/index.tsx +++ b/src/components/TaskCard/index.tsx @@ -2,13 +2,25 @@ import { Task } from "../../types" import { ArrowLeft, ArrowRight} from 'react-feather'; interface TaskCardProps { - task: Task + task: Task; + onMoveLeft: (taskId: string) => void; + onMoveRight: (taskId: string) => void; } -export function TaskCard({task}: TaskCardProps) { +export function TaskCard({ task, onMoveLeft, onMoveRight }: TaskCardProps) { return
- +

{task.text}

- +
} \ No newline at end of file diff --git a/src/components/TaskColumn/index.tsx b/src/components/TaskColumn/index.tsx index 3199d5a..06ff841 100644 --- a/src/components/TaskColumn/index.tsx +++ b/src/components/TaskColumn/index.tsx @@ -1,20 +1,29 @@ -import { Column, Task } from "../../types"; +import { Column, Direction, DIRECTION, Task } from "../../types"; import { TaskCard } from "../TaskCard"; interface TaskColumnProps { column: Column; tasks: Task[]; + onMoveTask: (taskId: string, direction: Direction) => void; } export function TaskColumn({ column, - tasks + tasks, + onMoveTask }: TaskColumnProps) { return (

{column.title}

- {tasks.map((task) => )} + {tasks.map((task) => + onMoveTask(taskId, DIRECTION.LEFT)} + onMoveRight={(taskId) => onMoveTask(taskId, DIRECTION.RIGHT)} + /> + )}
) } \ No newline at end of file diff --git a/src/components/TaskForm/index.tsx b/src/components/TaskForm/index.tsx index 1296fd9..a8b323b 100644 --- a/src/components/TaskForm/index.tsx +++ b/src/components/TaskForm/index.tsx @@ -1,13 +1,18 @@ import { ChangeEvent, FormEvent, useState } from "react"; import { Plus } from "react-feather"; -export function TaskForm() { +interface TaskFormProps { + onAddTask: (text: string) => void; +} + +export function TaskForm({ onAddTask }: TaskFormProps) { const [newTaskText, setNewTaskText] = useState('') const handleSubmit = (e: FormEvent) => { e.preventDefault(); const trimText = newTaskText.trim(); if(trimText) { + onAddTask(trimText); setNewTaskText(''); } } diff --git a/src/hooks/useTask.ts b/src/hooks/useTask.ts new file mode 100644 index 0000000..a9e2479 --- /dev/null +++ b/src/hooks/useTask.ts @@ -0,0 +1,71 @@ +import { useCallback, useState } from "react"; +import { Task, Direction, Board, DIRECTION } from "../types"; + +interface UseTaskOptions { + board: Board | null +} + +interface UseTaskReturn { + tasks: Task[]; + addTask: (text:string) => void; + moveTask: (taskId:string, direction: Direction) => void; +} + +const mockedTasks = [ + {id: 'task-1', text: 'Mow The lawn', columnId: 'todo', boardId: '1'}, + {id: 'task-2', text: 'Pull Weeds', columnId: 'inProgress', boardId: '1'}, + {id: 'task-3', text: 'Rake the leaves', columnId: 'done', boardId: '1'} +] + +export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { + const [tasks, setTasks] = useState(mockedTasks) + + const addTask = useCallback((text: string) => { + if(!board) return; + + const newTask: Task = { + id: crypto.randomUUID(), + text, + columnId: 'todo', + boardId: board.id + } + + setTasks(prevTasks => [...prevTasks, newTask]) + },[]) + + const moveTask = useCallback((taskId: string, direction: Direction) => { + if(!board) return; + + setTasks(prevTasks => { + return prevTasks.map(task => { + if(task.id === taskId){ + const currentColumn = board.columns.find(col => col.id === task.columnId) + let targetColumn; + + if(!currentColumn) return task + + if(direction === DIRECTION.RIGHT) { + targetColumn = board.columns.find(col => col.order === currentColumn.order + 1) + } + if(direction === DIRECTION.LEFT) { + targetColumn = board.columns.find(col => col.order === currentColumn.order - 1) + } + + if(!targetColumn) return task; + + return { + ...task, + columnId: targetColumn.id, + } + } + return task; + }) + }) + },[board]) + + return { + tasks, + addTask, + moveTask + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 2394b85..c6115fc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,9 @@ +export interface Board { + id: string; + name: string; + columns: Column[] +} + export interface Column { id: string; title: string; @@ -8,4 +14,12 @@ export interface Task { id: string; text: string; columnId: string; -} \ No newline at end of file + boardId: string; +} + +export type Direction = 'left' | 'right' + +export const DIRECTION = { + LEFT: 'left' as const, + RIGHT: 'right' as const +} as const; \ No newline at end of file From 8a66b3ab2fdc2de5810480262c94aea861667b72 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 16:42:39 -0300 Subject: [PATCH 04/12] feat: add updatedAt to sort tasks --- src/hooks/useTask.ts | 14 +++++++++----- src/types.ts | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/hooks/useTask.ts b/src/hooks/useTask.ts index a9e2479..127efda 100644 --- a/src/hooks/useTask.ts +++ b/src/hooks/useTask.ts @@ -12,9 +12,9 @@ interface UseTaskReturn { } const mockedTasks = [ - {id: 'task-1', text: 'Mow The lawn', columnId: 'todo', boardId: '1'}, - {id: 'task-2', text: 'Pull Weeds', columnId: 'inProgress', boardId: '1'}, - {id: 'task-3', text: 'Rake the leaves', columnId: 'done', boardId: '1'} + {id: 'task-1', text: 'Mow The lawn', columnId: 'todo', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'}, + {id: 'task-2', text: 'Pull Weeds', columnId: 'inProgress', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'}, + {id: 'task-3', text: 'Rake the leaves', columnId: 'done', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'} ] export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { @@ -27,7 +27,8 @@ export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { id: crypto.randomUUID(), text, columnId: 'todo', - boardId: board.id + boardId: board.id, + updatedAt: new Date().toISOString() } setTasks(prevTasks => [...prevTasks, newTask]) @@ -56,6 +57,7 @@ export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { return { ...task, columnId: targetColumn.id, + updatedAt: new Date().toISOString() } } return task; @@ -63,8 +65,10 @@ export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { }) },[board]) + const sortedTasksByUpdatedDate = tasks.sort((a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()); + return { - tasks, + tasks: sortedTasksByUpdatedDate, addTask, moveTask } diff --git a/src/types.ts b/src/types.ts index c6115fc..55b0109 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,7 @@ export interface Task { text: string; columnId: string; boardId: string; + updatedAt: string; } export type Direction = 'left' | 'right' From b270be2e003a832fc150dcc254b78259519cb19a Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:05:17 -0300 Subject: [PATCH 05/12] feat: add disabled buttons --- src/ChallengeComponent.tsx | 4 +++- src/components/TaskCard/index.tsx | 10 +++++++--- src/components/TaskColumn/index.tsx | 10 ++++++++-- src/components/TaskForm/index.tsx | 2 +- src/hooks/useTask.ts | 6 ++++-- tailwind.config.js | 7 +++++++ 6 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index 4e50cf4..b1f6f1c 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -38,12 +38,14 @@ export function ChallengeComponent() {
{ - columns.map((column) => ( + columns.map((column, index) => ( )) } diff --git a/src/components/TaskCard/index.tsx b/src/components/TaskCard/index.tsx index df994ba..7bb4321 100644 --- a/src/components/TaskCard/index.tsx +++ b/src/components/TaskCard/index.tsx @@ -5,20 +5,24 @@ interface TaskCardProps { task: Task; onMoveLeft: (taskId: string) => void; onMoveRight: (taskId: string) => void; + isFirstColumn: boolean; + isLastColumn: boolean; } -export function TaskCard({ task, onMoveLeft, onMoveRight }: TaskCardProps) { +export function TaskCard({ task, onMoveLeft, onMoveRight, isFirstColumn, isLastColumn }: TaskCardProps) { return

{task.text}

diff --git a/src/components/TaskColumn/index.tsx b/src/components/TaskColumn/index.tsx index 06ff841..ff851b9 100644 --- a/src/components/TaskColumn/index.tsx +++ b/src/components/TaskColumn/index.tsx @@ -5,15 +5,19 @@ interface TaskColumnProps { column: Column; tasks: Task[]; onMoveTask: (taskId: string, direction: Direction) => void; + isFirstColumn: boolean; + isLastColumn: boolean; } export function TaskColumn({ column, tasks, - onMoveTask + onMoveTask, + isFirstColumn, + isLastColumn }: TaskColumnProps) { return ( -
+

{column.title}

{tasks.map((task) => @@ -22,6 +26,8 @@ export function TaskColumn({ task={task} onMoveLeft={(taskId) => onMoveTask(taskId, DIRECTION.LEFT)} onMoveRight={(taskId) => onMoveTask(taskId, DIRECTION.RIGHT)} + isFirstColumn={isFirstColumn} + isLastColumn={isLastColumn} /> )}
diff --git a/src/components/TaskForm/index.tsx b/src/components/TaskForm/index.tsx index a8b323b..1854825 100644 --- a/src/components/TaskForm/index.tsx +++ b/src/components/TaskForm/index.tsx @@ -36,7 +36,7 @@ export function TaskForm({ onAddTask }: TaskFormProps) { />
diff --git a/src/hooks/useTask.ts b/src/hooks/useTask.ts index 127efda..ab1f358 100644 --- a/src/hooks/useTask.ts +++ b/src/hooks/useTask.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Task, Direction, Board, DIRECTION } from "../types"; interface UseTaskOptions { @@ -65,7 +65,9 @@ export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { }) },[board]) - const sortedTasksByUpdatedDate = tasks.sort((a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()); + const sortedTasksByUpdatedDate = useMemo(() => { + return tasks.sort((a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()); + },[ tasks]) return { tasks: sortedTasksByUpdatedDate, diff --git a/tailwind.config.js b/tailwind.config.js index 4e9b470..5f2a32a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,6 +6,13 @@ export default { fontFamily: { roboto: ["Roboto", "sans-serif"], }, + colors: { + "green": "#218d1f", + "green-disabled": "#90b08f", + "red-disabled": "#ff9e9e", + "red": "#c20b0b", + "blue-button": "#0066ff" + } }, }, plugins: [], From f9305f8f3602bb928a9718b041c0852feb3830e1 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:09:07 -0300 Subject: [PATCH 06/12] chore: make mocks on separete file --- src/ChallengeComponent.tsx | 31 ++++--------------------------- src/hooks/useTask.ts | 9 +++------ src/mocks/index.tsx | 29 +++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 src/mocks/index.tsx diff --git a/src/ChallengeComponent.tsx b/src/ChallengeComponent.tsx index b1f6f1c..e6d7a7a 100644 --- a/src/ChallengeComponent.tsx +++ b/src/ChallengeComponent.tsx @@ -1,33 +1,10 @@ import { TaskColumn } from "./components/TaskColumn"; import { TaskForm } from "./components/TaskForm"; import { useTasks } from "./hooks/useTask"; -import { Board } from "./types"; - -const columns = [ - { - id: 'todo', - title: 'To Do', - order: 0, - }, - { - id: 'inProgress', - title: 'In Progress', - order: 1, - }, - { - id: 'done', - title: 'Done', - order: 2, - }, -] - -const board: Board = { - id: '1', - name: 'interview', - columns: columns -} +import { mockedBoard } from "./mocks"; export function ChallengeComponent() { + const board = mockedBoard; const { tasks, addTask, moveTask } = useTasks({ board }) const getTasksForColumn = (columnId: string) => { @@ -38,14 +15,14 @@ export function ChallengeComponent() {
{ - columns.map((column, index) => ( + board.columns.map((column, index) => ( )) } diff --git a/src/hooks/useTask.ts b/src/hooks/useTask.ts index ab1f358..d4465b2 100644 --- a/src/hooks/useTask.ts +++ b/src/hooks/useTask.ts @@ -1,5 +1,6 @@ import { useCallback, useMemo, useState } from "react"; import { Task, Direction, Board, DIRECTION } from "../types"; +import { mockedTasks } from "../mocks"; interface UseTaskOptions { board: Board | null @@ -11,11 +12,7 @@ interface UseTaskReturn { moveTask: (taskId:string, direction: Direction) => void; } -const mockedTasks = [ - {id: 'task-1', text: 'Mow The lawn', columnId: 'todo', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'}, - {id: 'task-2', text: 'Pull Weeds', columnId: 'inProgress', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'}, - {id: 'task-3', text: 'Rake the leaves', columnId: 'done', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'} -] + export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { const [tasks, setTasks] = useState(mockedTasks) @@ -72,6 +69,6 @@ export const useTasks = ({ board }: UseTaskOptions): UseTaskReturn => { return { tasks: sortedTasksByUpdatedDate, addTask, - moveTask + moveTask, } } \ No newline at end of file diff --git a/src/mocks/index.tsx b/src/mocks/index.tsx new file mode 100644 index 0000000..3e5aab9 --- /dev/null +++ b/src/mocks/index.tsx @@ -0,0 +1,29 @@ +import { Board } from "../types" + +export const mockedTasks = [ + {id: 'task-1', text: 'Mow The lawn', columnId: 'todo', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'}, + {id: 'task-2', text: 'Pull Weeds', columnId: 'inProgress', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'}, + {id: 'task-3', text: 'Rake the leaves', columnId: 'done', boardId: '1', updatedAt: '2025-01-01T10:00:00Z'} +] + +export const mockedBoard: Board = { + id: '1', + name: 'interview', + columns: [ + { + id: 'todo', + title: 'To Do', + order: 0, + }, + { + id: 'inProgress', + title: 'In Progress', + order: 1, + }, + { + id: 'done', + title: 'Done', + order: 2, + }, + ] +} \ No newline at end of file From 7357f61a9ca4ad82e834f7dccc7f2927fa94473d Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:16:36 -0300 Subject: [PATCH 07/12] test: add tests for task card --- src/components/TaskCard/TaskCard.test.tsx | 101 ++++++++++++++++++++++ src/components/TaskCard/index.tsx | 2 + 2 files changed, 103 insertions(+) create mode 100644 src/components/TaskCard/TaskCard.test.tsx diff --git a/src/components/TaskCard/TaskCard.test.tsx b/src/components/TaskCard/TaskCard.test.tsx new file mode 100644 index 0000000..ddd9fd0 --- /dev/null +++ b/src/components/TaskCard/TaskCard.test.tsx @@ -0,0 +1,101 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { TaskCard } from './index'; +import type { Task } from '../../types'; +import { fireEvent, render, screen } from '@testing-library/react'; + +describe('TaskCard', () => { + const mockTask: Task = { + id: '1', + text: 'Test task', + columnId: 'todo', + boardId: 'board-1', + updatedAt: '2024-01-01T00:00:00Z' + }; + + const mockOnMoveLeft = vi.fn(); + const mockOnMoveRight = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('renders task text correctly', () => { + render( + + ); + + expect(screen.getByText('Test task')).toBeInTheDocument(); + }); + + test('calls onMoveLeft when left arrow is clicked', () => { + render( + + ); + + const leftButton = screen.getByTestId('move-left-1'); + fireEvent.click(leftButton); + + expect(mockOnMoveLeft).toHaveBeenCalledWith('1'); + expect(mockOnMoveLeft).toHaveBeenCalledTimes(1); + }); + + test('calls onMoveRight when right arrow is clicked', () => { + render( + + ); + + const rightButton = screen.getByTestId('move-right-1'); + fireEvent.click(rightButton); + + expect(mockOnMoveRight).toHaveBeenCalledWith('1'); + expect(mockOnMoveRight).toHaveBeenCalledTimes(1); + }); + + test('disables left button when isFirstColumn is true', () => { + render( + + ); + + const leftButton = screen.getByTestId('move-left-1'); + expect(leftButton).toBeDisabled(); + }); + + test('disables right button when isLastColumn is true', () => { + render( + + ); + + const rightButton = screen.getByTestId('move-right-1'); + expect(rightButton).toBeDisabled(); + }); +}); \ No newline at end of file diff --git a/src/components/TaskCard/index.tsx b/src/components/TaskCard/index.tsx index 7bb4321..eb69c3f 100644 --- a/src/components/TaskCard/index.tsx +++ b/src/components/TaskCard/index.tsx @@ -15,6 +15,7 @@ export function TaskCard({ task, onMoveLeft, onMoveRight, isFirstColumn, isLastC onClick={() => onMoveLeft(task.id)} disabled={isFirstColumn} className={`rounded-md h-16 w-12 flex justify-center items-center ${isFirstColumn ? "bg-red-disabled" :"bg-red"}`} + data-testid={`move-left-${task.id}`} > @@ -23,6 +24,7 @@ export function TaskCard({ task, onMoveLeft, onMoveRight, isFirstColumn, isLastC onClick={() => onMoveRight(task.id)} disabled={isLastColumn} className={`rounded-md h-16 w-12 flex justify-center items-center ${isLastColumn ? "bg-green-disabled" :"bg-green"}`} + data-testid={`move-right-${task.id}`} > From d14a92942a2429a22e93e47a909eb691e917eca8 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:21:24 -0300 Subject: [PATCH 08/12] test: add tests for task column --- src/components/TaskColumn/TaskColumn.test.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/components/TaskColumn/TaskColumn.test.tsx diff --git a/src/components/TaskColumn/TaskColumn.test.tsx b/src/components/TaskColumn/TaskColumn.test.tsx new file mode 100644 index 0000000..83ffb35 --- /dev/null +++ b/src/components/TaskColumn/TaskColumn.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { TaskColumn } from './index'; +import type { Task, Column } from '../../types'; + +describe('TaskColumn', () => { + const mockColumn: Column = { + id: 'todo', + title: 'Todo', + order: 0 + }; + + const mockTasks: Task[] = [ + { id: '1', text: 'Task 1', columnId: 'todo', boardId: 'board-1', updatedAt: '2024-01-01T00:00:00Z' }, + { id: '2', text: 'Task 2', columnId: 'todo', boardId: 'board-1', updatedAt: '2024-01-01T00:00:00Z' } + ]; + + const mockOnMoveTask = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('renders with correct title and tasks', () => { + render( + + ); + + expect(screen.getByText('Todo')).toBeInTheDocument(); + expect(screen.getByText('Task 1')).toBeInTheDocument(); + expect(screen.getByText('Task 2')).toBeInTheDocument(); + }); +}); \ No newline at end of file From df79369ee997c7f46d71f7570ae5bbe965fa32d2 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:30:21 -0300 Subject: [PATCH 09/12] test: add tests for task form --- src/components/TaskForm/TaskForm.test.tsx | 76 +++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/components/TaskForm/TaskForm.test.tsx diff --git a/src/components/TaskForm/TaskForm.test.tsx b/src/components/TaskForm/TaskForm.test.tsx new file mode 100644 index 0000000..9e4de11 --- /dev/null +++ b/src/components/TaskForm/TaskForm.test.tsx @@ -0,0 +1,76 @@ +import { render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import userEvent from '@testing-library/user-event'; +import { TaskForm } from './index'; +import { act } from 'react'; + +describe('TaskForm', () => { + const mockOnAddTask = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('renders form elements correctly', () => { + render(); + + expect(screen.getByRole('textbox', { + name: /new task text/i + })).toBeInTheDocument(); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + test('clears input after successful submission', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByRole('textbox', { + name: /new task text/i + }) as HTMLInputElement; + + await act(async () => { + await user.type(input, 'New task'); + await user.click(screen.getByRole('button')); + }) + + expect(input.value).toBe(''); + }); + + test('does not submit empty or whitespace only tasks', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByRole('textbox', { + name: /new task text/i + }) as HTMLInputElement; + const button = screen.getByRole('button'); + + await act(async () => { + await user.click(button); + }) + + expect(mockOnAddTask).not.toHaveBeenCalled(); + + await act(async () => { + await user.type(input, ' '); + await user.click(button); + }) + + expect(mockOnAddTask).not.toHaveBeenCalled(); + }); + + test('submits form on Enter key press', async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByRole('textbox', { + name: /new task text/i + }) as HTMLInputElement; + + await act(async () => { + await user.type(input, 'Task from enter{enter}'); + }) + + expect(mockOnAddTask).toHaveBeenCalledWith('Task from enter'); + }); +}); \ No newline at end of file From caeff3134324e78e181e468d31f9a4009e28f108 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:44:46 -0300 Subject: [PATCH 10/12] test: challenge component --- src/ChallengeComponent.test.tsx | 60 +++++++++++++++++++++++ src/components/TaskForm/TaskForm.test.tsx | 7 ++- src/components/TaskForm/index.tsx | 4 +- 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/ChallengeComponent.test.tsx diff --git a/src/ChallengeComponent.test.tsx b/src/ChallengeComponent.test.tsx new file mode 100644 index 0000000..79fce61 --- /dev/null +++ b/src/ChallengeComponent.test.tsx @@ -0,0 +1,60 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, test } from 'vitest'; +import { ChallengeComponent } from './ChallengeComponent'; +import { act } from 'react'; +import userEvent from '@testing-library/user-event'; +describe('TaskColumn', () => { + test('renders with correct title and tasks', () => { + render( + + ); + + expect(screen.getByText('To Do')).toBeInTheDocument(); + expect(screen.getByText('In Progress')).toBeInTheDocument(); + expect(screen.getByText('Done')).toBeInTheDocument(); + + expect(screen.getByText('Mow The lawn')).toBeInTheDocument(); + expect(screen.getByText('Rake the leaves')).toBeInTheDocument(); + }); + + test('should move tasks between columns', () => { + render( + + ); + + expect(screen.getByTestId('move-left-task-1')).toBeDisabled() + fireEvent.click(screen.getByTestId('move-right-task-1')) + expect(screen.getByTestId('move-left-task-1')).toBeEnabled() + fireEvent.click(screen.getByTestId('move-right-task-1')) + expect(screen.getByTestId('move-left-task-1')).toBeEnabled() + expect(screen.getByTestId('move-right-task-1')).toBeDisabled() + + expect(screen.getByTestId('move-right-task-3')).toBeDisabled() + fireEvent.click(screen.getByTestId('move-left-task-3')) + expect(screen.getByTestId('move-right-task-3')).toBeEnabled() + fireEvent.click(screen.getByTestId('move-left-task-3')) + expect(screen.getByTestId('move-right-task-3')).toBeEnabled() + expect(screen.getByTestId('move-left-task-3')).toBeDisabled() + }); + + test('should add a new task', async () => { + const user = userEvent.setup(); + + render( + + ); + + const input = screen.getByRole('textbox', { + name: /new task text/i + }) as HTMLInputElement; + + await act(async () => { + await user.type(input, 'New task'); + await user.click(screen.getByRole('button', { + name: /new task button/i + })); + }) + + expect(screen.getByText(/new task/i)).toBeInTheDocument() + }); +}); \ No newline at end of file diff --git a/src/components/TaskForm/TaskForm.test.tsx b/src/components/TaskForm/TaskForm.test.tsx index 9e4de11..99a9e23 100644 --- a/src/components/TaskForm/TaskForm.test.tsx +++ b/src/components/TaskForm/TaskForm.test.tsx @@ -41,9 +41,12 @@ describe('TaskForm', () => { render(); const input = screen.getByRole('textbox', { - name: /new task text/i + name: /new task text/i }) as HTMLInputElement; - const button = screen.getByRole('button'); + + const button = screen.getByRole('button', { + name: /new task button/i + }) await act(async () => { await user.click(button); diff --git a/src/components/TaskForm/index.tsx b/src/components/TaskForm/index.tsx index 1854825..71666bd 100644 --- a/src/components/TaskForm/index.tsx +++ b/src/components/TaskForm/index.tsx @@ -36,7 +36,9 @@ export function TaskForm({ onAddTask }: TaskFormProps) { />
From 336539901893da623d0d03e3dc896169586bc045 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:45:40 -0300 Subject: [PATCH 11/12] chore: change port back --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 0627e02..279c5d2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,7 @@ import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], server: { - port: 5173, + port: 3000, open: true, }, test: { From 6cf7ff070ffc7c9f1961211eb50ada9022d0f3e3 Mon Sep 17 00:00:00 2001 From: FeruYasu Date: Fri, 12 Sep 2025 17:49:47 -0300 Subject: [PATCH 12/12] chore: version update --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8623d40..a977a88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "engineer-interview", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "engineer-interview", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/package.json b/package.json index c9fed80..2648015 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "engineer-interview", - "version": "0.1.0", + "version": "0.2.0", "private": true, "type": "module", "scripts": {