Kyle Pope 38bce21ac3 Polish toast actions and extend delete confirm timeout
- Toast: snooze/dismiss buttons side-by-side on the right, dismiss
  uses X icon, snooze shows clock icon + 'Snooze' label
- SnoozeDropdown trigger now shows text label alongside icon
- Delete confirm 'Sure?' lingers 4 seconds instead of 2

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

174 lines
5.2 KiB
TypeScript

import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { Bell, BellOff, Trash2, Pencil } from 'lucide-react';
import { format, isPast, isToday, parseISO } from 'date-fns';
import api from '@/lib/api';
import type { Reminder } from '@/types';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
interface ReminderItemProps {
reminder: Reminder;
onEdit: (reminder: Reminder) => void;
}
const recurrenceLabels: Record<string, string> = {
daily: 'Daily',
weekly: 'Weekly',
monthly: 'Monthly',
};
const QUERY_KEYS = [['reminders'], ['dashboard'], ['upcoming']] as const;
export default function ReminderItem({ reminder, onEdit }: ReminderItemProps) {
const queryClient = useQueryClient();
const [confirmingDelete, setConfirmingDelete] = useState(false);
const remindDate = reminder.remind_at ? parseISO(reminder.remind_at) : null;
const isOverdue = !reminder.is_dismissed && remindDate && isPast(remindDate) && !isToday(remindDate);
const isDueToday = remindDate ? isToday(remindDate) : false;
const dismissMutation = useMutation({
mutationFn: async () => {
const { data } = await api.patch(`/reminders/${reminder.id}/dismiss`);
return data;
},
onSuccess: () => {
QUERY_KEYS.forEach((key) => queryClient.invalidateQueries({ queryKey: [...key] }));
toast.success('Reminder dismissed');
},
onError: () => {
toast.error('Failed to dismiss reminder');
},
});
const deleteMutation = useMutation({
mutationFn: async () => {
await api.delete(`/reminders/${reminder.id}`);
},
onMutate: async () => {
await queryClient.cancelQueries({ queryKey: ['reminders'] });
const previous = queryClient.getQueryData<Reminder[]>(['reminders']);
queryClient.setQueryData<Reminder[]>(['reminders'], (old) =>
old ? old.filter((r) => r.id !== reminder.id) : []
);
return { previous };
},
onSuccess: () => {
QUERY_KEYS.forEach((key) => queryClient.invalidateQueries({ queryKey: [...key] }));
toast.success('Reminder deleted');
},
onError: (_err, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(['reminders'], context.previous);
}
toast.error('Failed to delete reminder');
},
});
const handleDelete = () => {
if (!confirmingDelete) {
setConfirmingDelete(true);
setTimeout(() => setConfirmingDelete(false), 4000);
return;
}
deleteMutation.mutate();
setConfirmingDelete(false);
};
return (
<div
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md transition-colors duration-150',
'hover:bg-card-elevated',
reminder.is_dismissed && 'opacity-50'
)}
>
<Bell
className={cn(
'h-4 w-4 shrink-0',
isOverdue
? 'text-red-400'
: reminder.is_dismissed
? 'text-muted-foreground'
: 'text-orange-400'
)}
/>
<span
className={cn(
'text-sm font-medium truncate flex-1 min-w-0 cursor-pointer',
reminder.is_dismissed && 'line-through text-muted-foreground'
)}
onClick={() => onEdit(reminder)}
>
{reminder.title}
</span>
{reminder.recurrence_rule && (
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium uppercase tracking-wide bg-purple-500/15 text-purple-400 shrink-0">
{recurrenceLabels[reminder.recurrence_rule] || reminder.recurrence_rule}
</span>
)}
{remindDate && (
<span
className={cn(
'text-[11px] shrink-0',
isOverdue ? 'text-red-400' : isDueToday ? 'text-yellow-400' : 'text-muted-foreground'
)}
>
{format(remindDate, 'MMM d, h:mm a')}
</span>
)}
{!reminder.is_dismissed && (
<Button
variant="ghost"
size="icon"
onClick={() => dismissMutation.mutate()}
disabled={dismissMutation.isPending}
className="h-7 w-7 shrink-0 hover:bg-orange-500/10 hover:text-orange-400"
aria-label="Dismiss reminder"
>
<BellOff className="h-3 w-3" />
</Button>
)}
<Button
variant="ghost"
size="icon"
onClick={() => onEdit(reminder)}
className="h-7 w-7 shrink-0"
aria-label="Edit reminder"
>
<Pencil className="h-3 w-3" />
</Button>
{confirmingDelete ? (
<Button
variant="ghost"
aria-label="Confirm delete"
onClick={handleDelete}
disabled={deleteMutation.isPending}
className="h-7 shrink-0 px-2 bg-destructive/20 text-destructive text-[11px] font-medium"
>
Sure?
</Button>
) : (
<Button
variant="ghost"
size="icon"
aria-label="Delete reminder"
onClick={handleDelete}
disabled={deleteMutation.isPending}
className="h-7 w-7 shrink-0 hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div>
);
}