feat: dashboard drillthrough — widgets navigate to filtered views

- Total Emails stat card -> /app/folder/allmail
- Unread stat card -> /app/folder/unread
- Top Senders rows -> /app/search?q=from:{address}
- Category heatmap labels -> matching folder or search (Finance, Social, Notification, Promotional, Spam, Newsletter, Personal)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-06-30 20:20:09 +02:00
parent 2f6d6abdd3
commit 80c2167b89
3 changed files with 48 additions and 14 deletions
+40 -12
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Bar, Line, Doughnut } from 'react-chartjs-2';
import { AnalyticsApi } from '../api/client.js';
import {
@@ -15,9 +16,10 @@ const fmtBytes = (b) => {
return `${n.toFixed(1)} ${u[i]}`;
};
export function StatCard({ label, value }) {
export function StatCard({ label, value, to }) {
const navigate = useNavigate();
return (
<div className="widget stat">
<div className={`widget stat${to ? ' widget--link' : ''}`} onClick={to ? () => navigate(to) : undefined} title={to ? `View ${label}` : undefined}>
<div className="stat-value">{value}</div>
<div className="stat-label">{label}</div>
</div>
@@ -37,6 +39,7 @@ export function HealthWidget({ health }) {
}
export function TopSendersWidget({ senders }) {
const navigate = useNavigate();
if (!senders) return <Empty />;
return (
<div className="widget">
@@ -44,7 +47,12 @@ export function TopSendersWidget({ senders }) {
<table className="mini">
<tbody>
{senders.map((s) => (
<tr key={s.senderId}>
<tr
key={s.senderId}
className="mini-row--link"
onClick={() => navigate(`/app/search?q=${encodeURIComponent(`from:${s.address}`)}`)}
title={`Search emails from ${s.address}`}
>
<td title={s.address}>{s.displayName || s.address}</td>
<td className="num">{s.emailCount}</td>
</tr>
@@ -95,8 +103,21 @@ export function HeatmapWidget({ heatmap }) {
const DOW = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// Maps EmailCategory enum name → folder slug or search query
const CAT_SLUG = {
Finance: '/app/folder/finance',
Social: '/app/folder/social',
Notification: '/app/folder/automated',
Promotional: '/app/folder/shopping',
Spam: '/app/folder/spam',
Newsletter: '/app/search?q=newsletter',
Personal: '/app/search?q=is:unread',
Unknown: '/app/folder/allmail',
};
export function CategoryHeatmapWidget() {
const [cells, setCells] = useState(null);
const navigate = useNavigate();
useEffect(() => { AnalyticsApi.categoryHeatmap().then(setCells).catch(() => setCells([])); }, []);
if (!cells) return <div className="widget"><div className="muted">Loading</div></div>;
@@ -115,15 +136,22 @@ export function CategoryHeatmapWidget() {
<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>
))}
{categories.map((cat) => {
const dest = CAT_SLUG[cat];
return (
<div className="chm-row" key={cat}>
<span
className={`chm-label${dest ? ' chm-label--link' : ''}`}
title={dest ? `View ${cat} emails` : cat}
onClick={dest ? () => navigate(dest) : undefined}
>{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>
);