import { useCallback, useEffect, useMemo, useState } from 'react'; import GridLayout from 'react-grid-layout'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; import { AnalyticsApi, LayoutApi, ExportApi, SyncApi } from '../api/client.js'; import SyncSplash from '../components/SyncSplash.jsx'; import { HealthWidget, StatCard, TopSendersWidget, VolumeWidget, CategoryBreakdownWidget, AttachmentsWidget, StorageWidget } from '../components/widgets.jsx'; import { Skeleton } from '../components/ui/skeleton.jsx'; // Default grid geometry; overridden by the user's saved layout. const DEFAULT_LAYOUT = [ { i: 'inbox-health', x: 0, y: 0, w: 3, h: 5 }, { i: 'total-emails', x: 3, y: 0, w: 2, h: 2 }, { i: 'unread-emails', x: 5, y: 0, w: 2, h: 2 }, { i: 'storage', x: 7, y: 0, w: 2, h: 2 }, { i: 'top-senders', x: 3, y: 2, w: 3, h: 5 }, { i: 'volume', x: 6, y: 2, w: 6, h: 4 }, { i: 'category-breakdown', x: 0, y: 5, w: 6, h: 5 }, { i: 'attachments', x: 6, y: 6, w: 4, h: 4 }, ]; const ALL_WIDGETS = DEFAULT_LAYOUT.map((l) => l.i); function WidgetSkeleton() { return (
); } export default function Dashboard() { const [data, setData] = useState(null); const [layout, setLayout] = useState(DEFAULT_LAYOUT); const [hidden, setHidden] = useState([]); const [syncing, setSyncing] = useState(false); const loadData = useCallback(() => { AnalyticsApi.dashboard().then(setData).catch(() => {}); }, []); useEffect(() => { LayoutApi.get().then((saved) => { if (saved?.length) { const savedLayout = saved.map((w) => ({ i: w.widgetKey, x: w.x, y: w.y, w: w.w, h: w.h })); const savedKeys = new Set(savedLayout.map((l) => l.i)); // Append any newer default widgets the saved layout doesn't know about yet. const merged = [...savedLayout, ...DEFAULT_LAYOUT.filter((d) => !savedKeys.has(d.i))]; setLayout(merged); setHidden(saved.filter((w) => !w.visible).map((w) => w.widgetKey)); } }).catch(() => {}); // Decide whether to show the loading splash: if a sync is running, watch it; // if the inbox has never been synced, kick one off automatically. SyncApi.status().then((s) => { if (s.isRunning) { setSyncing(true); } else if (!s.lastSuccessfulSyncUtc) { SyncApi.incremental().then(() => setSyncing(true)).catch(loadData); } else { loadData(); } }).catch(loadData); }, [loadData]); // Show the splash when a sync is triggered from the header "Sync now" button. useEffect(() => { const onStart = () => setSyncing(true); window.addEventListener('inboxintel:sync-started', onStart); return () => window.removeEventListener('inboxintel:sync-started', onStart); }, []); const onSyncDone = useCallback(() => { setSyncing(false); loadData(); }, [loadData]); const persist = (nextLayout, nextHidden) => { const dto = nextLayout.map((l, idx) => ({ widgetKey: l.i, x: l.x, y: l.y, w: l.w, h: l.h, visible: !nextHidden.includes(l.i), sortOrder: idx, settingsJson: null })); LayoutApi.save(dto).catch(() => {}); }; const onLayoutChange = (l) => { setLayout(l); persist(l, hidden); }; const toggle = (key) => { const next = hidden.includes(key) ? hidden.filter((h) => h !== key) : [...hidden, key]; setHidden(next); persist(layout, next); }; const visibleLayout = useMemo(() => layout.filter((l) => !hidden.includes(l.i)), [layout, hidden]); const render = (key) => { // category-breakdown self-fetches, so it renders regardless of dashboard load state. if (key === 'category-breakdown') return ; // While the dashboard payload loads, show a skeleton placeholder per widget. if (!data) return ; switch (key) { case 'inbox-health': return ; case 'total-emails': return ; case 'unread-emails': return ; case 'storage': return ; case 'top-senders': return ; case 'volume': return ; case 'attachments': return ; default: return null; } }; return (
{syncing && }
{ALL_WIDGETS.map((k) => ( ))}
{visibleLayout.map((l) => (
{render(l.i)}
))}
); }