import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useQuery } from '@tanstack/react-query' import { AlertTriangle, CheckCircle2, Clock, Lock, Pencil, Play, Repeat } from 'lucide-react' import { ShellTopbar } from '@/components/layout/ShellTopbar' import { AppFooter } from '@/components/layout/AppFooter' import { ConferenceFormCard } from '@/components/calendar/ConferenceFormCard' import { ConferenceHoverCard } from '@/components/ui/ConferenceHoverCard' import { CopyPill } from '@/components/ui/CopyPill' import { getMyConferences, type ConferenceOut } from '@/api/conferences' import { formatNextOccurrence } from '@/lib/localTime' import { addMinutesToTime, formatRecurrenceSummary } from '@/lib/recurrenceFormat' import '@/styles/lobby.css' import '@/styles/my-conferences.css' type Filter = 'all' | 'pinned' | 'upcoming' /** До начала разовой конференции можно войти заранее — порог «скоро начнётся» (UX-эвристика, не ограничение backend). */ const EARLY_JOIN_WINDOW_MS = 10 * 60 * 1000 function formatRelativeStart(iso: string): string { const diffMs = new Date(iso).getTime() - Date.now() const diffMin = Math.round(diffMs / 60_000) if (diffMin <= 0) return 'вот-вот' if (diffMin < 60) return `через ${diffMin} мин` const diffHours = Math.round(diffMin / 60) if (diffHours < 24) return `через ${diffHours} ч` return `через ${Math.round(diffHours / 24)} дн.` } function formatSubtitle(conf: ConferenceOut): string { if (conf.is_pinned && conf.recurrence) return formatRecurrenceSummary(conf.recurrence) if (!conf.scheduled_at) return 'Разовая конференция' const start = new Date(conf.scheduled_at) const dateLabel = start.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long' }) const timeLabel = start.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) const endLabel = conf.duration_minutes ? addMinutesToTime(timeLabel, conf.duration_minutes) : null return `Разовая · ${dateLabel}, ${timeLabel}${endLabel ? `–${endLabel}` : ''}` } /** * «Мои конференции» (design/mockups/my-conferences.html): * закреплённые (повторяющиеся) и предстоящие разовые конференции, где текущий * пользователь — организатор или приглашённый участник (решение от 2026-07-20, * см. ADR-003); кнопки владельца скрываются по `is_owner`. * Кнопка редактирования показывается только организатору (`is_owner`); * состав участников — в ховер-карточке (ленивый запрос по наведению/фокусу). * * Контракт `/conferences/my` не возвращает завершённые конференции — фильтр * «Завершённые» и карточка «Открыть саммари» из макета опущены. */ export function MyConferencesPage() { const navigate = useNavigate() const [filter, setFilter] = useState('all') const [editingConference, setEditingConference] = useState(null) const [hoverTarget, setHoverTarget] = useState<{ conferenceId: string; anchorEl: HTMLElement } | null>(null) const { data: conferences, isPending, isError, refetch, } = useQuery({ queryKey: ['conferences', 'my'], queryFn: getMyConferences }) const list = useMemo(() => conferences ?? [], [conferences]) const pinnedCount = list.filter((c) => c.is_pinned).length const filtered = useMemo(() => { if (filter === 'pinned') return list.filter((c) => c.is_pinned) if (filter === 'upcoming') return list.filter((c) => !c.is_pinned) return list }, [list, filter]) function handleJoin(conf: ConferenceOut) { navigate(`/j/${conf.slug}`) } return (

Мои конференции

{list.length} {list.length === 1 ? 'конференция' : 'конференций'} · {pinnedCount} закреплённых с повторением

{isPending ? (

Загрузка конференций…

) : isError ? (
) : filtered.length === 0 ? (

Здесь пока пусто — запланируйте конференцию в календаре.

) : (
{filtered.map((conf) => { const joinable = conf.is_pinned || conf.status === 'active' || (conf.scheduled_at ? new Date(conf.scheduled_at).getTime() - new Date().getTime() <= EARLY_JOIN_WINDOW_MS : false) const link = `${window.location.origin}/j/${conf.slug}` return (
setHoverTarget({ conferenceId: conf.id, anchorEl: e.currentTarget })} onMouseLeave={() => setHoverTarget((prev) => (prev?.conferenceId === conf.id ? null : prev))} onFocus={(e) => setHoverTarget({ conferenceId: conf.id, anchorEl: e.currentTarget })} onBlur={() => setHoverTarget((prev) => (prev?.conferenceId === conf.id ? null : prev))} >

{conf.is_closed &&

{formatSubtitle(conf)}

{conf.is_pinned ? ( ) : conf.status === 'active' ? ( ) : conf.status === 'ended' ? ( ) : ( )}
{conf.is_pinned && conf.next_occurrence && (
)} {conf.status !== 'ended' && (
)}
{conf.status === 'ended' ? ( ) : joinable ? ( ) : ( )} {conf.status !== 'ended' && conf.is_owner && ( )}
) })}
)}
{editingConference && (
setEditingConference(null)}>
e.stopPropagation()}> setEditingConference(null)} onDeleted={() => setEditingConference(null)} onCancelEdit={() => setEditingConference(null)} />
)}
) }