Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,25 @@
"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/node": "^24.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"
}
}
98 changes: 92 additions & 6 deletions src/ChallengeComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,96 @@
import { useEffect, useState } from "react";
import { TaskColumn } from "./components/TaskColumn";
import { Task, TaskStatus } from "./types";
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 [tasks, setTasks] = useState<Task[]>([]);

useEffect(() => {
(async () => {
// 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();
}
setTasks(allTasks);
})();
}, []);

const moveTask = async (id: string, direction: "previous" | "next") => {
const order: TaskStatus[] = ["todo", "inProgress", "done"];
const task = tasks.find((task) => task.id === id);

if (!task) return;

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);

const updated = await taskService.update(id, {
status: order[taskStatusIndex],
});

if (updated) {
setTasks((prev) => prev.map((task) => (task.id === id ? updated : task)));
}
};

const handleCreate = async (task: Omit<Task, "id">) => {
const newTask = await taskService.create(task);
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 (
<>
{/* Delete this h2, and add your own code here. */}
<h2 className="text-center py-48 text-xl text-gray-700">
Your code goes here
</h2>
</>
<div className="flex flex-col gap-6 m-6">
<div className="flex flex-col md:flex-row gap-4">
{TASK_STATUS_IDS.map((taskStatusId) => (
<TaskColumn
key={taskStatusId}
title={COLUMN_LABELS[taskStatusId]}
tasks={filterByStatus(tasks, taskStatusId)}
moveTask={moveTask}
onDelete={handleDelete}
/>
))}
</div>
<div className="flex flex-col items-start gap-4 p-4 bg-gray-50">
<CreateTaskForm onCreate={handleCreate} />
<div className="flex gap-2">
<button
onClick={handleClear}
className="px-3 py-1 bg-red-400 text-white rounded hover:bg-red-500"
>
Clear Tasks
</button>
<button
onClick={handleSeed}
className="px-3 py-1 bg-green-400 text-white rounded hover:bg-green-500"
>
Re-Seed Tasks
</button>
</div>
</div>
</div>
);
}
52 changes: 52 additions & 0 deletions src/components/CreateTaskForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useState, FormEvent } from "react";
import { Task } from "../types";

interface CreateTaskFormProps {
onCreate: (task: Omit<Task, "id">) => 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",
});

// Note: if this were to be handled by an HTTP request, we would only reset the
// inputs after onSuccess.
setTitle("");
setDescription("");
};

return (
<form onSubmit={handleSubmit} className="flex flex-col gap-2 mb-4">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Task title"
aria-label="Task title"
className="border rounded px-2 py-1"
/>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Task description"
aria-label="Task description"
className="border rounded px-2 py-1"
/>
<button
type="submit"
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Add Task
</button>
</form>
);
}
64 changes: 64 additions & 0 deletions src/components/TaskCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { COLUMN_LABELS } from "@/utils/constants";
import { Task, TaskStatus } from "../types";

interface TaskCardProps {
task: Task;
onMovePrevious?: (taskId: string) => void;
onMoveNext?: (taskId: string) => void;
onDelete?: (taskId: string) => void;
}

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 statusColors: Record<TaskStatus, string> = {
todo: "bg-blue-100",
inProgress: "bg-yellow-100",
done: "bg-green-100",
};

return (
<div
className={`${statusColors[status]} p-4 rounded-md shadow-sm mb-2 flex flex-col`}
>
<div className="flex justify-between items-center mb-2">
<h3 className="font-semibold">{title}</h3>
<p className="text-sm text-gray-600">{COLUMN_LABELS[status]}</p>
</div>
<p className="mb-2">{description}</p>

<div className="flex justify-between items-center">
<button
className="px-2 py-1 text-sm bg-red-200 rounded hover:bg-red-300"
onClick={() => onDelete?.(id)}
>
Delete
</button>
<div className="flex gap-2">
<button
className="px-2 py-1 bg-gray-200 rounded hover:bg-gray-300 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed"
onClick={() => onMovePrevious?.(id)}
disabled={currentIndex === 0}
>
Previous
</button>
<button
className="px-2 py-1 bg-gray-200 rounded hover:bg-gray-300 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed"
onClick={() => onMoveNext?.(id)}
disabled={currentIndex === statusOrder.length - 1}
>
Next
</button>
</div>
</div>
</div>
);
}
40 changes: 40 additions & 0 deletions src/components/TaskColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Task } from "../types";
import { TaskCard } from "./TaskCard";

interface TaskColumnProps {
title: string;
tasks: Task[];
moveTask: (id: string, direction: "previous" | "next") => void;
onDelete: (id: string) => void;
}

export function TaskColumn({
title,
tasks,
moveTask,
onDelete,
}: TaskColumnProps) {
return (
<div className="flex-1 p-4 border rounded-md shadow-md min-w-[250px] max-w-[300px] flex flex-col min-h-[750px]">
<h2 className="font-semibold mb-2 text-center border-b pb-4 mb-4">
{title}
</h2>
<ul className="flex-1 overflow-y-auto space-y-2">
{tasks.length === 0 ? (
<li className="text-gray-500 text-center mt-4">No tasks</li>
) : (
tasks.map((task) => (
<li key={task.id}>
<TaskCard
task={task}
onMovePrevious={(id) => moveTask(id, "previous")}
onMoveNext={(id) => moveTask(id, "next")}
onDelete={onDelete}
/>
</li>
))
)}
</ul>
</div>
);
}
8 changes: 8 additions & 0 deletions src/helpers/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Task, TaskStatus } from "../types";

export const filterByStatus = (
allTasks: Task[],
status: TaskStatus
): Task[] => {
return allTasks.filter((task) => task.status === status);
};
Loading