import { useCallback, useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { Plus } from 'lucide-react' import { ShellTopbar } from '@/components/layout/ShellTopbar' import { AppFooter } from '@/components/layout/AppFooter' import { ConferenceCalendar, type CalendarViewMode } from '@/components/calendar/ConferenceCalendar' import { ConferenceFormCard } from '@/components/calendar/ConferenceFormCard' import { ConferenceOccurrenceDialog } from '@/components/calendar/ConferenceOccurrenceDialog' import { useModalDismiss } from '@/hooks/useModalDismiss' import { getCalendarOccurrences, getMyConferences, type ConferenceOut, type OccurrenceOut } from '@/api/conferences' import { pluralizeConferences } from '@/lib/pluralize' import '@/styles/lobby.css' import '@/styles/calendar.css' interface VisibleRange { fromIso: string toIso: string } /** Стабильный дефолт для пропса `occurrences` `ConferenceCalendar` (пока `useQuery` не загрузил данные) — см. комментарий в ConferenceCalendar.tsx про identity пропсов FullCalendar. */ const EMPTY_OCCURRENCES: OccurrenceOut[] = [] /** * Календарь-менеджер запланированных/закреплённых конференций * (см. design/mockups/calendar.html). * * Форма создания/редактирования (`ConferenceFormCard`) живёт в модалке, не в * сетке страницы — календарь занимает всю ширину. Клик по чипу (в т.ч. по * своей конференции) открывает информационное окно (`ConferenceOccurrenceDialog`); * редактирование — отдельное действие: карандашик на чипе (только у * владельца, см. `myConferenceById`) или кнопка «Редактировать» в самом * информационном окне. */ export function CalendarPage() { const [view, setView] = useState('dayGridWeek') const [range, setRange] = useState(null) const [isFormOpen, setIsFormOpen] = useState(false) const [editingConference, setEditingConference] = useState(null) const [occurrenceDialogTarget, setOccurrenceDialogTarget] = useState(null) const { data: occurrences, isLoading: occurrencesLoading } = useQuery({ queryKey: ['conferences', 'calendar', range?.fromIso, range?.toIso], queryFn: () => getCalendarOccurrences(range!.fromIso, range!.toIso), enabled: !!range, }) const { data: myConferences } = useQuery({ queryKey: ['conferences', 'my'], queryFn: getMyConferences }) const myConferenceById = useMemo(() => new Map((myConferences ?? []).map((c) => [c.id, c])), [myConferences]) const openCreateForm = useCallback(() => { setEditingConference(null) setIsFormOpen(true) }, []) const openEditForm = useCallback((conference: ConferenceOut) => { setEditingConference(conference) setIsFormOpen(true) setOccurrenceDialogTarget(null) }, []) const closeForm = useCallback(() => setIsFormOpen(false), []) // Бэйл-аут по значению (не по ссылке): FullCalendar вызывает `datesSet` // повторно всякий раз, когда меняется identity его пропсов (см. подробный // комментарий в ConferenceCalendar.tsx). Если диапазон фактически не // изменился, `setRange` возвращает тот же объект — React пропускает // ре-рендер (Object.is-бэйлаут), и цикл не запускается даже если // `datesSet` дёрнется ещё раз вхолостую. const handleRangeChange = useCallback((fromIso: string, toIso: string) => { setRange((prev) => (prev && prev.fromIso === fromIso && prev.toIso === toIso ? prev : { fromIso, toIso })) }, []) const dialogOwnedConference = occurrenceDialogTarget ? myConferenceById.get(occurrenceDialogTarget.conference_id) : undefined return (

Календарь конференций

{occurrences?.length ?? 0} {pluralizeConferences(occurrences?.length ?? 0)} в выбранном периоде — запланируйте разовую встречу или закрепите постоянную конференцию с повторением

{isFormOpen && ( )} {occurrenceDialogTarget && ( setOccurrenceDialogTarget(null)} onEdit={openEditForm} /> )}
) } interface ConferenceFormModalProps { conference: ConferenceOut | null onSaved: () => void onDeleted: () => void onClose: () => void } /** Модалка создания/редактирования — разметка по образцу `ConferenceOccurrenceDialog` (modal-overlay/modal-panel, закрытие по клику на оверлей, Escape и возврат фокуса). */ function ConferenceFormModal({ conference, onSaved, onDeleted, onClose }: ConferenceFormModalProps) { useModalDismiss(onClose) return (
e.stopPropagation()}>
) }