// Calendar Panel - Month/Week view with event list // Used inside CalendarBrowserShape import React, { useState, useMemo, useCallback } from "react" import { useCalendarEvents, type DecryptedCalendarEvent, } from "@/hooks/useCalendarEvents" interface CalendarPanelProps { onClose?: () => void onEventSelect?: (event: DecryptedCalendarEvent) => void shapeMode?: boolean initialView?: "month" | "week" initialDate?: Date } type ViewMode = "month" | "week" // Helper functions const getDaysInMonth = (year: number, month: number) => { return new Date(year, month + 1, 0).getDate() } const getFirstDayOfMonth = (year: number, month: number) => { const day = new Date(year, month, 1).getDay() // Convert Sunday (0) to 7 for Monday-first week return day === 0 ? 6 : day - 1 } const formatMonthYear = (date: Date) => { return date.toLocaleDateString("en-US", { month: "long", year: "numeric" }) } const isSameDay = (date1: Date, date2: Date) => { return ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() ) } const isToday = (date: Date) => { return isSameDay(date, new Date()) } export function CalendarPanel({ onClose: _onClose, onEventSelect, shapeMode: _shapeMode = false, initialView = "month", initialDate, }: CalendarPanelProps) { const [viewMode, setViewMode] = useState(initialView) const [currentDate, setCurrentDate] = useState(initialDate || new Date()) const [selectedDate, setSelectedDate] = useState(null) // Detect dark mode const isDarkMode = typeof document !== "undefined" && document.documentElement.classList.contains("dark") // Get calendar events for the visible range const startOfVisibleRange = useMemo(() => { const year = currentDate.getFullYear() const month = currentDate.getMonth() // Start from previous month to show leading days return new Date(year, month - 1, 1) }, [currentDate]) const endOfVisibleRange = useMemo(() => { const year = currentDate.getFullYear() const month = currentDate.getMonth() // End at next month to show trailing days return new Date(year, month + 2, 0) }, [currentDate]) const { events, loading, error, refresh, getEventsForDate, getUpcoming } = useCalendarEvents({ startDate: startOfVisibleRange, endDate: endOfVisibleRange, }) // Colors const colors = isDarkMode ? { bg: "#1a1a1a", cardBg: "#252525", headerBg: "#22c55e", text: "#e4e4e7", textMuted: "#a1a1aa", border: "#404040", todayBg: "#22c55e20", selectedBg: "#3b82f620", eventDot: "#3b82f6", buttonBg: "#374151", buttonHover: "#4b5563", } : { bg: "#ffffff", cardBg: "#f9fafb", headerBg: "#22c55e", text: "#1f2937", textMuted: "#6b7280", border: "#e5e7eb", todayBg: "#22c55e15", selectedBg: "#3b82f615", eventDot: "#3b82f6", buttonBg: "#f3f4f6", buttonHover: "#e5e7eb", } // Navigation handlers const goToPrevious = () => { if (viewMode === "month") { setCurrentDate( new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1) ) } else { const newDate = new Date(currentDate) newDate.setDate(newDate.getDate() - 7) setCurrentDate(newDate) } } const goToNext = () => { if (viewMode === "month") { setCurrentDate( new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1) ) } else { const newDate = new Date(currentDate) newDate.setDate(newDate.getDate() + 7) setCurrentDate(newDate) } } const goToToday = () => { setCurrentDate(new Date()) setSelectedDate(new Date()) } // Generate month grid const monthGrid = useMemo(() => { const year = currentDate.getFullYear() const month = currentDate.getMonth() const daysInMonth = getDaysInMonth(year, month) const firstDay = getFirstDayOfMonth(year, month) const days: { date: Date; isCurrentMonth: boolean }[] = [] // Previous month days const prevMonth = month === 0 ? 11 : month - 1 const prevYear = month === 0 ? year - 1 : year const daysInPrevMonth = getDaysInMonth(prevYear, prevMonth) for (let i = firstDay - 1; i >= 0; i--) { days.push({ date: new Date(prevYear, prevMonth, daysInPrevMonth - i), isCurrentMonth: false, }) } // Current month days for (let i = 1; i <= daysInMonth; i++) { days.push({ date: new Date(year, month, i), isCurrentMonth: true, }) } // Next month days to complete grid const nextMonth = month === 11 ? 0 : month + 1 const nextYear = month === 11 ? year + 1 : year const remainingDays = 42 - days.length // 6 rows * 7 days for (let i = 1; i <= remainingDays; i++) { days.push({ date: new Date(nextYear, nextMonth, i), isCurrentMonth: false, }) } return days }, [currentDate]) // Format event time const formatEventTime = (event: DecryptedCalendarEvent) => { if (event.isAllDay) return "All day" return event.startTime.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, }) } // Upcoming events for sidebar const upcomingEvents = useMemo(() => { return getUpcoming(10) }, [getUpcoming]) // Events for selected date const selectedDateEvents = useMemo(() => { if (!selectedDate) return [] return getEventsForDate(selectedDate) }, [selectedDate, getEventsForDate]) // Day cell component const DayCell = ({ date, isCurrentMonth, }: { date: Date isCurrentMonth: boolean }) => { const dayEvents = getEventsForDate(date) const isSelectedDate = selectedDate && isSameDay(date, selectedDate) const isTodayDate = isToday(date) return (
setSelectedDate(date)} style={{ padding: "4px", minHeight: "60px", cursor: "pointer", backgroundColor: isSelectedDate ? colors.selectedBg : isTodayDate ? colors.todayBg : "transparent", borderRadius: "4px", border: isTodayDate ? `2px solid ${colors.headerBg}` : "1px solid transparent", transition: "background-color 0.15s ease", }} onMouseEnter={(e) => { if (!isSelectedDate && !isTodayDate) { e.currentTarget.style.backgroundColor = colors.buttonBg } }} onMouseLeave={(e) => { if (!isSelectedDate && !isTodayDate) { e.currentTarget.style.backgroundColor = "transparent" } }} >
{date.getDate()}
{dayEvents.slice(0, 3).map((event) => (
))} {dayEvents.length > 3 && (
+{dayEvents.length - 3}
)}
) } // Event list item const EventItem = ({ event }: { event: DecryptedCalendarEvent }) => (
onEventSelect?.(event)} style={{ padding: "10px 12px", backgroundColor: colors.cardBg, borderRadius: "8px", cursor: "pointer", borderLeft: `3px solid ${colors.eventDot}`, transition: "background-color 0.15s ease", }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = colors.buttonBg }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = colors.cardBg }} >
{event.summary}
{formatEventTime(event)} {event.location && ( <> | {event.location} )}
) // Loading state if (loading) { return (
Loading...
Fetching calendar events
) } // Error state if (error) { return (
Error
{error}
) } // No events state if (events.length === 0) { return (
📅
No Calendar Events
Import your Google Calendar to see events here.
) } return (
{/* Main Calendar Area */}
{/* Navigation Header */}
{formatMonthYear(currentDate)}
{/* View toggle */}
{/* Calendar Grid */}
{/* Day headers */}
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((day) => (
{day}
))}
{/* Day cells */}
{monthGrid.map(({ date, isCurrentMonth }, i) => ( ))}
{/* Sidebar - Events */}
{/* Selected Date Events or Upcoming */}
{selectedDate ? selectedDate.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", }) : "Upcoming Events"}
{(selectedDate ? selectedDateEvents : upcomingEvents).map( (event) => ( ) )} {(selectedDate ? selectedDateEvents : upcomingEvents).length === 0 && (
{selectedDate ? "No events on this day" : "No upcoming events"}
)}
{/* Click hint */} {onEventSelect && (
Click an event to add it to the canvas
)}
) } export default CalendarPanel