feat(web): wire tasks page to real API data #473
@@ -4,57 +4,123 @@ import { useState, useEffect } from "react";
|
||||
import type { ReactElement } from "react";
|
||||
|
||||
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";
|
||||
|
||||
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(() => {
|
||||
void loadTasks();
|
||||
}, []);
|
||||
|
||||
async function loadTasks(): Promise<void> {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// TODO: Replace with real API call when backend is ready
|
||||
// const data = await fetchTasks();
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
setTasks(mockTasks);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: "We had trouble loading your tasks. Please try again when you're ready."
|
||||
);
|
||||
} finally {
|
||||
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 text-gray-900">Tasks</h1>
|
||||
<p className="text-gray-600 mt-2">Organize your work at your own pace</p>
|
||||
<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>
|
||||
|
||||
{error !== null ? (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-6 text-center">
|
||||
<p className="text-amber-800">{error}</p>
|
||||
{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={() => void loadTasks()}
|
||||
className="mt-4 rounded-md bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-700 transition-colors"
|
||||
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={isLoading} />
|
||||
<TaskList tasks={tasks} isLoading={false} />
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
export interface TaskFilters {
|
||||
@@ -34,81 +34,3 @@ export async function fetchTasks(filters?: TaskFilters): Promise<Task[]> {
|
||||
const response = await apiGet<ApiResponse<Task[]>>(endpoint, filters?.workspaceId);
|
||||
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"),
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user