ConferenceOccurrenceDialog расширен под общий информационный диалог (клик по чипу — и для чужих, и для своих конференций): статус-бейдж (общая логика occurrenceBadge вынесена в lib/occurrenceStatus.ts, переиспользуется с ConferenceCalendar), кнопка «Редактировать», открывающая модалку формы — только когда передан ownedConference (текущий пользователь — организатор). Плюс Escape и возврат фокуса (useModalDismiss).
97 lines
5.0 KiB
TypeScript
97 lines
5.0 KiB
TypeScript
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>
|
||
)
|
||
}
|