// Dashboard view: stats, calendar heatmap, recent recordings
function Dashboard({ data, setView, setSelectedDay, setSelectedRecording }) {
const now = new Date();
// Aggregate stats
const totalHours = data.recordings.reduce((a, r) => a + r.durationMinutes / 60, 0);
const totalGB = data.recordings.reduce((a, r) => a + r.fileSizeMB, 0) / 1024;
const transcribedCount = data.recordings.filter(r => r.hasTranscript).length;
const highlightCount = data.highlights.length;
// By source
const bySrc = {};
for (const r of data.recordings) {
bySrc[r.source] = (bySrc[r.source] || 0) + 1;
}
// Calendar heatmap: last 8 months
const calStart = new Date(now);
calStart.setMonth(calStart.getMonth() - 7);
calStart.setDate(1);
// Build day map
const dayMap = {};
for (const r of data.recordings) {
const key = window.toLocalDateString(r.startTime);
if (!dayMap[key]) dayMap[key] = { count: 0, minutes: 0 };
dayMap[key].count++;
dayMap[key].minutes += r.durationMinutes;
}
const maxMinutes = Math.max(...Object.values(dayMap).map(d => d.minutes), 1);
// Recent recordings
const recent = [...data.recordings]
.sort((a, b) => b.startTime - a.startTime)
.slice(0, 8);
function handleDayClick(dateStr) {
setSelectedDay(dateStr);
setView('calendar');
}
function fmtDuration(min) {
const h = Math.floor(min / 60);
const m = min % 60;
return h > 0 ? `${h}h ${m}m` : `${m}m`;
}
return (
{/* Stat cards */}
{[
{ label: 'Recordings', value: data.recordings.length.toLocaleString(), sub: 'total files' },
{ label: 'Total Duration', value: `${Math.round(totalHours)}h`, sub: `${Math.round(totalHours / 24)} days of audio` },
{ label: 'Storage Used', value: `${totalGB.toFixed(1)} GB`, sub: `avg ${(totalGB / data.recordings.length * 1024).toFixed(0)} MB/file` },
{ label: 'Highlights', value: highlightCount, sub: `${transcribedCount} transcribed` },
].map(card => (
{card.label}
{card.value}
{card.sub}
))}
{/* Source breakdown */}
{Object.entries(bySrc).map(([src, count]) => {
const srcHours = data.recordings.filter(r => r.source === src).reduce((a, r) => a + r.durationMinutes / 60, 0);
return (
{src}
{count} files
{Math.round(srcHours)}h recorded
);
})}
{/* Calendar heatmap */}
{/* Recent recordings */}
Recent Recordings
{recent.map(r => (
setSelectedRecording(r)} />
))}
);
}
function CalendarHeatmap({ dayMap, maxMinutes, onDayClick }) {
const now = new Date();
const months = [];
for (let m = 7; m >= 0; m--) {
const d = new Date(now.getFullYear(), now.getMonth() - m, 1);
months.push({ year: d.getFullYear(), month: d.getMonth(), label: d.toLocaleString('default', { month: 'short' }) + ' ' + d.getFullYear() });
}
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function intensityColor(minutes) {
if (!minutes) return 'var(--bg4)';
const t = Math.min(minutes / maxMinutes, 1);
if (t < 0.2) return 'oklch(0.35 0.08 65)';
if (t < 0.5) return 'oklch(0.50 0.11 65)';
if (t < 0.8) return 'oklch(0.65 0.13 65)';
return 'oklch(0.75 0.15 65)';
}
return (
{months.map(({ year, month, label }) => {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startDow = firstDay.getDay(); // 0=Sun
const daysInMonth = lastDay.getDate();
const cells = [];
// Empty cells for offset
for (let i = 0; i < startDow; i++) {
cells.push(
);
}
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
const info = dayMap[dateStr];
const isToday = dateStr === window.toLocalDateString(new Date());
cells.push(
info && onDayClick(dateStr)}
title={info ? `${dateStr}: ${info.count} recordings, ${Math.round(info.minutes / 60)}h` : dateStr}
style={{
width: 14, height: 14, borderRadius: 2,
background: intensityColor(info?.minutes),
cursor: info ? 'pointer' : 'default',
outline: isToday ? '2px solid var(--amber)' : 'none',
outlineOffset: 1,
transition: 'transform 0.1s',
}}
onMouseEnter={e => { if (info) e.target.style.transform = 'scale(1.3)'; }}
onMouseLeave={e => { e.target.style.transform = 'scale(1)'; }}
/>
);
}
return (
);
})}
);
}
function RecordingRow({ recording, onClick }) {
const r = recording;
const timeStr = r.startTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const dateStr = r.startTime.toLocaleDateString([], { month: 'short', day: 'numeric' });
const dur = `${Math.floor(r.durationMinutes / 60)}h ${Math.round(r.durationMinutes % 60)}m`;
return (
e.currentTarget.style.background = 'var(--bg3)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>
{dateStr} {timeStr}
{r.source}
{dur}
{r.fileSizeMB} MB
{r.highlights.length > 0 && (
{r.highlights.length} ◈
)}
{r.hasTranscript && (
✦ transcript
)}
);
}
Object.assign(window, { Dashboard, CalendarHeatmap, RecordingRow });