Files
Neon-Desk/components/notifications/task-notifications-drawer.tsx

182 lines
6.3 KiB
TypeScript

'use client';
import { formatDistanceToNow } from 'date-fns';
import { BellOff, CheckCheck, EyeOff, X } from 'lucide-react';
import type { Task } from '@/lib/types';
import { taskTypeLabel } from '@/components/notifications/task-stage-helpers';
import { Button } from '@/components/ui/button';
import { StatusPill } from '@/components/ui/status-pill';
type TaskNotificationsDrawerProps = {
isOpen: boolean;
onClose: () => void;
activeTasks: Task[];
visibleFinishedTasks: Task[];
awaitingReviewTasks: Task[];
showReadFinished: boolean;
setShowReadFinished: (value: boolean) => void;
openTaskDetails: (taskId: string) => void;
markTaskRead: (taskId: string, read?: boolean) => Promise<void>;
silenceTask: (taskId: string, silenced?: boolean) => Promise<void>;
};
function TaskRow({
task,
openTaskDetails,
markTaskRead,
silenceTask
}: {
task: Task;
openTaskDetails: (taskId: string) => void;
markTaskRead: (taskId: string, read?: boolean) => Promise<void>;
silenceTask: (taskId: string, silenced?: boolean) => Promise<void>;
}) {
const isTerminal = task.status === 'completed' || task.status === 'failed';
const isRead = task.notification_read_at !== null;
return (
<article className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-3">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-medium text-[color:var(--terminal-bright)]">{taskTypeLabel(task.task_type)}</p>
<p className="mt-1 text-xs text-[color:var(--terminal-muted)]">{task.stage_detail ?? task.stage}</p>
<p className="mt-1 text-xs text-[color:var(--terminal-muted)]">
Updated {formatDistanceToNow(new Date(task.updated_at), { addSuffix: true })}
</p>
</div>
<StatusPill status={task.status} />
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
<Button
variant="secondary"
className="px-2 py-1 text-xs"
onClick={() => openTaskDetails(task.id)}
>
Details
</Button>
{isTerminal ? (
<Button
variant="ghost"
className="px-2 py-1 text-xs"
onClick={() => {
void markTaskRead(task.id, !isRead);
}}
>
{isRead ? <EyeOff className="size-3" /> : <CheckCheck className="size-3" />}
{isRead ? 'Mark unread' : 'Mark read'}
</Button>
) : (
<Button
variant="ghost"
className="px-2 py-1 text-xs"
onClick={() => {
void silenceTask(task.id, true);
}}
>
<BellOff className="size-3" />
Silence
</Button>
)}
</div>
</article>
);
}
export function TaskNotificationsDrawer({
isOpen,
onClose,
activeTasks,
visibleFinishedTasks,
awaitingReviewTasks,
showReadFinished,
setShowReadFinished,
openTaskDetails,
markTaskRead,
silenceTask
}: TaskNotificationsDrawerProps) {
if (!isOpen) {
return null;
}
return (
<div className="fixed inset-0 z-50">
<button
type="button"
aria-label="Close notifications drawer"
className="absolute inset-0 bg-[color:rgba(0,0,0,0.62)]"
onClick={onClose}
/>
<aside className="absolute right-0 top-0 h-full w-full max-w-[28rem] border-l border-[color:var(--line-weak)] bg-[color:var(--panel)] p-4 shadow-[0_25px_60px_rgba(0,0,0,0.5)]">
<header className="mb-4 flex items-center justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Notifications box</p>
<h3 className="text-lg font-semibold text-[color:var(--terminal-bright)]">Job inbox</h3>
</div>
<button
type="button"
aria-label="Close notifications drawer"
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-[color:var(--line-weak)] text-[color:var(--terminal-bright)] transition hover:border-[color:var(--line-strong)]"
onClick={onClose}
>
<X className="size-4" />
</button>
</header>
<div className="mb-4 rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2">
<label className="flex items-center justify-between gap-2 text-sm text-[color:var(--terminal-bright)]">
<span>Show read finished jobs</span>
<input
type="checkbox"
checked={showReadFinished}
onChange={(event) => setShowReadFinished(event.target.checked)}
className="h-4 w-4 accent-[color:var(--accent)]"
/>
</label>
<p className="mt-1 text-xs text-[color:var(--terminal-muted)]">Unread finished: {awaitingReviewTasks.length}</p>
</div>
<section className="mb-4">
<h4 className="mb-2 text-xs uppercase tracking-[0.14em] text-[color:var(--terminal-muted)]">Active jobs</h4>
<div className="space-y-2">
{activeTasks.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No active jobs.</p>
) : (
activeTasks.map((task) => (
<TaskRow
key={task.id}
task={task}
openTaskDetails={openTaskDetails}
markTaskRead={markTaskRead}
silenceTask={silenceTask}
/>
))
)}
</div>
</section>
<section className="h-[calc(100%-19rem)] overflow-y-auto">
<h4 className="mb-2 text-xs uppercase tracking-[0.14em] text-[color:var(--terminal-muted)]">Awaiting review</h4>
<div className="space-y-2">
{visibleFinishedTasks.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No finished jobs to review.</p>
) : (
visibleFinishedTasks.map((task) => (
<TaskRow
key={task.id}
task={task}
openTaskDetails={openTaskDetails}
markTaskRead={markTaskRead}
silenceTask={silenceTask}
/>
))
)}
</div>
</section>
</aside>
</div>
);
}