feat(web): wire tasks page to real API data #473

Merged
jason.woltje merged 1 commits from feat/tasks-real-api into main 2026-02-23 03:51:09 +00:00
2 changed files with 95 additions and 107 deletions

View File

@@ -4,57 +4,123 @@ import { useState, useEffect } from "react";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import { TaskList } from "@/components/tasks/TaskList"; import { TaskList } from "@/components/tasks/TaskList";
import { mockTasks } from "@/lib/api/tasks"; import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
import { fetchTasks } from "@/lib/api/tasks";
import { useWorkspaceId } from "@/lib/hooks";
import type { Task } from "@mosaic/shared"; import type { Task } from "@mosaic/shared";
export default function TasksPage(): ReactElement { export default function TasksPage(): ReactElement {
const workspaceId = useWorkspaceId();
const [tasks, setTasks] = useState<Task[]>([]); const [tasks, setTasks] = useState<Task[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
void loadTasks(); if (!workspaceId) {
}, []); setIsLoading(false);
return;
}
let cancelled = false;
setError(null);
setIsLoading(true);
async function loadTasks(): Promise<void> { async function loadTasks(): Promise<void> {
setIsLoading(true);
setError(null);
try { try {
// TODO: Replace with real API call when backend is ready const filters = workspaceId !== null ? { workspaceId } : {};
// const data = await fetchTasks(); const data = await fetchTasks(filters);
await new Promise((resolve) => setTimeout(resolve, 300)); if (!cancelled) {
setTasks(mockTasks); setTasks(data);
} catch (err) { }
} catch (err: unknown) {
console.error("[Tasks] Failed to fetch tasks:", err);
if (!cancelled) {
setError( setError(
err instanceof Error err instanceof Error
? err.message ? err.message
: "We had trouble loading your tasks. Please try again when you're ready." : "We had trouble loading your tasks. Please try again when you're ready."
); );
}
} finally { } finally {
if (!cancelled) {
setIsLoading(false); setIsLoading(false);
} }
} }
}
void loadTasks();
return (): void => {
cancelled = true;
};
}, [workspaceId]);
function handleRetry(): void {
if (!workspaceId) return;
setError(null);
setIsLoading(true);
fetchTasks({ workspaceId })
.then((data) => {
setTasks(data);
})
.catch((err: unknown) => {
console.error("[Tasks] Retry failed:", err);
setError(
err instanceof Error
? err.message
: "We had trouble loading your tasks. Please try again when you're ready."
);
})
.finally(() => {
setIsLoading(false);
});
}
return ( return (
<main className="container mx-auto px-4 py-8"> <main className="container mx-auto px-4 py-8">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Tasks</h1> <h1 className="text-3xl font-bold" style={{ color: "var(--text)" }}>
<p className="text-gray-600 mt-2">Organize your work at your own pace</p> Tasks
</h1>
<p className="mt-2" style={{ color: "var(--text-muted)" }}>
Organize your work at your own pace
</p>
</div> </div>
{error !== null ? ( {isLoading ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 p-6 text-center"> <div className="flex justify-center py-16">
<p className="text-amber-800">{error}</p> <MosaicSpinner label="Loading tasks..." />
</div>
) : error !== null ? (
<div
className="rounded-lg p-6 text-center"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
}}
>
<p style={{ color: "var(--danger)" }}>{error}</p>
<button <button
onClick={() => void loadTasks()} onClick={handleRetry}
className="mt-4 rounded-md bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-700 transition-colors" className="mt-4 rounded-md px-4 py-2 text-sm font-medium text-white transition-colors"
style={{ background: "var(--danger)" }}
> >
Try again Try again
</button> </button>
</div> </div>
) : tasks.length === 0 ? (
<div
className="rounded-lg p-8 text-center"
style={{
background: "var(--surface)",
border: "1px solid var(--border)",
}}
>
<p style={{ color: "var(--text-muted)" }}>No tasks found</p>
</div>
) : ( ) : (
<TaskList tasks={tasks} isLoading={isLoading} /> <TaskList tasks={tasks} isLoading={false} />
)} )}
</main> </main>
); );

View File

@@ -4,7 +4,7 @@
*/ */
import type { Task } from "@mosaic/shared"; import type { Task } from "@mosaic/shared";
import { TaskStatus, TaskPriority } from "@mosaic/shared"; import type { TaskStatus, TaskPriority } from "@mosaic/shared";
import { apiGet, type ApiResponse } from "./client"; import { apiGet, type ApiResponse } from "./client";
export interface TaskFilters { export interface TaskFilters {
@@ -34,81 +34,3 @@ export async function fetchTasks(filters?: TaskFilters): Promise<Task[]> {
const response = await apiGet<ApiResponse<Task[]>>(endpoint, filters?.workspaceId); const response = await apiGet<ApiResponse<Task[]>>(endpoint, filters?.workspaceId);
return response.data; return response.data;
} }
/**
* Mock tasks for development (until backend endpoints are ready)
*/
export const mockTasks: Task[] = [
{
id: "task-1",
title: "Review pull request",
description: "Review and provide feedback on frontend PR",
status: TaskStatus.IN_PROGRESS,
priority: TaskPriority.HIGH,
dueDate: new Date("2026-01-29"),
creatorId: "user-1",
assigneeId: "user-1",
workspaceId: "workspace-1",
projectId: null,
parentId: null,
sortOrder: 0,
metadata: {},
completedAt: null,
createdAt: new Date("2026-01-28"),
updatedAt: new Date("2026-01-28"),
},
{
id: "task-2",
title: "Update documentation",
description: "Add setup instructions for new developers",
status: TaskStatus.IN_PROGRESS,
priority: TaskPriority.MEDIUM,
dueDate: new Date("2026-01-30"),
creatorId: "user-1",
assigneeId: "user-1",
workspaceId: "workspace-1",
projectId: null,
parentId: null,
sortOrder: 1,
metadata: {},
completedAt: null,
createdAt: new Date("2026-01-28"),
updatedAt: new Date("2026-01-28"),
},
{
id: "task-3",
title: "Plan Q1 roadmap",
description: "Define priorities for Q1 2026",
status: TaskStatus.NOT_STARTED,
priority: TaskPriority.HIGH,
dueDate: new Date("2026-02-03"),
creatorId: "user-1",
assigneeId: "user-1",
workspaceId: "workspace-1",
projectId: null,
parentId: null,
sortOrder: 2,
metadata: {},
completedAt: null,
createdAt: new Date("2026-01-28"),
updatedAt: new Date("2026-01-28"),
},
{
id: "task-4",
title: "Research new libraries",
description: "Evaluate options for state management",
status: TaskStatus.PAUSED,
priority: TaskPriority.LOW,
dueDate: new Date("2026-02-10"),
creatorId: "user-1",
assigneeId: "user-1",
workspaceId: "workspace-1",
projectId: null,
parentId: null,
sortOrder: 3,
metadata: {},
completedAt: null,
createdAt: new Date("2026-01-28"),
updatedAt: new Date("2026-01-28"),
},
];