import { useState, type ComponentType, type ReactNode } from 'react'; import { toast } from 'sonner'; import { Landmark, RefreshCw, Loader2, AlertTriangle, RotateCcw, Upload, Plus } from 'lucide-react'; import { api } from '@/api'; import { cn, errMessage } from '@/lib/utils'; import { Button } from '@/components/ui/button'; // Relative "2h ago" / "3d ago"; returns null for empty input. function relativeTime(iso: string | null | undefined): string | null { if (!iso) return null; const then = new Date(iso).getTime(); if (Number.isNaN(then)) return null; const secs = Math.max(0, Math.round((Date.now() - then) / 1000)); if (secs < 60) return 'just now'; const mins = Math.round(secs / 60); if (mins < 60) return `${mins}m ago`; const hrs = Math.round(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.round(hrs / 24); return `${days}d ago`; } function HeroShell({ tone = 'default', icon: Icon, children }: { tone?: string; icon: ComponentType<{ className?: string }>; children: ReactNode; }) { const toneRing = ({ default: 'border-border/60', good: 'border-emerald-500/30', warn: 'border-amber-500/40', error: 'border-rose-500/30', } as Record)[tone]; const toneChip = ({ default: 'bg-muted/50 text-muted-foreground', good: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400', warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-400', error: 'bg-rose-500/10 text-rose-600 dark:text-rose-400', } as Record)[tone]; return (
{children}
); } interface ConnData { name?: string | null; last_sync_at?: string | null; last_error?: string | null; } interface ConnectionHeroProps { loading?: boolean; error?: unknown; // truthy when the status/summary fetch failed enabled?: boolean; // status.enabled (server feature flag) hasConnections?: boolean; // status.has_connections conn?: ConnData | null; // the simplefin data_source (name, last_sync_at, last_error) or null txnTotal?: number | null; // total synced transactions (at-a-glance), or null onRetry?: () => void; onGoTo?: (sectionId: string) => void; // (sectionId) => void onSynced?: () => void; // () => void — refresh after a successful sync } /** * The Data page's connection status hero. Five states, so a network blip is never * mistaken for "not connected", and a server without bank sync never gets a dead * Connect button: * loading · disabled · error · not-connected · connected (± needs-attention) */ export default function ConnectionHero({ loading, error, enabled, hasConnections, conn, txnTotal, onRetry, onGoTo, onSynced, }: ConnectionHeroProps) { const [syncing, setSyncing] = useState(false); async function handleSyncNow() { setSyncing(true); try { const r = await api.syncAllSources() as { errors?: unknown[]; transactions_new?: number }; const errs = Array.isArray(r?.errors) ? r.errors : []; if (errs.length) { toast.warning(`Synced, but ${errs.length} connection${errs.length === 1 ? '' : 's'} need attention.`); } else { const n = r?.transactions_new ?? 0; toast.success(`Synced — ${n} new transaction${n === 1 ? '' : 's'}.`); } onSynced?.(); } catch (err) { if ((err as { status?: number })?.status === 429) toast.error('Please wait a moment before syncing again.'); else toast.error(errMessage(err, 'Sync failed.')); } finally { setSyncing(false); } } // ── loading ── if (loading) { return (
); } // ── disabled (server has no bank sync) ── if (!enabled) { return (

Automatic bank sync isn’t enabled

This server doesn’t have SimpleFIN configured — you can still import and export your data.

); } // ── error (couldn't check) ── if (error) { return (

Couldn’t check your bank connection

Something went wrong loading your sync status.

); } // ── not connected ── if (!hasConnections || !conn) { return (

Connect your bank to get started

Sync transactions automatically, or import your existing history.

); } // ── connected (± needs attention) ── const needsAttention = Boolean(conn.last_error); const synced = relativeTime(conn.last_sync_at); return (

{needsAttention ? 'Your bank connection needs attention' : 'Your bank is connected'}

{needsAttention ? conn.last_error : ( <> {conn.name || 'SimpleFIN'} {Number(txnTotal) > 0 ? <> · {Number(txnTotal).toLocaleString('en-US')} transactions : null} {synced ? <> · synced {synced} : null} {' · syncs automatically'} )}

{needsAttention && ( )}
); }