/** * Calendar view layout: header, grid, duty list, day detail. * Composes calendar UI and owns sticky scroll, swipe, month data, and day-detail ref. */ "use client"; import { useRef, useState, useEffect, useCallback } from "react"; import { useAppStore } from "@/store/app-store"; import { useShallow } from "zustand/react/shallow"; import { useMonthData } from "@/hooks/use-month-data"; import { useSwipe } from "@/hooks/use-swipe"; import { useStickyScroll } from "@/hooks/use-sticky-scroll"; import { useAutoRefresh } from "@/hooks/use-auto-refresh"; import { CalendarHeader } from "@/components/calendar/CalendarHeader"; import { CalendarGrid } from "@/components/calendar/CalendarGrid"; import { DutyList } from "@/components/duty/DutyList"; import { DayDetail, type DayDetailHandle } from "@/components/day-detail"; import { ErrorState } from "@/components/states/ErrorState"; import { triggerHapticLight } from "@/lib/telegram-haptic"; /** Fallback height (px) until ResizeObserver reports the sticky block size. Matches --calendar-block-min-height + pb-2 (260 + 8). */ const STICKY_HEIGHT_FALLBACK_PX = 268; export interface CalendarPageProps { /** Whether the user is allowed (for data loading). */ isAllowed: boolean; /** Raw initData string for API auth. */ initDataRaw: string | undefined; } export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) { const dayDetailRef = useRef(null); const calendarStickyRef = useRef(null); const [stickyBlockHeight, setStickyBlockHeight] = useState(STICKY_HEIGHT_FALLBACK_PX); useEffect(() => { const el = calendarStickyRef.current; if (!el) return; const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) setStickyBlockHeight(entry.contentRect.height); }); observer.observe(el); return () => observer.disconnect(); }, []); const { currentMonth, pendingMonth, loading, error, accessDenied, duties, calendarEvents, selectedDay, nextMonth, prevMonth, setCurrentMonth, setSelectedDay, setAppContentReady, } = useAppStore( useShallow((s) => ({ currentMonth: s.currentMonth, pendingMonth: s.pendingMonth, loading: s.loading, error: s.error, accessDenied: s.accessDenied, duties: s.duties, calendarEvents: s.calendarEvents, selectedDay: s.selectedDay, nextMonth: s.nextMonth, prevMonth: s.prevMonth, setCurrentMonth: s.setCurrentMonth, setSelectedDay: s.setSelectedDay, setAppContentReady: s.setAppContentReady, })) ); const { retry } = useMonthData({ initDataRaw, enabled: isAllowed, }); const now = new Date(); const isCurrentMonth = currentMonth.getFullYear() === now.getFullYear() && currentMonth.getMonth() === now.getMonth(); useAutoRefresh(retry, isCurrentMonth); const navDisabled = loading || accessDenied || selectedDay !== null; const handlePrevMonth = useCallback(() => { if (navDisabled) return; triggerHapticLight(); prevMonth(); }, [navDisabled, prevMonth]); const handleNextMonth = useCallback(() => { if (navDisabled) return; triggerHapticLight(); nextMonth(); }, [navDisabled, nextMonth]); useSwipe( calendarStickyRef, handleNextMonth, handlePrevMonth, { threshold: 50, disabled: navDisabled } ); useStickyScroll(calendarStickyRef); const handleDayClick = useCallback( (dateKey: string, anchorRect: DOMRect) => { const [y, m] = dateKey.split("-").map(Number); if ( y !== currentMonth.getFullYear() || m !== currentMonth.getMonth() + 1 ) { return; } dayDetailRef.current?.openWithRect(dateKey, anchorRect); }, [currentMonth] ); const handleCloseDayDetail = useCallback(() => { setSelectedDay(null); }, [setSelectedDay]); const readyCalledRef = useRef(false); // Mark content ready when first load finishes or access denied, so page can call ready() and show content. useEffect(() => { if ((!loading || accessDenied) && !readyCalledRef.current) { readyCalledRef.current = true; setAppContentReady(true); } }, [loading, accessDenied, setAppContentReady]); return (
{error && ( )} {!error && ( )}
); }