- 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>
169 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|