error handling

This commit is contained in:
null 2026-05-28 02:34:24 -05:00
parent e8218a3dd8
commit 1426ee3bb5
4 changed files with 85 additions and 24 deletions

View File

@ -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() {
<div className="table-surface overflow-hidden rounded-xl">
{loading ? (
<div className="py-16 text-center text-sm text-muted-foreground">Loading...</div>
) : loadError ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<AlertCircle className="h-8 w-8 text-destructive mb-3" />
<p className="text-sm font-medium text-foreground">Failed to load categories</p>
<p className="mt-1 text-xs text-muted-foreground">{loadError}</p>
<Button size="sm" variant="outline" onClick={load}
className="mt-4 gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50">
<RefreshCw className="h-3 w-3" />
Try again
</Button>
</div>
) : visibleCategories.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 px-4 py-16 text-center text-sm text-muted-foreground">
<span>

View File

@ -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';
@ -219,15 +219,20 @@ export default function SettingsPage() {
const [settings, setSettings] = useState(DEFAULTS);
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 (
<div className="flex flex-col items-center justify-center py-24 text-center rounded-xl border border-destructive/20 bg-destructive/5">
<AlertCircle className="h-10 w-10 text-destructive mb-3" />
<p className="text-sm font-medium text-foreground">Failed to load settings</p>
<p className="mt-1 text-xs text-muted-foreground">{loadError}</p>
<Button size="sm" variant="outline" onClick={loadSettings}
className="mt-4 gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50">
<RefreshCw className="h-3 w-3" />
Try again
</Button>
</div>
);
}
return (
<div>

View File

@ -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 (
<div className="flex flex-col items-center justify-center py-24 text-center rounded-xl border border-destructive/20 bg-destructive/5">
<AlertCircle className="h-10 w-10 text-destructive mb-3" />
<p className="text-sm font-medium text-foreground">Failed to load snowball data</p>
<p className="mt-1 text-xs text-muted-foreground">{loadError}</p>
<Button size="sm" variant="outline" onClick={load}
className="mt-4 gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50">
<RefreshCw className="h-3 w-3" />
Try again
</Button>
</div>
);
}
const inp = 'bg-background/50 border-border/60 h-9 text-sm font-mono';
return (

View File

@ -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() {
</div>
)}
{/* ── Fetch error state ── */}
{isError && (
<div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-destructive/20 bg-destructive/5">
<div className="h-10 w-10 rounded-full bg-destructive/10 flex items-center justify-center mb-3">
<AlertCircle className="h-5 w-5 text-destructive" />
</div>
<p className="text-sm font-medium text-foreground">Failed to load tracker data</p>
<p className="mt-1 text-xs text-muted-foreground">{error?.message || 'An unexpected error occurred.'}</p>
<Button
size="sm"
variant="outline"
onClick={() => refetch()}
className="mt-4 gap-1.5 text-xs border-destructive/30 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/50"
>
<RefreshCw className="h-3 w-3" />
Try again
</Button>
</div>
)}
{/* ── Empty state ── */}
{rows.length === 0 && data !== null && (
{!isError && rows.length === 0 && data !== null && (
<div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-border bg-muted/20">
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center mb-3">
<CheckCircle2 className="h-5 w-5 text-muted-foreground" />
@ -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 && (
<div className="space-y-5" aria-busy="true">
<div className="rounded-xl border border-border overflow-hidden bg-card p-4">
<div className="flex items-center justify-between mb-4">
@ -2425,8 +2436,8 @@ export default function TrackerPage() {
</div>
</div>
)}
{first.length > 0 && <Bucket label="1st 14th" rows={first} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
{second.length > 0 && <Bucket label="15th 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
{!isError && first.length > 0 && <Bucket label="1st 14th" rows={first} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
{!isError && second.length > 0 && <Bucket label="15th 31st" rows={second} year={year} month={month} refresh={refetch} onEditBill={handleOpenEditBill} loading={loading} />}
{/* Edit Bill modal — opened by clicking a bill name in any tracker row */}
{editBillData && (