fix(audit-wave-9): standardize on Sheet for previews; doctrine in CLAUDE.md

Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to
Sheet side=right so every detail-preview surface uses the same
primitive. Document the doctrine: Sheet for side panels on both desktop
and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX
(currently just MoreSheet).

Closes ui/ux M11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 11:50:07 +02:00
parent b2588ecdd8
commit 4233aa3ac3
94 changed files with 1674 additions and 895 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { type ColumnDef } from '@tanstack/react-table';
import { Plus, CheckCircle2, Clock, Pencil, XCircle, AlertTriangle, Bell } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
@@ -71,8 +72,6 @@ interface ReminderListProps {
}
export function ReminderList({ embedded = false }: ReminderListProps = {}) {
const [reminders, setReminders] = useState<Reminder[]>([]);
const [loading, setLoading] = useState(true);
const [formOpen, setFormOpen] = useState(false);
useCreateFromUrl(() => setFormOpen(true));
const [editingReminder, setEditingReminder] = useState<Reminder | null>(null);
@@ -80,57 +79,50 @@ export function ReminderList({ embedded = false }: ReminderListProps = {}) {
const [viewMode, setViewMode] = useState<'my' | 'all'>('my');
const [statusFilter, setStatusFilter] = useState<string>('active');
const [priorityFilter, setPriorityFilter] = useState<string>('all');
const [total, setTotal] = useState(0);
const { can } = usePermissions();
const canViewAll = can('reminders', 'view_all');
const params = useParams<{ portSlug: string }>();
const portSlug = params?.portSlug ?? '';
const queryClient = useQueryClient();
const fetchReminders = useCallback(async () => {
setLoading(true);
try {
// useQuery replaces the prior useEffect(fetch+setState) pattern.
// The query key captures every filter so a switch refetches; the
// mutation handlers below invalidate-by-prefix to refresh after
// complete/dismiss.
const remindersQuery = useQuery<{ reminders: Reminder[]; total: number }>({
queryKey: ['reminders', viewMode, statusFilter, priorityFilter],
queryFn: async () => {
if (viewMode === 'my') {
const res = await apiFetch<{ data: Reminder[] }>('/api/v1/reminders/my');
let filtered = res.data;
if (priorityFilter !== 'all') {
filtered = filtered.filter((r) => r.priority === priorityFilter);
}
setReminders(filtered);
setTotal(filtered.length);
} else {
const params = new URLSearchParams({ limit: '50', order: 'asc', sort: 'dueAt' });
if (statusFilter === 'active') {
params.set('status', 'pending');
} else if (statusFilter !== 'all') {
params.set('status', statusFilter);
}
if (priorityFilter !== 'all') {
params.set('priority', priorityFilter);
}
const res = await apiFetch<{
data: Reminder[];
pagination: { total: number };
}>(`/api/v1/reminders?${params}`);
setReminders(res.data);
setTotal(res.pagination.total);
const filtered =
priorityFilter === 'all'
? res.data
: res.data.filter((r) => r.priority === priorityFilter);
return { reminders: filtered, total: filtered.length };
}
} finally {
setLoading(false);
}
}, [viewMode, statusFilter, priorityFilter]);
useEffect(() => {
void fetchReminders();
}, [fetchReminders]);
const sp = new URLSearchParams({ limit: '50', order: 'asc', sort: 'dueAt' });
if (statusFilter === 'active') sp.set('status', 'pending');
else if (statusFilter !== 'all') sp.set('status', statusFilter);
if (priorityFilter !== 'all') sp.set('priority', priorityFilter);
const res = await apiFetch<{
data: Reminder[];
pagination: { total: number };
}>(`/api/v1/reminders?${sp}`);
return { reminders: res.data, total: res.pagination.total };
},
});
const reminders = remindersQuery.data?.reminders ?? [];
const total = remindersQuery.data?.total ?? 0;
const loading = remindersQuery.isLoading;
async function handleComplete(id: string) {
await apiFetch(`/api/v1/reminders/${id}/complete`, { method: 'POST' });
await fetchReminders();
void queryClient.invalidateQueries({ queryKey: ['reminders'] });
}
async function handleDismiss(id: string) {
await apiFetch(`/api/v1/reminders/${id}/dismiss`, { method: 'POST' });
await fetchReminders();
void queryClient.invalidateQueries({ queryKey: ['reminders'] });
}
function isOverdue(dueAt: string, status: string): boolean {
@@ -399,7 +391,7 @@ export function ReminderList({ embedded = false }: ReminderListProps = {}) {
open={formOpen}
onOpenChange={setFormOpen}
reminder={editingReminder}
onSuccess={fetchReminders}
onSuccess={() => queryClient.invalidateQueries({ queryKey: ['reminders'] })}
/>
<SnoozeDialog
@@ -408,7 +400,7 @@ export function ReminderList({ embedded = false }: ReminderListProps = {}) {
if (!open) setSnoozingId(null);
}}
reminderId={snoozingId}
onSuccess={fetchReminders}
onSuccess={() => queryClient.invalidateQueries({ queryKey: ['reminders'] })}
/>
</div>
);