feat(ui): email dashboard UX refactor (PHASES 1/3/4) (#44)
CI / backend (push) Successful in 1m33s
CI / frontend (push) Successful in 33s
CI / format (push) Successful in 2m2s
CI / db-tests (push) Successful in 1m45s
Deploy Staging / deploy (push) Successful in 38s
Security / secrets (push) Successful in 6s
Security / dependencies (push) Successful in 1m17s
Security / sast (push) Successful in 56s
CI / backend (pull_request) Successful in 1m9s
CI / frontend (pull_request) Successful in 21s
CI / format (pull_request) Successful in 1m4s
CI / db-tests (pull_request) Successful in 1m27s
Security / secrets (pull_request) Successful in 6s
Security / dependencies (pull_request) Successful in 1m21s
Security / sast (pull_request) Successful in 1m8s

This commit was merged in pull request #44.
This commit is contained in:
2026-07-04 21:16:21 +02:00
parent 04ff72097b
commit 2f173dc3e5
15 changed files with 712 additions and 192 deletions
+25 -10
View File
@@ -6,8 +6,9 @@ import { AnalyticsApi, LayoutApi, ExportApi, SyncApi } from '../api/client.js';
import SyncSplash from '../components/SyncSplash.jsx';
import {
HealthWidget, StatCard, TopSendersWidget, VolumeWidget,
CategoryHeatmapWidget, AttachmentsWidget, StorageWidget
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 = [
@@ -17,12 +18,23 @@ const DEFAULT_LAYOUT = [
{ 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-heatmap', x: 0, y: 5, w: 6, h: 5 },
{ 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 (
<div className="widget">
<Skeleton className="mb-3 h-4 w-1/3" />
<Skeleton className="mb-2 h-3 w-full" />
<Skeleton className="mb-2 h-3 w-5/6" />
<Skeleton className="h-3 w-2/3" />
</div>
);
}
export default function Dashboard() {
const [data, setData] = useState(null);
const [layout, setLayout] = useState(DEFAULT_LAYOUT);
@@ -87,15 +99,18 @@ export default function Dashboard() {
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 <CategoryBreakdownWidget />;
// While the dashboard payload loads, show a skeleton placeholder per widget.
if (!data) return <WidgetSkeleton />;
switch (key) {
case 'inbox-health': return <HealthWidget health={data?.health} />;
case 'total-emails': return <StatCard label="Total Emails" value={(data?.totalEmails ?? 0).toLocaleString()} to="/app/folder/allmail" />;
case 'unread-emails': return <StatCard label="Unread" value={(data?.unreadEmails ?? 0).toLocaleString()} to="/app/folder/unread" />;
case 'storage': return <StorageWidget bytes={data?.storageEstimateBytes} />;
case 'top-senders': return <TopSendersWidget senders={data?.topSenders} />;
case 'volume': return <VolumeWidget volume={data?.volumeOverTime} />;
case 'category-heatmap': return <CategoryHeatmapWidget />;
case 'attachments': return <AttachmentsWidget attachments={data?.attachmentBreakdown} />;
case 'inbox-health': return <HealthWidget health={data.health} />;
case 'total-emails': return <StatCard label="Total Emails" value={(data.totalEmails ?? 0).toLocaleString()} to="/app/folder/allmail" />;
case 'unread-emails': return <StatCard label="Unread" value={(data.unreadEmails ?? 0).toLocaleString()} to="/app/folder/unread" />;
case 'storage': return <StorageWidget bytes={data.storageEstimateBytes} />;
case 'top-senders': return <TopSendersWidget senders={data.topSenders} />;
case 'volume': return <VolumeWidget volume={data.volumeOverTime} />;
case 'attachments': return <AttachmentsWidget attachments={data.attachmentBreakdown} />;
default: return null;
}
};