feat: Smart Folders sidebar, category heatmap, sidebar counts API

- Replace plain activity heatmap with CategoryHeatmapWidget on dashboard
- Add collapsible Smart Folders sidebar with Favorites, Mailbox, Smart Folders sections
- Favorites: customisable pinned shortcuts (default: Inbox, Unread, Large, Old, Read Later); pin/unpin smart folders via buttons; persists to localStorage
- Mailbox section: 11 Gmail mailbox items with icons and live count badges
- Smart Folders section: 10 category folders sorted by count desc
- Live count badges via new GET /api/v1/analytics/sidebar-counts endpoint
- Backend: SidebarCountsDto, GetSidebarCountsAsync with label lookups for SENT/DRAFT/SPAM, size/age filters, EmailCategory mapping
- Sidebar collapses to icon-only; section states persist to localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-06-30 19:40:54 +02:00
parent fe5920919f
commit ca02f40841
17 changed files with 2866 additions and 66 deletions
+21 -5
View File
@@ -6,7 +6,7 @@ import { AnalyticsApi, LayoutApi, ExportApi, SyncApi } from '../api/client.js';
import SyncSplash from '../components/SyncSplash.jsx';
import {
HealthWidget, StatCard, TopSendersWidget, VolumeWidget,
HeatmapWidget, AttachmentsWidget, StorageWidget
CategoryHeatmapWidget, AttachmentsWidget, StorageWidget
} from '../components/widgets.jsx';
// Default grid geometry; overridden by the user's saved layout.
@@ -17,8 +17,8 @@ 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: 'heatmap', x: 0, y: 5, w: 6, h: 4 },
{ i: 'attachments', x: 6, y: 6, w: 4, h: 4 }
{ i: 'category-heatmap', 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);
@@ -36,7 +36,11 @@ export default function Dashboard() {
useEffect(() => {
LayoutApi.get().then((saved) => {
if (saved?.length) {
setLayout(saved.map((w) => ({ i: w.widgetKey, x: w.x, y: w.y, w: w.w, h: w.h })));
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(() => {});
@@ -54,6 +58,13 @@ export default function Dashboard() {
}).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();
@@ -83,7 +94,7 @@ export default function Dashboard() {
case 'storage': return <StorageWidget bytes={data?.storageEstimateBytes} />;
case 'top-senders': return <TopSendersWidget senders={data?.topSenders} />;
case 'volume': return <VolumeWidget volume={data?.volumeOverTime} />;
case 'heatmap': return <HeatmapWidget heatmap={data?.heatmap} />;
case 'category-heatmap': return <CategoryHeatmapWidget />;
case 'attachments': return <AttachmentsWidget attachments={data?.attachmentBreakdown} />;
default: return null;
}
@@ -112,6 +123,11 @@ export default function Dashboard() {
cols={12}
rowHeight={60}
width={1200}
isResizable
isDraggable
resizeHandles={['se']}
compactType="vertical"
margin={[12, 12]}
onLayoutChange={onLayoutChange}
draggableHandle=".widget h3, .widget .stat-label"
>