import { useMemo, useState, useEffect, Fragment } from 'react'; import { useStats } from '../../hooks/useStats'; import { timeToMinutes } from '../../utils/time'; interface TimetableProps { from_date: string; to_date: string; day_start: string; day_end: string; } function getDayName(dateStr: string) { return new Date(dateStr + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'short' }); } function formatDayLabel(dateStr: string) { const date = new Date(dateStr + 'T00:00:00'); const weekday = date.toLocaleDateString('en-US', { weekday: 'short' }); const day = date.getDate(); return `${weekday} ${day}`; } function formatTimeLabel(mins: number) { const hours = Math.floor(mins / 60); const minutes = mins % 60; return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; } const Timetable = ({ from_date, to_date, day_start, day_end }: TimetableProps) => { const timezoneOffset = typeof window !== 'undefined' ? new Date().getTimezoneOffset() : 0; const { timetable, isLoading } = useStats(from_date, to_date, day_start, day_end, '04:00', '19:00', timezoneOffset); const [selectedDayIndex, setSelectedDayIndex] = useState(0); const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024); const days = useMemo(() => { const start = new Date(from_date + 'T00:00:00'); const end = new Date(to_date + 'T23:59:59'); const result: string[] = []; const current = new Date(start); while (current <= end) { result.push(current.toLocaleDateString('en-CA')); current.setDate(current.getDate() + 1); } return result; // Chronological (oldest to newest) }, [from_date, to_date]); useEffect(() => { const handleResize = () => setWindowWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); useEffect(() => { if (days.length > 0) { const newIndex = days.length - 1; setSelectedDayIndex(prev => (prev !== newIndex ? newIndex : prev)); } }, [days]); const reversedDays = useMemo(() => [...days].reverse(), [days]); const timeSteps = useMemo(() => { return Array.from({ length: 96 }, (_, i) => i * 15); }, []); const tier = useMemo(() => { if (windowWidth >= 768) return 'desktop'; if (windowWidth >= 400) return 'medium'; return 'small'; }, [windowWidth]); const { visibleDays, tabGroups } = useMemo(() => { if (tier === 'desktop') { return { visibleDays: days, tabGroups: [] }; } const activeDays = reversedDays; const selectedDay = days[selectedDayIndex]; const indexInActiveDays = activeDays.indexOf(selectedDay); if (tier === 'medium') { const groups = []; for (let i = 0; i < activeDays.length; i += 3) { groups.push({ label: `${formatDayLabel(activeDays[Math.min(i + 2, activeDays.length - 1)])} - ${formatDayLabel(activeDays[i])}`, startIndex: i, }); } return { visibleDays: activeDays.slice(indexInActiveDays, indexInActiveDays + 3).reverse(), tabGroups: groups, }; } // Small tier (single day) const groups = activeDays.map((day, idx) => ({ label: formatDayLabel(day), startIndex: idx, })); return { visibleDays: activeDays.slice(indexInActiveDays, indexInActiveDays + 1).reverse(), tabGroups: groups, }; }, [tier, days, reversedDays, selectedDayIndex]); const isSleepingInInterval = (date: string, intervalStart: number, intervalEnd: number) => { if (!timetable?.slots) return false; return timetable.slots.some((slot) => { if (slot.date !== date || !slot.sleeping) return false; const sStart = timeToMinutes(slot.slot_start); const sEnd = timeToMinutes(slot.slot_end); if (sEnd <= sStart) { const overlap1 = Math.max(sStart, intervalStart) < Math.min(1440, intervalEnd); const overlap2 = Math.max(0, intervalStart) < Math.min(sEnd, intervalEnd); return overlap1 || overlap2; } return Math.max(sStart, intervalStart) < Math.min(sEnd, intervalEnd); }); }; const handleTabClick = (startIndexInReversed: number) => { const day = reversedDays[startIndexInReversed]; const indexInDays = days.indexOf(day); setSelectedDayIndex(indexInDays); }; const currentMins = new Date().getHours() * 60 + new Date().getMinutes(); const rowHeightPx = 8; if (isLoading) return
Loading timetable...
; if (!timetable?.slots || timetable.slots.length === 0) { return
No data available for this period.
; } return (

Sleep Timetable

{/* Tab Selector (Mobile & Medium) */} {(tier === 'medium' || tier === 'small') && (
{tabGroups.map((group, idx) => { const isActive = selectedDayIndex === days.indexOf(reversedDays[group.startIndex]); return ( ); })}
)} {/* Grid Container */}
{/* Header Row */}
{/* Empty corner cell */} {visibleDays.map((day) => (
{getDayName(day)}
))}
{/* Time Grid Container */}
{/* Time Rows */} {timeSteps.map((step) => ( {/* Time Label */}
{step % 60 === 0 ? formatTimeLabel(step) : ''}
{/* Grid Cells */} {visibleDays.map((day) => { const sleeping = isSleepingInInterval(day, step, step + 15); return (
); })} ))} {/* Red Marker Line */}
); }; export default Timetable;