import { useState } from 'react';
import { toast } from 'sonner';
import { Landmark, RefreshCw, Loader2, AlertTriangle, RotateCcw, Upload, Plus } from 'lucide-react';
import { api } from '@/api';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
// Relative "2h ago" / "3d ago"; returns null for empty input.
function relativeTime(iso) {
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 }) {
const toneRing = {
default: 'border-border/60',
good: 'border-emerald-500/30',
warn: 'border-amber-500/40',
error: 'border-rose-500/30',
}[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',
}[tone];
return (
{children}
);
}
/**
* 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, // truthy when the status/summary fetch failed
enabled, // status.enabled (server feature flag)
hasConnections, // status.has_connections
conn, // the simplefin data_source (name, last_sync_at, last_error) or null
txnTotal, // total synced transactions (at-a-glance), or null
onRetry,
onGoTo, // (sectionId) => void
onSynced, // () => void — refresh after a successful sync
}) {
const [syncing, setSyncing] = useState(false);
async function handleSyncNow() {
setSyncing(true);
try {
const r = await api.syncAllSources();
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?.status === 429) toast.error('Please wait a moment before syncing again.');
else toast.error(err?.message || '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 && (
)}
);
}