From 1426ee3bb5366835834f87910f28d7a910b84b6d Mon Sep 17 00:00:00 2001 From: null Date: Thu, 28 May 2026 02:34:24 -0500 Subject: [PATCH] error handling --- client/pages/CategoriesPage.jsx | 17 ++++++++++++-- client/pages/SettingsPage.jsx | 32 ++++++++++++++++++++++----- client/pages/SnowballPage.jsx | 21 ++++++++++++++++-- client/pages/TrackerPage.jsx | 39 +++++++++++++++++++++------------ 4 files changed, 85 insertions(+), 24 deletions(-) diff --git a/client/pages/CategoriesPage.jsx b/client/pages/CategoriesPage.jsx index 9b2ba26..c738bb9 100644 --- a/client/pages/CategoriesPage.jsx +++ b/client/pages/CategoriesPage.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { toast } from 'sonner'; import { - ChevronDown, Plus, Pencil, Trash2, ReceiptText, + ChevronDown, Plus, Pencil, Trash2, ReceiptText, AlertCircle, RefreshCw, } from 'lucide-react'; import { api } from '@/api.js'; import { Button, buttonVariants } from '@/components/ui/button'; @@ -231,6 +231,7 @@ function ExpandedBills({ category }) { export default function CategoriesPage() { const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); const [newName, setNewName] = useState(''); const [adding, setAdding] = useState(false); const [expanded, setExpanded] = useState(() => new Set()); @@ -243,11 +244,12 @@ export default function CategoriesPage() { const [deleting, setDeleting] = useState(false); const load = useCallback(async () => { + setLoadError(null); try { const cats = await api.categories(); setCategories(cats); } catch (err) { - toast.error(err.message); + setLoadError(err.message || 'Failed to load categories'); } finally { setLoading(false); } @@ -412,6 +414,17 @@ export default function CategoriesPage() {
{loading ? (
Loading...
+ ) : loadError ? ( +
+ +

Failed to load categories

+

{loadError}

+ +
) : visibleCategories.length === 0 ? (
diff --git a/client/pages/SettingsPage.jsx b/client/pages/SettingsPage.jsx index 70a728b..a87b17f 100644 --- a/client/pages/SettingsPage.jsx +++ b/client/pages/SettingsPage.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { Sun, Moon, Users } from 'lucide-react'; +import { Sun, Moon, Users, AlertCircle, RefreshCw } from 'lucide-react'; import { api } from '@/api'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; @@ -218,16 +218,21 @@ export default function SettingsPage() { }; const [settings, setSettings] = useState(DEFAULTS); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); + const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); + const [saving, setSaving] = useState(false); - useEffect(() => { + const loadSettings = useCallback(() => { + setLoading(true); + setLoadError(null); api.settings() .then((d) => setSettings({ ...DEFAULTS, ...d })) - .catch(() => {}) + .catch((err) => setLoadError(err.message || 'Failed to load settings')) .finally(() => setLoading(false)); }, []); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { loadSettings(); }, [loadSettings]); + const set = (k, v) => setSettings((p) => ({ ...p, [k]: v })); const handleSave = async () => { @@ -254,6 +259,21 @@ export default function SettingsPage() { ); } + if (loadError) { + return ( +
+ +

Failed to load settings

+

{loadError}

+ +
+ ); + } + return (
diff --git a/client/pages/SnowballPage.jsx b/client/pages/SnowballPage.jsx index b02c042..a72cb91 100644 --- a/client/pages/SnowballPage.jsx +++ b/client/pages/SnowballPage.jsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { GripVertical, TrendingDown, Zap, Save, CalendarCheck, AlertTriangle, PenLine, EyeOff, CheckCircle2, Circle } from 'lucide-react'; +import { GripVertical, TrendingDown, Zap, Save, CalendarCheck, AlertTriangle, PenLine, EyeOff, CheckCircle2, Circle, AlertCircle, RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { Button } from '@/components/ui/button'; @@ -425,6 +425,7 @@ export default function SnowballPage() { const [bills, setBills] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(null); const [saving, setSaving] = useState(false); const [dirty, setDirty] = useState(false); const [editBill, setEditBill] = useState(null); @@ -454,6 +455,7 @@ export default function SnowballPage() { const load = useCallback(async () => { setLoading(true); + setLoadError(null); try { const [billsArr, catsArr, settings] = await Promise.all([ api.snowball(), api.categories(), api.snowballSettings(), @@ -468,7 +470,7 @@ export default function SnowballPage() { setExtraPayment(ep); extraPaymentRef.current = ep; } catch (err) { - toast.error(err.message || 'Failed to load snowball data'); + setLoadError(err.message || 'Failed to load snowball data'); } finally { setLoading(false); } }, []); @@ -666,6 +668,21 @@ export default function SnowballPage() { ); } + if (loadError) { + return ( +
+ +

Failed to load snowball data

+

{loadError}

+ +
+ ); + } + const inp = 'bg-background/50 border-border/60 h-9 text-sm font-mono'; return ( diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index 25214c7..6844a2c 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback, useRef, useMemo, useTransition } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { ChevronLeft, ChevronRight, Pencil, TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, Loader2, Plus, Search, X } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Pencil, TrendingUp, AlertCircle, Clock, CheckCircle2, Settings2, Loader2, Plus, Search, X, RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api.js'; import { useTracker } from '@/hooks/useQueries'; @@ -2170,15 +2170,6 @@ export default function TrackerPage() { setMonth(n.getMonth() + 1); } - // Handle errors from React Query (use ref to prevent duplicate toasts) - const errorShownRef = useRef(false); - useEffect(() => { - if (isError && !errorShownRef.current) { - toast.error(error?.message || 'Failed to load tracker data'); - errorShownRef.current = true; - } - if (!isError) errorShownRef.current = false; - }, [isError, error]); const rows = data?.rows || []; const summary = data?.summary || {}; @@ -2379,8 +2370,28 @@ export default function TrackerPage() {
)} + {/* ── Fetch error state ── */} + {isError && ( +
+
+ +
+

Failed to load tracker data

+

{error?.message || 'An unexpected error occurred.'}

+ +
+ )} + {/* ── Empty state ── */} - {rows.length === 0 && data !== null && ( + {!isError && rows.length === 0 && data !== null && (
@@ -2393,7 +2404,7 @@ export default function TrackerPage() { )} {/* ── Buckets — year and month passed so Row can open the correct monthly state dialog ── */} - {loading && ( + {!isError && loading && (
@@ -2425,8 +2436,8 @@ export default function TrackerPage() {
)} - {first.length > 0 && } - {second.length > 0 && } + {!isError && first.length > 0 && } + {!isError && second.length > 0 && } {/* Edit Bill modal — opened by clicking a bill name in any tracker row */} {editBillData && (