/** * 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 { callMiniAppReadyOnce } from "@/lib/telegram-ready"; import { ErrorState } from "@/components/states/ErrorState"; import { AccessDenied } from "@/components/states/AccessDenied"; /** 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, loading, error, accessDenied, accessDeniedDetail, duties, calendarEvents, selectedDay, nextMonth, prevMonth, setCurrentMonth, setSelectedDay, } = useAppStore( useShallow((s) => ({ currentMonth: s.currentMonth, loading: s.loading, error: s.error, accessDenied: s.accessDenied, accessDeniedDetail: s.accessDeniedDetail, duties: s.duties, calendarEvents: s.calendarEvents, selectedDay: s.selectedDay, nextMonth: s.nextMonth, prevMonth: s.prevMonth, setCurrentMonth: s.setCurrentMonth, setSelectedDay: s.setSelectedDay, })) ); 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; prevMonth(); }, [navDisabled, prevMonth]); const handleNextMonth = useCallback(() => { if (navDisabled) return; 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 handleGoToToday = useCallback(() => { setCurrentMonth(new Date()); retry(); }, [setCurrentMonth, retry]); const readyCalledRef = useRef(false); // Signal Telegram to hide loading when the first load finishes (loading goes true -> false). useEffect(() => { if (!loading && !readyCalledRef.current) { readyCalledRef.current = true; callMiniAppReadyOnce(); } }, [loading]); return (
{accessDenied && ( )} {!accessDenied && error && ( )} {!accessDenied && !error && ( )}
); }