diff --git a/frontend/src/components/calendar/CalendarPage.tsx b/frontend/src/components/calendar/CalendarPage.tsx index dff063a..cde92b4 100644 --- a/frontend/src/components/calendar/CalendarPage.tsx +++ b/frontend/src/components/calendar/CalendarPage.tsx @@ -51,7 +51,10 @@ export default function CalendarPage() { const [createDefaults, setCreateDefaults] = useState(null); const { settings } = useSettings(); + const firstDayOfWeek = settings?.first_day_of_week ?? 0; const { data: calendars = [], sharedData, allCalendarIds } = useCalendars({ pollingEnabled: true }); + const [currentDate, setCurrentDate] = useState(() => format(new Date(), 'yyyy-MM-dd')); + const [navKey, setNavKey] = useState(0); const [visibleSharedIds, setVisibleSharedIds] = useState>(new Set()); const calendarContainerRef = useRef(null); @@ -107,6 +110,10 @@ export default function CalendarPage() { localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarWidth)); }, [sidebarWidth]); + const handleMiniCalClick = useCallback((dateStr: string) => { + calendarRef.current?.getApi().gotoDate(dateStr); + }, []); + // Location data for event panel const { data: locations = [] } = useQuery({ queryKey: ['locations'], @@ -513,6 +520,10 @@ export default function CalendarPage() { setVisibleRange((prev) => prev.start === start && prev.end === end ? prev : { start, end } ); + // Track current date anchor for mini calendar sync + setCurrentDate(format(arg.view.currentStart, 'yyyy-MM-dd')); + // Increment nav key so mini calendar clears selection even when month doesn't change + setNavKey((k) => k + 1); }; const navigatePrev = () => calendarRef.current?.getApi().prev(); @@ -591,7 +602,7 @@ export default function CalendarPage() { return (
- +
setMobileSidebarOpen(false)} /> - { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} /> + { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} onDateClick={(dateStr) => { setMobileSidebarOpen(false); handleMiniCalClick(dateStr); }} currentDate={currentDate} firstDayOfWeek={firstDayOfWeek} navKey={navKey} /> )} @@ -712,12 +723,12 @@ export default function CalendarPage() { >
void; onSharedVisibilityChange?: (visibleIds: Set) => void; width: number; + onDateClick?: (dateStr: string) => void; + currentDate?: string; + firstDayOfWeek?: number; + navKey?: number; } -const CalendarSidebar = forwardRef(function CalendarSidebar({ onUseTemplate, onSharedVisibilityChange, width }, ref) { +const CalendarSidebar = forwardRef(function CalendarSidebar({ onUseTemplate, onSharedVisibilityChange, width, onDateClick, currentDate, firstDayOfWeek, navKey }, ref) { const queryClient = useQueryClient(); const { data: calendars = [], sharedData: sharedCalendars = [] } = useCalendars(); const [showForm, setShowForm] = useState(false); @@ -95,20 +100,36 @@ const CalendarSidebar = forwardRef(functio return (
-
+
Calendars -
+ {onDateClick && ( +
+ +
+ )}
{/* Owned calendars list (non-shared only) */} -
+
+
+ + My Calendars + + +
+
{calendars.filter((c) => !c.is_shared).map((cal) => (
(functio
))}
+
{/* Shared calendars section -- owned + member */} {(calendars.some((c) => c.is_shared) || sharedCalendars.length > 0) && ( diff --git a/frontend/src/components/calendar/MiniCalendar.tsx b/frontend/src/components/calendar/MiniCalendar.tsx new file mode 100644 index 0000000..2fa3147 --- /dev/null +++ b/frontend/src/components/calendar/MiniCalendar.tsx @@ -0,0 +1,166 @@ +import { useState, useEffect, useMemo, useCallback, memo } from 'react'; +import { + startOfMonth, endOfMonth, startOfWeek, endOfWeek, + eachDayOfInterval, format, isSameDay, isSameMonth, isToday, + addMonths, subMonths, parse, +} from 'date-fns'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface MiniCalendarProps { + onDateClick: (dateStr: string) => void; + currentDate?: string; + firstDayOfWeek?: number; + navKey?: number; +} + +const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']; + +function buildGrid(displayed: Date, firstDay: number) { + const monthStart = startOfMonth(displayed); + const monthEnd = endOfMonth(displayed); + const gridStart = startOfWeek(monthStart, { weekStartsOn: firstDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 }); + const gridEnd = endOfWeek(monthEnd, { weekStartsOn: firstDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 }); + return eachDayOfInterval({ start: gridStart, end: gridEnd }); +} + +function getOrderedLabels(firstDay: number) { + return [...DAY_LABELS.slice(firstDay), ...DAY_LABELS.slice(0, firstDay)]; +} + +const MiniCalendar = memo(function MiniCalendar({ + onDateClick, + currentDate, + firstDayOfWeek = 0, + navKey, +}: MiniCalendarProps) { + const REF_DATE = useMemo(() => new Date(), []); + + const parseDate = useCallback( + (dateStr: string) => parse(dateStr, 'yyyy-MM-dd', REF_DATE), + [REF_DATE] + ); + + const [displayedMonth, setDisplayedMonth] = useState(() => + currentDate ? startOfMonth(parseDate(currentDate)) : startOfMonth(new Date()) + ); + const [selectedDate, setSelectedDate] = useState(null); + + // Sync displayed month when main calendar navigates across months + useEffect(() => { + if (!currentDate) return; + const incoming = startOfMonth(parseDate(currentDate)); + setDisplayedMonth((prev) => + prev.getTime() === incoming.getTime() ? prev : incoming + ); + }, [currentDate, parseDate]); + + // Clear selection on any toolbar navigation (today/prev/next) + // navKey increments on every datesSet, even when the month doesn't change + useEffect(() => { + setSelectedDate(null); + }, [navKey]); + + const days = useMemo( + () => buildGrid(displayedMonth, firstDayOfWeek), + [displayedMonth, firstDayOfWeek] + ); + + const orderedLabels = useMemo( + () => getOrderedLabels(firstDayOfWeek), + [firstDayOfWeek] + ); + + const handlePrev = useCallback(() => setDisplayedMonth((m) => subMonths(m, 1)), []); + const handleNext = useCallback(() => setDisplayedMonth((m) => addMonths(m, 1)), []); + + const handleDayClick = useCallback((day: Date) => { + const dateStr = format(day, 'yyyy-MM-dd'); + setSelectedDate(dateStr); + setDisplayedMonth((prev) => isSameMonth(day, prev) ? prev : startOfMonth(day)); + onDateClick(dateStr); + }, [onDateClick]); + + const selectedDateObj = useMemo( + () => selectedDate ? parseDate(selectedDate) : null, + [selectedDate, parseDate] + ); + + return ( +
+ {/* Month header with navigation */} +
+ + + {format(displayedMonth, 'MMMM yyyy')} + + +
+ + {/* Day-of-week headers */} +
+ {orderedLabels.map((label) => ( + + {label} + + ))} +
+ + {/* Day grid */} +
+ {days.map((day) => { + const isCurrentMonth = isSameMonth(day, displayedMonth); + const today = isToday(day); + const isSelected = selectedDateObj ? isSameDay(day, selectedDateObj) : false; + + return ( + + ); + })} +
+
+ ); +}); + +export default MiniCalendar;