From c08df1c45bd0de2c40f909083f244cdf4a580a57 Mon Sep 17 00:00:00 2001 From: jaydenkalu Date: Mon, 29 Jun 2026 12:20:47 +0000 Subject: [PATCH] feat: normalize API error messaging across pages (#469) - Add src/lib/api-error.ts: parseApiError() normalises {error,code,retriable} response shapes; apiFetch() wrapper throws human-readable error strings - meters/page.tsx: replace inline error state with useToast pushToast() - registerMeter/revokeMeter use apiFetch (consistent error extraction) - onError/onSuccess push toast instead of setting local error state - Remove redundant inline error

and

blocks Closes #469 --- apps/web/src/app/meters/page.tsx | 46 +++++++++----------------------- apps/web/src/lib/api-error.ts | 40 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/lib/api-error.ts diff --git a/apps/web/src/app/meters/page.tsx b/apps/web/src/app/meters/page.tsx index 0417ae8..3ec209c 100644 --- a/apps/web/src/app/meters/page.tsx +++ b/apps/web/src/app/meters/page.tsx @@ -5,6 +5,8 @@ import { WalletGate } from '@/components/wallet-gate' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { PlusCircle, ShieldOff } from 'lucide-react' import { CopyableText } from '@/components/copy-button' +import { useToast } from '@/components/ToastProvider' +import { apiFetch } from '@/lib/api-error' interface Meter { id: string @@ -19,8 +21,7 @@ interface Meter { } async function fetchMeters(): Promise { - const res = await fetch('/api/meters') - if (!res.ok) throw new Error('Failed to load meters') + const res = await apiFetch('/api/meters') return res.json() } @@ -31,27 +32,23 @@ async function registerMeter(body: { meter_group?: string tags?: string[] }): Promise { - const res = await fetch('/api/meters', { + const res = await apiFetch('/api/meters', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) - if (!res.ok) { - const err = await res.json().catch(() => ({})) - throw new Error(err.error ?? 'Registration failed') - } return res.json() } async function revokeMeter(id: string): Promise { - const res = await fetch(`/api/meters/${id}/revoke`, { method: 'PATCH' }) - if (!res.ok) throw new Error('Revoke failed') + await apiFetch(`/api/meters/${id}/revoke`, { method: 'PATCH' }) } // --------------------------------------------------------------------------- // Register form // --------------------------------------------------------------------------- function RegisterForm({ onSuccess }: { onSuccess: () => void }) { + const { pushToast } = useToast() const [form, setForm] = useState({ name: '', serial_number: '', @@ -59,7 +56,6 @@ function RegisterForm({ onSuccess }: { onSuccess: () => void }) { meter_group: '', tags: '', }) - const [error, setError] = useState('') const mutation = useMutation({ mutationFn: (data: typeof form) => @@ -74,22 +70,15 @@ function RegisterForm({ onSuccess }: { onSuccess: () => void }) { meter_group: data.meter_group || undefined, }), onSuccess: () => { - setForm({ - name: '', - serial_number: '', - pubkey_hex: '', - meter_group: '', - tags: '', - }) - setError('') + setForm({ name: '', serial_number: '', pubkey_hex: '', meter_group: '', tags: '' }) + pushToast({ variant: 'success', title: 'Meter registered', description: 'The new meter is now active.' }) onSuccess() }, - onError: (err: Error) => setError(err.message), + onError: (err: Error) => pushToast({ variant: 'error', title: 'Registration failed', description: err.message }), }) function handleSubmit(e: React.FormEvent) { e.preventDefault() - setError('') mutation.mutate(form) } @@ -103,12 +92,6 @@ function RegisterForm({ onSuccess }: { onSuccess: () => void }) { Register new meter - {error && ( -
- {error} -
- )} -
- {error && ( -

- {error} -

- )} -