'use client' import { useMemo } from 'react' import { useCalendarStore, useEffectiveSpatialGranularity } from '@/lib/store' import { getSemanticLocationLabel } from '@/lib/location' import { useMonthEvents } from '@/hooks/useEvents' import { TemporalGranularity } from '@/lib/types' import type { EventListItem } from '@/lib/types' import { EventDetailModal } from './EventDetailModal' import { clsx } from 'clsx' const HOURS = Array.from({ length: 24 }, (_, i) => i) function formatHour(hour: number): string { if (hour === 0) return '12 AM' if (hour < 12) return `${hour} AM` if (hour === 12) return '12 PM' return `${hour - 12} PM` } function getWeekDays(date: Date): Date[] { const start = new Date(date) start.setDate(date.getDate() - date.getDay()) // Start on Sunday start.setHours(0, 0, 0, 0) return Array.from({ length: 7 }, (_, i) => { const d = new Date(start) d.setDate(start.getDate() + i) return d }) } function getEventPosition(event: EventListItem, dayStart: Date) { const start = new Date(event.start) const end = new Date(event.end) const dayEnd = new Date(dayStart) dayEnd.setHours(23, 59, 59, 999) const effectiveStart = start < dayStart ? dayStart : start const effectiveEnd = end > dayEnd ? dayEnd : end const startMinutes = effectiveStart.getHours() * 60 + effectiveStart.getMinutes() const endMinutes = effectiveEnd.getHours() * 60 + effectiveEnd.getMinutes() const duration = Math.max(endMinutes - startMinutes, 30) return { top: (startMinutes / (24 * 60)) * 100, height: (duration / (24 * 60)) * 100, } } export function WeekView() { const { currentDate, selectedEventId, setSelectedEventId, setCurrentDate, setTemporalGranularity, hiddenSources, } = useCalendarStore() const effectiveSpatial = useEffectiveSpatialGranularity() const weekDays = useMemo(() => getWeekDays(currentDate), [currentDate]) const year = currentDate.getFullYear() const month = currentDate.getMonth() + 1 // We may need events from adjacent months if the week spans months const { data: eventsData, isLoading } = useMonthEvents(year, month) const today = useMemo(() => { const d = new Date() d.setHours(0, 0, 0, 0) return d }, []) const nowPosition = useMemo(() => { const now = new Date() const minutes = now.getHours() * 60 + now.getMinutes() return (minutes / (24 * 60)) * 100 }, []) // Group events by day key const eventsByDay = useMemo(() => { const map = new Map() if (!eventsData?.results) return map for (const event of eventsData.results) { if (hiddenSources.includes(event.source)) continue for (const day of weekDays) { const dayKey = day.toISOString().split('T')[0] const eventStart = new Date(event.start).toISOString().split('T')[0] const eventEnd = new Date(event.end).toISOString().split('T')[0] if (eventStart <= dayKey && eventEnd >= dayKey) { if (!map.has(dayKey)) map.set(dayKey, { allDay: [], timed: [] }) const bucket = map.get(dayKey)! if (event.all_day) { if (!bucket.allDay.find((e) => e.id === event.id)) bucket.allDay.push(event) } else { if (!bucket.timed.find((e) => e.id === event.id)) bucket.timed.push(event) } } } } return map }, [eventsData?.results, weekDays, hiddenSources]) const hasAllDayEvents = useMemo( () => Array.from(eventsByDay.values()).some((d) => d.allDay.length > 0), [eventsByDay] ) const handleDayClick = (date: Date) => { setCurrentDate(date) setTemporalGranularity(TemporalGranularity.DAY) } const weekRange = `${weekDays[0].toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} \u2013 ${weekDays[6].toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}` return (
{/* Week header */}

{weekRange}

Week {Math.ceil((currentDate.getDate() + new Date(year, month - 1, 1).getDay()) / 7)}
{/* Day headers */}
{/* Spacer for time column */}
{weekDays.map((day) => { const isToday = day.getTime() === today.getTime() const dayKey = day.toISOString().split('T')[0] return ( ) })}
{/* All-day events row */} {hasAllDayEvents && (
All day
{weekDays.map((day) => { const dayKey = day.toISOString().split('T')[0] const dayEvents = eventsByDay.get(dayKey) return (
{dayEvents?.allDay.map((event) => ( ))}
) })}
)} {/* Time grid */}
{/* Hour lines */} {HOURS.map((hour) => (
{formatHour(hour)}
{weekDays.map((day) => (
))}
))} {/* Now indicator */} {weekDays.some((d) => d.getTime() === today.getTime()) && (
)} {/* Events overlay */}
{weekDays.map((day, colIdx) => { const dayKey = day.toISOString().split('T')[0] const dayEvents = eventsByDay.get(dayKey) const dayStart = new Date(day) dayStart.setHours(0, 0, 0, 0) return (
{dayEvents?.timed.map((event) => { const pos = getEventPosition(event, dayStart) const locationLabel = getSemanticLocationLabel(event, effectiveSpatial) return ( ) })}
) })}
{/* Loading */} {isLoading && (
{weekDays.map((_, i) => (
))}
)}
{/* Event detail modal */} {selectedEventId && ( setSelectedEventId(null)} /> )}
) }