UMBRA/frontend/src/components/reminders/RemindersPage.tsx
Kyle Pope 898ecc407a Stage 7: final polish — transitions, navigation, calendar panel
- Add animate-fade-in page transitions to all pages
- Persist sidebar collapsed state in localStorage
- Add two-click logout confirmation using useConfirmAction
- Restructure Todos header: replace <select> with pill filters, move search right
- Move Reminders search right-aligned with spacer
- Add event search dropdown + Create Event button to Calendar toolbar
- Add search input to Projects header with name/description filtering
- Fix CategoryFilterBar search focus ring clipping with ring-inset
- Create EventDetailPanel: read-only event view with copyable fields,
  recurrence display, edit/delete actions, location name resolution
- Refactor CalendarPage to 55/45 split-panel layout matching People/Locations
- Add mobile overlay panel for calendar event details
- Add navigation state handler for CalendarPage (date/view from dashboard)
- Add navigation state handler for ProjectsPage (status filter from dashboard)
- Make all dashboard widgets navigable: stat cards → pages, week timeline
  days → calendar day view, upcoming items → source pages, countdown items
  → calendar, today's events/todos/reminders → respective pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:08:08 +08:00

169 lines
6.2 KiB
TypeScript

import { useState, useMemo } from 'react';
import { Plus, Bell, BellOff, AlertCircle, Search } from 'lucide-react';
import { useQuery } from '@tanstack/react-query';
import { isPast, isToday, parseISO } from 'date-fns';
import api from '@/lib/api';
import type { Reminder } from '@/types';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
import { ListSkeleton } from '@/components/ui/skeleton';
import ReminderList from './ReminderList';
import ReminderForm from './ReminderForm';
const statusFilters = [
{ value: 'active', label: 'Active' },
{ value: 'dismissed', label: 'Dismissed' },
{ value: 'all', label: 'All' },
] as const;
type StatusFilter = (typeof statusFilters)[number]['value'];
export default function RemindersPage() {
const [showForm, setShowForm] = useState(false);
const [editingReminder, setEditingReminder] = useState<Reminder | null>(null);
const [filter, setFilter] = useState<StatusFilter>('active');
const [search, setSearch] = useState('');
const { data: reminders = [], isLoading } = useQuery({
queryKey: ['reminders'],
queryFn: async () => {
const { data } = await api.get<Reminder[]>('/reminders');
return data;
},
});
const filteredReminders = useMemo(
() =>
reminders.filter((r) => {
if (filter === 'active' && r.is_dismissed) return false;
if (filter === 'dismissed' && !r.is_dismissed) return false;
if (search && !r.title.toLowerCase().includes(search.toLowerCase())) return false;
return true;
}),
[reminders, filter, search]
);
const activeCount = reminders.filter((r) => !r.is_dismissed).length;
const overdueCount = reminders.filter(
(r) => !r.is_dismissed && r.remind_at && isPast(parseISO(r.remind_at)) && !isToday(parseISO(r.remind_at))
).length;
const dismissedCount = reminders.filter((r) => r.is_dismissed).length;
const handleEdit = (reminder: Reminder) => {
setEditingReminder(reminder);
setShowForm(true);
};
const handleCloseForm = () => {
setShowForm(false);
setEditingReminder(null);
};
return (
<div className="flex flex-col h-full animate-fade-in">
{/* Header */}
<div className="border-b bg-card px-6 h-16 flex items-center gap-4 shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Reminders</h1>
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4">
{statusFilters.map((sf) => (
<button
key={sf.value}
onClick={() => setFilter(sf.value)}
className={`px-3 py-1.5 text-sm font-medium transition-colors duration-150 ${
filter === sf.value
? 'bg-accent/15 text-accent'
: 'text-muted-foreground hover:text-foreground hover:bg-card-elevated'
}`}
style={{
backgroundColor:
filter === sf.value ? 'hsl(var(--accent-color) / 0.15)' : undefined,
color: filter === sf.value ? 'hsl(var(--accent-color))' : undefined,
}}
>
{sf.label}
</button>
))}
</div>
<div className="flex-1" />
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
placeholder="Search..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-52 h-8 pl-8 text-sm"
/>
</div>
<Button onClick={() => setShowForm(true)} size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Reminder
</Button>
</div>
<div className="flex-1 overflow-y-auto px-6 py-5">
{/* Summary stats */}
{!isLoading && reminders.length > 0 && (
<div className="grid gap-2.5 grid-cols-3 mb-5">
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3">
<div className="p-1.5 rounded-md bg-orange-500/10">
<Bell className="h-4 w-4 text-orange-400" />
</div>
<div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">
Active
</p>
<p className="font-heading text-xl font-bold tabular-nums">{activeCount}</p>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3">
<div className="p-1.5 rounded-md bg-red-500/10">
<AlertCircle className="h-4 w-4 text-red-400" />
</div>
<div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">
Overdue
</p>
<p className="font-heading text-xl font-bold tabular-nums">{overdueCount}</p>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3">
<div className="p-1.5 rounded-md bg-gray-500/10">
<BellOff className="h-4 w-4 text-gray-400" />
</div>
<div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">
Dismissed
</p>
<p className="font-heading text-xl font-bold tabular-nums">{dismissedCount}</p>
</div>
</CardContent>
</Card>
</div>
)}
{isLoading ? (
<ListSkeleton rows={6} />
) : (
<ReminderList
reminders={filteredReminders}
onEdit={handleEdit}
onAdd={() => setShowForm(true)}
/>
)}
</div>
{showForm && <ReminderForm reminder={editingReminder} onClose={handleCloseForm} />}
</div>
);
}