import { useState, useEffect, useCallback, useMemo } from 'react' import { Link } from 'react-router-dom' import { useSources, CalendarEvent } from '@/hooks/useSources' import './Calendar.css' const DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] const MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ] interface DayCell { date: Date isCurrentMonth: boolean isToday: boolean events: CalendarEvent[] } const Calendar = () => { const { getUpcomingEvents } = useSources() const [events, setEvents] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [currentDate, setCurrentDate] = useState(new Date()) const [selectedEvent, setSelectedEvent] = useState(null) const loadEvents = useCallback(async (date: Date) => { setLoading(true) setError(null) try { // Calculate range for the month view (include overflow days) const year = date.getFullYear() const month = date.getMonth() // Start from first day of previous month (for overflow) const startDate = new Date(year, month - 1, 1) // End at last day of next month (for overflow) const endDate = new Date(year, month + 2, 0) const data = await getUpcomingEvents({ startDate: startDate.toISOString(), endDate: endDate.toISOString(), limit: 200, }) setEvents(data) } catch (e) { setError(e instanceof Error ? e.message : 'Failed to load events') } finally { setLoading(false) } }, [getUpcomingEvents]) useEffect(() => { loadEvents(currentDate) }, [loadEvents, currentDate]) // Generate calendar grid for current month view const calendarDays = useMemo((): DayCell[] => { const year = currentDate.getFullYear() const month = currentDate.getMonth() // First day of the month const firstDay = new Date(year, month, 1) // Last day of the month const lastDay = new Date(year, month + 1, 0) // Get the day of week for first day (0 = Sunday, convert to Monday start) let startDayOfWeek = firstDay.getDay() startDayOfWeek = startDayOfWeek === 0 ? 6 : startDayOfWeek - 1 // Convert to Monday = 0 const days: DayCell[] = [] const today = new Date() today.setHours(0, 0, 0, 0) // Add days from previous month to fill the first week const prevMonth = new Date(year, month, 0) for (let i = startDayOfWeek - 1; i >= 0; i--) { const date = new Date(year, month - 1, prevMonth.getDate() - i) days.push({ date, isCurrentMonth: false, isToday: date.getTime() === today.getTime(), events: getEventsForDate(date, events), }) } // Add days of current month for (let day = 1; day <= lastDay.getDate(); day++) { const date = new Date(year, month, day) days.push({ date, isCurrentMonth: true, isToday: date.getTime() === today.getTime(), events: getEventsForDate(date, events), }) } // Add days from next month to complete the grid (6 rows) const remainingDays = 42 - days.length // 6 weeks * 7 days for (let day = 1; day <= remainingDays; day++) { const date = new Date(year, month + 1, day) days.push({ date, isCurrentMonth: false, isToday: date.getTime() === today.getTime(), events: getEventsForDate(date, events), }) } return days }, [currentDate, events]) const goToPreviousMonth = () => { setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)) } const goToNextMonth = () => { setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)) } const goToToday = () => { setCurrentDate(new Date()) } const formatEventTime = (event: CalendarEvent) => { if (event.all_day) return '' const date = new Date(event.start_time) return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }).replace(' ', '') } return (
Back

{MONTH_NAMES[currentDate.getMonth()]} {currentDate.getFullYear()}

{error && (

{error}

)}
{/* Day headers */} {DAYS_OF_WEEK.map(day => (
{day}
))} {/* Calendar cells */} {calendarDays.map((day, index) => (
{day.date.getDate()}
{day.events.slice(0, 4).map((event, eventIndex) => (
{ e.stopPropagation() setSelectedEvent(event) }} > {!event.all_day && ( {formatEventTime(event)} )} {event.event_title}
))} {day.events.length > 4 && (
+{day.events.length - 4} more
)}
))}
{loading &&
Loading events...
} {/* Event Detail Modal */} {selectedEvent && (
setSelectedEvent(null)}>
e.stopPropagation()}>

{selectedEvent.event_title}

Date {new Date(selectedEvent.start_time).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
Time {selectedEvent.all_day ? 'All day' : ( <> {new Date(selectedEvent.start_time).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} {selectedEvent.end_time && ( <> – {new Date(selectedEvent.end_time).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} )} )}
{selectedEvent.location && (
Location {selectedEvent.location}
)} {selectedEvent.calendar_name && (
Calendar {selectedEvent.calendar_name}
)} {selectedEvent.recurrence_rule && (
Repeats Recurring event
)}
)}
Configure calendar accounts
) } function getEventsForDate(date: Date, events: CalendarEvent[]): CalendarEvent[] { const dateStr = date.toISOString().split('T')[0] return events.filter(event => { const eventDate = new Date(event.start_time).toISOString().split('T')[0] return eventDate === dateStr }).sort((a, b) => { // All-day events first, then by time if (a.all_day && !b.all_day) return -1 if (!a.all_day && b.all_day) return 1 return new Date(a.start_time).getTime() - new Date(b.start_time).getTime() }) } export default Calendar