clio/clio-ui/components/Dashboard.jsx
Your Name 528813d4a0
All checks were successful
Deploy UI / deploy (push) Successful in 1s
fixing bugs
2026-05-19 20:34:00 -04:00

213 lines
9.5 KiB
JavaScript

// 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 (
<div style={{ flex: 1, overflow: 'auto', padding: '24px 28px', display: 'flex', flexDirection: 'column', gap: 24 }}>
{/* Stat cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
{[
{ 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 => (
<div key={card.label} style={{
background: 'var(--bg2)', border: '1px solid var(--border)',
borderRadius: 6, padding: '16px 18px',
}}>
<div style={{ fontSize: 11, color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.08em', fontFamily: 'var(--font-mono)', marginBottom: 6 }}>{card.label}</div>
<div style={{ fontSize: 28, fontFamily: 'var(--font-mono)', fontWeight: 500, color: 'var(--amber)', lineHeight: 1 }}>{card.value}</div>
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 5, fontFamily: 'var(--font-mono)' }}>{card.sub}</div>
</div>
))}
</div>
{/* Source breakdown */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
{Object.entries(bySrc).map(([src, count]) => {
const srcHours = data.recordings.filter(r => r.source === src).reduce((a, r) => a + r.durationMinutes / 60, 0);
return (
<div key={src} style={{ background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 6, padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 14 }}>
<div>
<span className={`source-pill source-${src}`}>{src}</span>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 18, color: 'var(--text)', fontWeight: 500 }}>{count} <span style={{ fontSize: 12, color: 'var(--text3)' }}>files</span></div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text3)' }}>{Math.round(srcHours)}h recorded</div>
</div>
</div>
);
})}
</div>
{/* Calendar heatmap */}
<div style={{ background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 6, padding: '18px 20px' }}>
<div style={{ fontSize: 11, color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.08em', fontFamily: 'var(--font-mono)', marginBottom: 14 }}>Activity Last 8 Months</div>
<CalendarHeatmap dayMap={dayMap} maxMinutes={maxMinutes} onDayClick={handleDayClick} />
</div>
{/* Recent recordings */}
<div style={{ background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 6, padding: '18px 20px' }}>
<div style={{ fontSize: 11, color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.08em', fontFamily: 'var(--font-mono)', marginBottom: 14 }}>Recent Recordings</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{recent.map(r => (
<RecordingRow key={r.id} recording={r} onClick={() => setSelectedRecording(r)} />
))}
</div>
</div>
</div>
);
}
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 (
<div style={{ display: 'flex', gap: 16, overflowX: 'auto', paddingBottom: 4 }}>
{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(<div key={`empty-${i}`} style={{ width: 14, height: 14 }} />);
}
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(
<div
key={d}
onClick={() => 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 (
<div key={label} style={{ flexShrink: 0 }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text3)', marginBottom: 6, whiteSpace: 'nowrap' }}>{label}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 14px)', gap: 3 }}>{cells}</div>
</div>
);
})}
</div>
);
}
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 (
<div
onClick={onClick}
style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '9px 12px', borderRadius: 4, cursor: 'pointer',
transition: 'background 0.1s',
}}
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg3)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text3)', width: 80, flexShrink: 0 }}>{dateStr} {timeStr}</div>
<span className={`source-pill source-${r.source}`}>{r.source}</span>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text2)', width: 60, flexShrink: 0 }}>{dur}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, flexShrink: 0 }} className={`q-${r.quality}`}>{r.quality}</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text3)', flexShrink: 0 }}>{r.fileSizeMB} MB</div>
{r.highlights.length > 0 && (
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--amber)', background: 'var(--amber-glow)', padding: '1px 6px', borderRadius: 3 }}>
{r.highlights.length}
</div>
)}
{r.hasTranscript && (
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--text3)' }}> transcript</div>
)}
</div>
);
}
Object.assign(window, { Dashboard, CalendarHeatmap, RecordingRow });