// 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 = r.startTime.toISOString().slice(0, 10); 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`; } function qualityClass(q) { return `q-${q}`; } 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 */}
Activity — Last 8 Months
{/* Recent recordings */}
Recent Recordings
{recent.map(r => ( { setSelectedRecording(r); setView('detail'); }} /> ))}
); } 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 === new Date().toISOString().slice(0, 10); 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 (
{label}
{cells}
); })}
); } 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.quality}
{r.fileSizeMB} MB
{r.highlights.length > 0 && (
{r.highlights.length} ◈
)} {r.hasTranscript && (
✦ transcript
)}
); } Object.assign(window, { Dashboard, CalendarHeatmap, RecordingRow });