Kyle Pope e3ecc11a21 Redesign Reminders page to match Todos compact list pattern
- Compact h-16 header with segmented filter, search input
- Stat cards (Active/Overdue/Dismissed) with semantic colors
- New ReminderItem component: single-line rows with grouped sections
- Optimistic delete, 2-second confirm pattern, dismiss action
- Mark Stage 4 Reminders as completed in ui_refresh.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:40:29 +08:00

110 lines
3.2 KiB
TypeScript

import { useMemo } from 'react';
import { Bell } from 'lucide-react';
import { parseISO, isPast, isToday, compareAsc } from 'date-fns';
import type { Reminder } from '@/types';
import { EmptyState } from '@/components/ui/empty-state';
import ReminderItem from './ReminderItem';
interface ReminderListProps {
reminders: Reminder[];
onEdit: (reminder: Reminder) => void;
onAdd: () => void;
}
interface ReminderGroup {
key: string;
label: string;
reminders: Reminder[];
}
function sortByRemindAt(a: Reminder, b: Reminder): number {
if (!a.remind_at && !b.remind_at) return 0;
if (!a.remind_at) return 1;
if (!b.remind_at) return -1;
return compareAsc(parseISO(a.remind_at), parseISO(b.remind_at));
}
export default function ReminderList({ reminders, onEdit, onAdd }: ReminderListProps) {
const groups = useMemo(() => {
const overdue: Reminder[] = [];
const today: Reminder[] = [];
const upcoming: Reminder[] = [];
const noDate: Reminder[] = [];
const dismissed: Reminder[] = [];
for (const reminder of reminders) {
if (reminder.is_dismissed) {
dismissed.push(reminder);
continue;
}
if (!reminder.remind_at) {
noDate.push(reminder);
continue;
}
const date = parseISO(reminder.remind_at);
if (isToday(date)) {
today.push(reminder);
} else if (isPast(date)) {
overdue.push(reminder);
} else {
upcoming.push(reminder);
}
}
overdue.sort(sortByRemindAt);
today.sort(sortByRemindAt);
upcoming.sort(sortByRemindAt);
const result: ReminderGroup[] = [];
if (overdue.length > 0) result.push({ key: 'overdue', label: 'Overdue', reminders: overdue });
if (today.length > 0) result.push({ key: 'today', label: 'Today', reminders: today });
if (upcoming.length > 0) result.push({ key: 'upcoming', label: 'Upcoming', reminders: upcoming });
if (noDate.length > 0) result.push({ key: 'no-date', label: 'No Date', reminders: noDate });
if (dismissed.length > 0) result.push({ key: 'dismissed', label: 'Dismissed', reminders: dismissed });
return result;
}, [reminders]);
if (reminders.length === 0) {
return (
<EmptyState
icon={Bell}
title="No reminders"
description="Create a reminder so you never miss an important date or event."
actionLabel="Add Reminder"
onAction={onAdd}
/>
);
}
if (groups.length === 1) {
return (
<div className="space-y-0.5">
{groups[0].reminders.map((reminder) => (
<ReminderItem key={reminder.id} reminder={reminder} onEdit={onEdit} />
))}
</div>
);
}
return (
<div className="space-y-5">
{groups.map((group) => (
<div key={group.key}>
<h3 className="text-xs uppercase tracking-wider text-muted-foreground mb-2 px-1">
{group.label}
<span className="ml-1.5 tabular-nums">({group.reminders.length})</span>
</h3>
<div className="space-y-0.5">
{group.reminders.map((reminder) => (
<ReminderItem key={reminder.id} reminder={reminder} onEdit={onEdit} />
))}
</div>
</div>
))}
</div>
);
}