diff --git a/backend/app/routers/reminders.py b/backend/app/routers/reminders.py
index 9ff6d1b..26d3fc6 100644
--- a/backend/app/routers/reminders.py
+++ b/backend/app/routers/reminders.py
@@ -134,6 +134,10 @@ async def update_reminder(
update_data = reminder_update.model_dump(exclude_unset=True)
+ # Clear stale snooze if remind_at is being changed
+ if 'remind_at' in update_data:
+ reminder.snoozed_until = None
+
for key, value in update_data.items():
setattr(reminder, key, value)
@@ -176,6 +180,7 @@ async def dismiss_reminder(
raise HTTPException(status_code=404, detail="Reminder not found")
reminder.is_dismissed = True
+ reminder.snoozed_until = None
await db.commit()
await db.refresh(reminder)
diff --git a/frontend/src/components/dashboard/AlertBanner.tsx b/frontend/src/components/dashboard/AlertBanner.tsx
index 1ebcad3..2ea3f06 100644
--- a/frontend/src/components/dashboard/AlertBanner.tsx
+++ b/frontend/src/components/dashboard/AlertBanner.tsx
@@ -1,4 +1,5 @@
import { Bell, X } from 'lucide-react';
+import { getRelativeTime } from '@/lib/date-utils';
import type { Reminder } from '@/types';
interface AlertBannerProps {
@@ -55,17 +56,3 @@ export default function AlertBanner({ alerts, onDismiss, onSnooze }: AlertBanner
);
}
-
-function getRelativeTime(dateStr: string): string {
- const date = new Date(dateStr);
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- const diffMins = Math.floor(diffMs / 60000);
-
- if (diffMins < 1) return 'Just now';
- if (diffMins < 60) return `${diffMins}m ago`;
- const diffHours = Math.floor(diffMins / 60);
- if (diffHours < 24) return `${diffHours}h ago`;
- const diffDays = Math.floor(diffHours / 24);
- return `${diffDays}d ago`;
-}
diff --git a/frontend/src/components/dashboard/DashboardPage.tsx b/frontend/src/components/dashboard/DashboardPage.tsx
index 5a05ff8..fa3c202 100644
--- a/frontend/src/components/dashboard/DashboardPage.tsx
+++ b/frontend/src/components/dashboard/DashboardPage.tsx
@@ -239,7 +239,7 @@ export default function DashboardPage() {
{reminder.title}
- {format(new Date(reminder.remind_at), 'MMM d, h:mm a')}
+ {reminder.remind_at ? format(new Date(reminder.remind_at), 'MMM d, h:mm a') : ''}
))}
diff --git a/frontend/src/components/layout/AppLayout.tsx b/frontend/src/components/layout/AppLayout.tsx
index 2c9f924..4b9df6a 100644
--- a/frontend/src/components/layout/AppLayout.tsx
+++ b/frontend/src/components/layout/AppLayout.tsx
@@ -2,36 +2,37 @@ import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Menu } from 'lucide-react';
import { useTheme } from '@/hooks/useTheme';
-import { useAlerts } from '@/hooks/useAlerts';
+import { AlertsProvider } from '@/hooks/useAlerts';
import { Button } from '@/components/ui/button';
import Sidebar from './Sidebar';
export default function AppLayout() {
useTheme();
- useAlerts();
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
return (
-
-
setCollapsed(!collapsed)}
- mobileOpen={mobileOpen}
- onMobileClose={() => setMobileOpen(false)}
- />
-
- {/* Mobile header */}
-
-
-
UMBRA
+
+
+
setCollapsed(!collapsed)}
+ mobileOpen={mobileOpen}
+ onMobileClose={() => setMobileOpen(false)}
+ />
+
+ {/* Mobile header */}
+
+
+
UMBRA
+
+
+
+
-
-
-
-
+
);
}
diff --git a/frontend/src/hooks/useAlerts.tsx b/frontend/src/hooks/useAlerts.tsx
index b824b5f..62f6881 100644
--- a/frontend/src/hooks/useAlerts.tsx
+++ b/frontend/src/hooks/useAlerts.tsx
@@ -1,13 +1,31 @@
-import { useRef, useEffect, useCallback } from 'react';
+import { createContext, useContext, useRef, useEffect, useCallback, type ReactNode } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useLocation } from 'react-router-dom';
import { toast } from 'sonner';
+import { Bell } from 'lucide-react';
import api from '@/lib/api';
+import { getRelativeTime } from '@/lib/date-utils';
import type { Reminder } from '@/types';
const MAX_TOASTS = 3;
+interface AlertsContextValue {
+ alerts: Reminder[];
+ dismiss: (id: number) => void;
+ snooze: (id: number, minutes: 5 | 10 | 15) => void;
+}
+
+const AlertsContext = createContext
({
+ alerts: [],
+ dismiss: () => {},
+ snooze: () => {},
+});
+
export function useAlerts() {
+ return useContext(AlertsContext);
+}
+
+export function AlertsProvider({ children }: { children: ReactNode }) {
const queryClient = useQueryClient();
const location = useLocation();
const firedRef = useRef>(new Set());
@@ -37,29 +55,89 @@ export function useAlerts() {
});
}, [alerts]);
- // Handle route changes
- useEffect(() => {
- const wasOnDashboard = prevPathnameRef.current === '/' || prevPathnameRef.current === '/dashboard';
- const nowOnDashboard = isDashboard;
- prevPathnameRef.current = location.pathname;
+ const dismissMutation = useMutation({
+ mutationFn: (id: number) => api.patch(`/reminders/${id}/dismiss`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['reminders'] });
+ queryClient.invalidateQueries({ queryKey: ['dashboard'] });
+ },
+ });
- if (nowOnDashboard) {
- // Moving TO dashboard — dismiss all toasts, banner takes over
- alerts.forEach((a) => toast.dismiss(`reminder-${a.id}`));
- toast.dismiss('reminder-summary');
- firedRef.current.clear();
- } else if (wasOnDashboard && !nowOnDashboard) {
- // Moving AWAY from dashboard — fire toasts for current alerts
- firedRef.current.clear();
- fireToasts(alerts);
- }
- }, [location.pathname]); // eslint-disable-line react-hooks/exhaustive-deps
+ const snoozeMutation = useMutation({
+ mutationFn: ({ id, minutes }: { id: number; minutes: 5 | 10 | 15 }) =>
+ api.patch(`/reminders/${id}/snooze`, { minutes }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['reminders'] });
+ queryClient.invalidateQueries({ queryKey: ['dashboard'] });
+ },
+ });
- // Fire toasts for new alerts on non-dashboard pages
- useEffect(() => {
- if (isDashboard) return;
- fireToasts(alerts);
- }, [alerts, isDashboard]); // eslint-disable-line react-hooks/exhaustive-deps
+ const handleDismiss = useCallback((id: number) => {
+ toast.dismiss(`reminder-${id}`);
+ toast.dismiss('reminder-summary');
+ firedRef.current.delete(id);
+ dismissMutation.mutate(id);
+ }, [dismissMutation]);
+
+ const handleSnooze = useCallback((id: number, minutes: 5 | 10 | 15) => {
+ toast.dismiss(`reminder-${id}`);
+ toast.dismiss('reminder-summary');
+ firedRef.current.delete(id);
+ snoozeMutation.mutate({ id, minutes });
+ }, [snoozeMutation]);
+
+ // Store latest callbacks in refs so toast closures always call current versions
+ const dismissRef = useRef(handleDismiss);
+ const snoozeRef = useRef(handleSnooze);
+ useEffect(() => { dismissRef.current = handleDismiss; }, [handleDismiss]);
+ useEffect(() => { snoozeRef.current = handleSnooze; }, [handleSnooze]);
+
+ function renderToast(_t: string | number, reminder: Reminder) {
+ const timeAgo = reminder.remind_at ? getRelativeTime(reminder.remind_at) : '';
+ return (
+
+
+
+
+
+
{reminder.title}
+
{timeAgo}
+
+
+ {[5, 10, 15].map((m) => (
+
+ ))}
+
+
+
+
+
+ );
+ }
+
+ function renderSummaryToast(_t: string | number, count: number) {
+ return (
+
+
+
+
+
+ +{count} more reminder{count > 1 ? 's' : ''} due
+
+
+ );
+ }
function fireToasts(reminders: Reminder[]) {
const newAlerts = reminders.filter((a) => !firedRef.current.has(a.id));
@@ -87,102 +165,33 @@ export function useAlerts() {
}
}
- function renderToast(_t: string | number, reminder: Reminder) {
- const timeAgo = reminder.remind_at ? getRelativeTime(reminder.remind_at) : '';
- return (
-
-
-
-
{reminder.title}
-
{timeAgo}
-
-
- {[5, 10, 15].map((m) => (
-
- ))}
-
-
-
-
-
- );
- }
+ // Handle route changes
+ useEffect(() => {
+ const wasOnDashboard = prevPathnameRef.current === '/' || prevPathnameRef.current === '/dashboard';
+ const nowOnDashboard = isDashboard;
+ prevPathnameRef.current = location.pathname;
- function renderSummaryToast(_t: string | number, count: number) {
- return (
-
-
-
- +{count} more reminder{count > 1 ? 's' : ''} due
-
-
- );
- }
+ if (nowOnDashboard) {
+ // Moving TO dashboard — dismiss all toasts, banner takes over
+ alerts.forEach((a) => toast.dismiss(`reminder-${a.id}`));
+ toast.dismiss('reminder-summary');
+ firedRef.current.clear();
+ } else if (wasOnDashboard && !nowOnDashboard) {
+ // Moving AWAY from dashboard — fire toasts for current alerts
+ firedRef.current.clear();
+ fireToasts(alerts);
+ }
+ }, [location.pathname, isDashboard, alerts]);
- const dismissMutation = useMutation({
- mutationFn: (id: number) => api.patch(`/reminders/${id}/dismiss`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['reminders'] });
- queryClient.invalidateQueries({ queryKey: ['dashboard'] });
- },
- });
+ // Fire toasts for new alerts on non-dashboard pages
+ useEffect(() => {
+ if (isDashboard) return;
+ fireToasts(alerts);
+ }, [alerts, isDashboard]);
- const snoozeMutation = useMutation({
- mutationFn: ({ id, minutes }: { id: number; minutes: 5 | 10 | 15 }) =>
- api.patch(`/reminders/${id}/snooze`, { minutes }),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['reminders'] });
- queryClient.invalidateQueries({ queryKey: ['dashboard'] });
- },
- });
-
- const handleDismiss = useCallback((id: number) => {
- toast.dismiss(`reminder-${id}`);
- firedRef.current.delete(id);
- dismissMutation.mutate(id);
- // If summary toast exists and we're reducing count, dismiss it too — next poll will re-evaluate
- toast.dismiss('reminder-summary');
- }, [dismissMutation]);
-
- const handleSnooze = useCallback((id: number, minutes: 5 | 10 | 15) => {
- toast.dismiss(`reminder-${id}`);
- firedRef.current.delete(id);
- snoozeMutation.mutate({ id, minutes });
- toast.dismiss('reminder-summary');
- }, [snoozeMutation]);
-
- return { alerts, dismiss: handleDismiss, snooze: handleSnooze };
-}
-
-function getRelativeTime(dateStr: string): string {
- const date = new Date(dateStr);
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- const diffMins = Math.floor(diffMs / 60000);
-
- if (diffMins < 1) return 'Just now';
- if (diffMins < 60) return `${diffMins}m ago`;
- const diffHours = Math.floor(diffMins / 60);
- if (diffHours < 24) return `${diffHours}h ago`;
- const diffDays = Math.floor(diffHours / 24);
- return `${diffDays}d ago`;
+ return (
+
+ {children}
+
+ );
}
diff --git a/frontend/src/lib/date-utils.ts b/frontend/src/lib/date-utils.ts
new file mode 100644
index 0000000..48fcea8
--- /dev/null
+++ b/frontend/src/lib/date-utils.ts
@@ -0,0 +1,13 @@
+export function getRelativeTime(dateStr: string): string {
+ const date = new Date(dateStr);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+
+ if (diffMins < 1) return 'Just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ const diffHours = Math.floor(diffMins / 60);
+ if (diffHours < 24) return `${diffHours}h ago`;
+ const diffDays = Math.floor(diffHours / 24);
+ return `${diffDays}d ago`;
+}