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
+38
View File
@@ -1,4 +1,6 @@
import { useEffect, useState } from 'react';
import { Bar, Line, Doughnut } from 'react-chartjs-2';
import { AnalyticsApi } from '../api/client.js';
import {
Chart as ChartJS, CategoryScale, LinearScale, BarElement, PointElement,
LineElement, ArcElement, Tooltip, Legend
@@ -91,6 +93,42 @@ export function HeatmapWidget({ heatmap }) {
);
}
const DOW = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
export function CategoryHeatmapWidget() {
const [cells, setCells] = useState(null);
useEffect(() => { AnalyticsApi.categoryHeatmap().then(setCells).catch(() => setCells([])); }, []);
if (!cells) return <div className="widget"><div className="muted">Loading</div></div>;
if (cells.length === 0) return <div className="widget"><h3>Category Heatmap</h3><div className="muted">No data yet run a sync.</div></div>;
const categories = [...new Set(cells.map((c) => c.category))].sort();
const max = Math.max(1, ...cells.map((c) => c.count));
const grid = {};
cells.forEach((c) => { grid[`${c.category}-${c.dayOfWeek}`] = c.count; });
return (
<div className="widget">
<h3>Category Heatmap</h3>
<div className="cat-heatmap">
<div className="chm-row chm-head">
<span className="chm-label" />
{DOW.map((d) => <span key={d} className="chm-col">{d}</span>)}
</div>
{categories.map((cat) => (
<div className="chm-row" key={cat}>
<span className="chm-label" title={cat}>{cat}</span>
{DOW.map((_, dow) => {
const v = grid[`${cat}-${dow}`] || 0;
return <span key={dow} className="chm-cell" style={{ opacity: 0.12 + 0.88 * (v / max) }} title={`${cat} · ${DOW[dow]}${v}`}>{v || ''}</span>;
})}
</div>
))}
</div>
</div>
);
}
export function AttachmentsWidget({ attachments }) {
if (!attachments) return <Empty />;
const data = {