Files
vidconf/frontend/src/components/calendar/ConferenceOccurrenceDialog.tsx
Max Ronzhin c897f40363
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
feat(calendar): информационное окно вхождения — статус, кнопка «Редактировать» для владельца
ConferenceOccurrenceDialog расширен под общий информационный диалог (клик по
чипу — и для чужих, и для своих конференций): статус-бейдж (общая логика
occurrenceBadge вынесена в lib/occurrenceStatus.ts, переиспользуется с
ConferenceCalendar), кнопка «Редактировать», открывающая модалку формы —
только когда передан ownedConference (текущий пользователь — организатор).
Плюс Escape и возврат фокуса (useModalDismiss).
2026-07-26 23:35:10 +03:00

97 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Link, useNavigate } from 'react-router-dom'
import { Calendar, Hash, Lock, Pencil, Play, Repeat, X } from 'lucide-react'
import type { ConferenceOut, OccurrenceOut } from '@/api/conferences'
import { occurrenceBadge, OCCURRENCE_BADGE_ICON } from '@/lib/occurrenceStatus'
import { useModalDismiss } from '@/hooks/useModalDismiss'
interface ConferenceOccurrenceDialogProps {
occurrence: OccurrenceOut
/** Присутствует только когда текущий пользователь — организатор этого вхождения (`is_owner`) — показывает кнопку «Редактировать». */
ownedConference?: ConferenceOut
onClose: () => void
onEdit?: (conference: ConferenceOut) => void
}
function formatRange(startsAt: string, endsAt: string): string {
const start = new Date(startsAt)
const end = new Date(endsAt)
const dateLabel = start.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })
const startTime = start.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
const endTime = end.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
return `${dateLabel}, ${startTime}${endTime}`
}
/**
* Быстрый просмотр вхождения конференции по клику на чип в календаре — для
* всех конференций, включая свои: редактирование теперь отдельное действие
* (карандашик на чипе или кнопка «Редактировать» здесь, у владельца), сам
* клик по чипу всегда открывает это окно. Показывает статус, время, номер и,
* для владельца, вход в редактирование; подробности состава
* участников/организатора — в ховер-карточке при наведении/фокусе на чип
* (`ConferenceHoverCard`).
*/
export function ConferenceOccurrenceDialog({ occurrence, ownedConference, onClose, onEdit }: ConferenceOccurrenceDialogProps) {
const navigate = useNavigate()
useModalDismiss(onClose)
const badge = occurrenceBadge(occurrence, new Date(occurrence.starts_at), new Date(occurrence.ends_at))
const BadgeIcon = OCCURRENCE_BADGE_ICON[badge.kind]
return (
<div className="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="occurrence-dialog-title" onClick={onClose}>
<div className="modal-panel" onClick={(e) => e.stopPropagation()}>
<div className="modal-head">
<h2 id="occurrence-dialog-title">
{occurrence.is_closed && <Lock className="title-lock" aria-hidden="true" />}
{occurrence.title ?? 'Конференция без названия'}
</h2>
<button type="button" className="modal-close" aria-label="Закрыть" onClick={onClose}>
<X className="lucide" style={{ width: 16, height: 16 }} aria-hidden="true" />
</button>
</div>
<span className={`conf-badge ${badge.kind}`} style={{ marginBottom: 'var(--space-4)' }}>
<BadgeIcon style={{ width: 11, height: 11 }} aria-hidden="true" />
{badge.label}
</span>
<div className="detail-row">
<Calendar style={{ width: 18, height: 18 }} aria-hidden="true" />
<span className="label">Когда</span>
<span>{formatRange(occurrence.starts_at, occurrence.ends_at)}</span>
</div>
<div className="detail-row">
<Hash style={{ width: 18, height: 18 }} aria-hidden="true" />
<span className="label">Номер</span>
<span>{occurrence.number}</span>
</div>
{occurrence.is_pinned && (
<div className="detail-row">
<Repeat style={{ width: 18, height: 18 }} aria-hidden="true" />
<span className="label">Статус</span>
<span>Закреплённая конференция повторяется по расписанию</span>
</div>
)}
<div className="modal-actions">
<button type="button" className="btn btn-primary" onClick={() => navigate(`/j/${occurrence.slug}`)}>
<Play style={{ width: 16, height: 16 }} aria-hidden="true" />
Войти
</button>
{ownedConference && onEdit && (
<button type="button" className="btn btn-secondary" onClick={() => onEdit(ownedConference)}>
<Pencil style={{ width: 16, height: 16 }} aria-hidden="true" />
Редактировать
</button>
)}
</div>
{!ownedConference && (
<p className="field-hint" style={{ textAlign: 'center', marginTop: 'var(--space-3)' }}>
Управление и редактирование в <Link to="/my-conferences">«Моих конференциях»</Link>
</p>
)}
</div>
</div>
)
}