86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import { useState, type ReactElement } from 'react';
|
|
import { KanbanBoard } from '@/components/tasks/kanban-board';
|
|
import { TaskDetailModal } from '@/components/tasks/task-detail-modal';
|
|
import { TaskListView } from '@/components/tasks/task-list-view';
|
|
import { StaleDataNotice, UnavailableDataNotice } from '@/components/freshness/freshness-notices';
|
|
import { api } from '@/lib/api';
|
|
import { cn } from '@/lib/cn';
|
|
import type { Task } from '@/lib/types';
|
|
import { useFreshCollection, describeFailure } from '@/lib/freshness/use-fresh-collection';
|
|
import { validateTaskCollection } from '@/lib/freshness/validators';
|
|
|
|
type ViewMode = 'list' | 'kanban';
|
|
|
|
export function TasksPage(): ReactElement {
|
|
const tasks = useFreshCollection<Task[]>({
|
|
source: 'gateway:/api/tasks',
|
|
fetcher: (signal) => api<unknown>('/api/tasks', { signal }),
|
|
validate: validateTaskCollection,
|
|
cacheKey: 'tasks',
|
|
});
|
|
const [view, setView] = useState<ViewMode>('kanban');
|
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
|
|
|
const retry = (): void => {
|
|
void tasks.revalidate();
|
|
};
|
|
|
|
return (
|
|
<div data-freshness={tasks.freshness} className="flex min-h-screen flex-col px-4 py-6 sm:px-6">
|
|
<header className="mb-6 flex items-center justify-between gap-4 border-b px-1 pb-3">
|
|
<h1 className="text-2xl font-semibold">Tasks</h1>
|
|
<div className="flex rounded-lg border border-surface-border">
|
|
<button
|
|
type="button"
|
|
onClick={() => setView('list')}
|
|
className={cn(
|
|
'px-3 py-1.5 text-xs transition-colors',
|
|
view === 'list'
|
|
? 'bg-surface-elevated text-text-primary'
|
|
: 'text-text-muted hover:text-text-secondary',
|
|
)}
|
|
>
|
|
List
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setView('kanban')}
|
|
className={cn(
|
|
'px-3 py-1.5 text-xs transition-colors',
|
|
view === 'kanban'
|
|
? 'bg-surface-elevated text-text-primary'
|
|
: 'text-text-muted hover:text-text-secondary',
|
|
)}
|
|
>
|
|
Kanban
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{tasks.freshness === 'stale' && tasks.snapshot ? (
|
|
<div className="mb-6">
|
|
<StaleDataNotice label={tasks.snapshot} onRetry={retry} />
|
|
</div>
|
|
) : null}
|
|
|
|
{tasks.freshness === 'unknown' ? (
|
|
<p className="py-8 text-center text-sm text-text-muted">Loading tasks...</p>
|
|
) : tasks.freshness === 'unavailable' ? (
|
|
<UnavailableDataNotice
|
|
title="Tasks"
|
|
detail={describeFailure(tasks.failure)}
|
|
onRetry={retry}
|
|
/>
|
|
) : view === 'kanban' ? (
|
|
<KanbanBoard tasks={tasks.data ?? []} onTaskClick={setSelectedTask} />
|
|
) : (
|
|
<TaskListView tasks={tasks.data ?? []} onTaskClick={setSelectedTask} />
|
|
)}
|
|
|
|
{selectedTask ? (
|
|
<TaskDetailModal task={selectedTask} onClose={() => setSelectedTask(null)} />
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|