// DayView: calendar month grid + day timeline with audio density visualization const { useState: useStateDV, useEffect: useEffectDV, useRef: useRefDV } = React; function DayView({ data, selectedDay, setSelectedDay, setSelectedRecording, setView }) { const now = new Date(); const [viewMonth, setViewMonth] = useStateDV(() => { if (selectedDay) { const d = new Date(selectedDay); return { year: d.getFullYear(), month: d.getMonth() }; } return { year: now.getFullYear(), month: now.getMonth() }; }); // Build day map const dayMap = {}; for (const r of data.recordings) { const key = r.startTime.toISOString().slice(0, 10); if (!dayMap[key]) dayMap[key] = []; dayMap[key].push(r); } const maxCount = Math.max(...Object.values(dayMap).map(d => d.length), 1); // Calendar grid const { year, month } = viewMonth; const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const startDow = firstDay.getDay(); const daysInMonth = lastDay.getDate(); const prevMonth = () => { setViewMonth(m => { const d = new Date(m.year, m.month - 1, 1); return { year: d.getFullYear(), month: d.getMonth() }; }); }; const nextMonth = () => { setViewMonth(m => { const d = new Date(m.year, m.month + 1, 1); return { year: d.getFullYear(), month: d.getMonth() }; }); }; const monthLabel = firstDay.toLocaleString('default', { month: 'long', year: 'numeric' }); const dayRecs = selectedDay ? (dayMap[selectedDay] || []).sort((a, b) => a.startTime - b.startTime) : []; function fmtDay(dateStr) { const d = new Date(dateStr + 'T12:00:00'); return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }); } return (
{/* Left: Month calendar */}
{/* Month nav */}
{monthLabel}
{/* Weekday headers */}
{['S','M','T','W','T','F','S'].map((d, i) => (
{d}
))} {/* Empty cells */} {Array.from({ length: startDow }).map((_, i) =>
)} {/* Day cells */} {Array.from({ length: daysInMonth }).map((_, i) => { const d = i + 1; const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; const recs = dayMap[dateStr] || []; const isSelected = selectedDay === dateStr; const isToday = dateStr === now.toISOString().slice(0, 10); const hasRecs = recs.length > 0; const intensity = hasRecs ? recs.length / maxCount : 0; return (
hasRecs && setSelectedDay(dateStr)} style={{ position: 'relative', aspectRatio: '1', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', borderRadius: 4, cursor: hasRecs ? 'pointer' : 'default', background: isSelected ? 'var(--amber)' : isToday ? 'var(--bg4)' : 'transparent', outline: isToday && !isSelected ? '1px solid var(--border)' : 'none', transition: 'background 0.1s', }} onMouseEnter={e => { if (hasRecs && !isSelected) e.currentTarget.style.background = 'var(--bg4)'; }} onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = isToday ? 'var(--bg4)' : 'transparent'; }} > {d} {hasRecs && (
{recs.slice(0, 3).map((_, ri) => (
))}
)}
); })}
{/* Day summary */} {selectedDay && dayRecs.length > 0 && (
{dayRecs.length} recordings · {Math.round(dayRecs.reduce((a, r) => a + r.durationMinutes, 0) / 60)}h total
{['portable','stationary','phone'].map(src => { const count = dayRecs.filter(r => r.source === src).length; if (!count) return null; return (
{src} {count}
); })}
)}
{/* Right: Day timeline */}
{!selectedDay ? (
Select a day to view recordings
) : ( <>
{fmtDay(selectedDay)}
{dayRecs.length === 0 ? (
No recordings this day.
) : ( { setSelectedRecording(r); setView('detail'); }} /> )} )}
); } function DayTimeline({ recordings, onSelect }) { // 24-hour timeline with recording bars const HOUR_H = 52; const LABEL_W = 44; const BAR_W = 260; const hours = Array.from({ length: 24 }, (_, i) => i); function recTop(r) { const h = r.startTime.getHours() + r.startTime.getMinutes() / 60; return h * HOUR_H; } function recHeight(r) { return Math.max((r.durationMinutes / 60) * HOUR_H, 8); } function qualityColor(q) { const map = { excellent: 'var(--green)', good: 'var(--teal)', fair: 'var(--amber)', poor: 'var(--red)' }; return map[q] || 'var(--text3)'; } const totalH = 24 * HOUR_H; // Density mini-bars per hour (minutes of audio per hour) const densityPerHour = Array(24).fill(0); for (const r of recordings) { const startH = r.startTime.getHours() + r.startTime.getMinutes() / 60; const endH = startH + r.durationMinutes / 60; for (let h = 0; h < 24; h++) { const overlapStart = Math.max(h, startH); const overlapEnd = Math.min(h + 1, endH); if (overlapEnd > overlapStart) { densityPerHour[h] += (overlapEnd - overlapStart) * 60; } } } return (
{/* Timeline */}
{/* Hour lines + labels */} {hours.map(h => (
{String(h).padStart(2, '0')}:00
))} {/* Recording bars */} {recordings.map(r => { const top = recTop(r); const height = recHeight(r); const srcColors = { portable: 'var(--amber)', stationary: 'var(--teal)', phone: 'var(--green)' }; const srcColor = srcColors[r.source] || 'var(--text3)'; return (
onSelect(r)} title={`${r.source} · ${Math.floor(r.durationMinutes / 60)}h ${Math.round(r.durationMinutes % 60)}m · ${r.quality}`} style={{ position: 'absolute', top: top + 1, left: LABEL_W + 12, width: BAR_W, height: height - 2, background: `${srcColor}22`, border: `1px solid ${srcColor}66`, borderLeft: `3px solid ${srcColor}`, borderRadius: '0 4px 4px 0', cursor: 'pointer', display: 'flex', alignItems: 'flex-start', gap: 8, padding: '4px 8px', overflow: 'hidden', transition: 'background 0.1s', zIndex: 1, }} onMouseEnter={e => { e.currentTarget.style.background = `${srcColor}40`; }} onMouseLeave={e => { e.currentTarget.style.background = `${srcColor}22`; }} > {r.source} {Math.floor(r.durationMinutes / 60)}h {Math.round(r.durationMinutes % 60)}m {r.quality} {r.highlights.length > 0 && ( ◈ {r.highlights.length} )}
); })}
{/* Density strip - clickable to go to recording at that hour */}
density
{densityPerHour.map((mins, h) => { const pct = mins / 60; // Find recording that covers this hour const recAtHour = recordings.find(r => { const startH = r.startTime.getHours() + r.startTime.getMinutes() / 60; const endH = startH + r.durationMinutes / 60; return h >= startH && h < endH; }); return (
recAtHour && onSelect(recAtHour)} style={{ height: HOUR_H, display: 'flex', alignItems: 'stretch', cursor: recAtHour ? 'pointer' : 'default', }} >
0 ? 2 : 0, background: pct > 0.8 ? 'var(--amber)' : pct > 0.3 ? 'var(--amber-dim)' : 'var(--bg4)', transition: 'width 0.2s, opacity 0.1s', opacity: recAtHour ? 1 : 0.5, }} />
); })}
{/* Recording list for quick jump */}
Jump to
{recordings.map(r => { const timeStr = r.startTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); return (
onSelect(r)} style={{ background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 4, padding: '8px 12px', cursor: 'pointer', transition: 'border-color 0.1s', }} onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--amber)'; }} onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; }} >
{timeStr} {r.source} {Math.floor(r.durationMinutes / 60)}h {Math.round(r.durationMinutes % 60)}m
{r.quality} {r.sampleRate / 1000}kHz · {r.bitDepth}bit · {r.fileSizeMB}MB {r.hasTranscript && ✦ transcript} {r.highlights.length > 0 && ◈ {r.highlights.length}}
); })}
); } Object.assign(window, { DayView, DayTimeline });