Files
stack/apps/web/src/app/(authenticated)/tasks/page.tsx
Jason Woltje c510d6fbc6
Some checks failed
ci/woodpecker/push/web Pipeline failed
feat(web): wire tasks page to real API data
Replace mock data with real fetchTasks() API call. Add workspace-aware
data fetching, MosaicSpinner loading state, empty state card, and
error state with retry using design tokens throughout.

Refs #467
2026-02-22 21:49:38 -06:00

128 lines
3.4 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import type { ReactElement } from "react";
import { TaskList } from "@/components/tasks/TaskList";
import { MosaicSpinner } from "@/components/ui/MosaicSpinner";
import { fetchTasks } from "@/lib/api/tasks";
import { useWorkspaceId } from "@/lib/hooks";
import type { Task } from "@mosaic/shared";
export default function TasksPage(): ReactElement {
const workspaceId = useWorkspaceId();
const [tasks, setTasks] = useState<Task[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!workspaceId) {
setIsLoading(false);
return;
}
let cancelled = false;
setError(null);
setIsLoading(true);
async function loadTasks(): Promise<void> {
try {
const filters = workspaceId !== null ? { workspaceId } : {};
const data = await fetchTasks(filters);
if (!cancelled) {
setTasks(data);
}
} catch (err: unknown) {
console.error("[Tasks] Failed to fetch tasks:", err);
if (!cancelled) {
setError(
err instanceof Error
? err.message
: "We had trouble loading your tasks. Please try again when you're ready."
);
}
} finally {
if (!cancelled) {
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 (
<main className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold" style={{ color: "var(--text)" }}>
Tasks
</h1>
<p className="mt-2" style={{ color: "var(--text-muted)" }}>
Organize your work at your own pace
</p>
</div>
{isLoading ? (
<div className="flex justify-center py-16">
<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
onClick={handleRetry}
className="mt-4 rounded-md px-4 py-2 text-sm font-medium text-white transition-colors"
style={{ background: "var(--danger)" }}
>
Try again
</button>
</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={false} />
)}
</main>
);
}