feat(calendar): информационное окно вхождения — статус, кнопка «Редактировать» для владельца
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled

ConferenceOccurrenceDialog расширен под общий информационный диалог (клик по
чипу — и для чужих, и для своих конференций): статус-бейдж (общая логика
occurrenceBadge вынесена в lib/occurrenceStatus.ts, переиспользуется с
ConferenceCalendar), кнопка «Редактировать», открывающая модалку формы —
только когда передан ownedConference (текущий пользователь — организатор).
Плюс Escape и возврат фокуса (useModalDismiss).
This commit is contained in:
2026-07-26 23:35:10 +03:00
parent 66935b04f8
commit c897f40363
2 changed files with 59 additions and 12 deletions

View File

@@ -1,10 +1,15 @@
import { Link, useNavigate } from 'react-router-dom'
import { Calendar, Lock, Play, Repeat, X } from 'lucide-react'
import type { OccurrenceOut } from '@/api/conferences'
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 {
@@ -18,14 +23,19 @@ function formatRange(startsAt: string, endsAt: string): string {
/**
* Быстрый просмотр вхождения конференции по клику на чип в календаре — для
* конференций, которые пользователь не организует (не владелец —
* `is_owner`): свои конференции CalendarPage открывает сразу в форме
* редактирования (`ConferenceFormCard`). Здесь — минимум: когда, статус, вход
* и ссылка на управление; подробности состава участников/организатора — в
* ховер-карточке при наведении/фокусе на чип (`ConferenceHoverCard`).
* всех конференций, включая свои: редактирование теперь отдельное действие
* (карандашик на чипе или кнопка «Редактировать» здесь, у владельца), сам
* клик по чипу всегда открывает это окно. Показывает статус, время, номер и,
* для владельца, вход в редактирование; подробности состава
* участников/организатора — в ховер-карточке при наведении/фокусе на чип
* (`ConferenceHoverCard`).
*/
export function ConferenceOccurrenceDialog({ occurrence, onClose }: ConferenceOccurrenceDialogProps) {
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}>
@@ -40,13 +50,18 @@ export function ConferenceOccurrenceDialog({ occurrence, onClose }: ConferenceOc
</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">
<Repeat style={{ width: 18, height: 18 }} aria-hidden="true" />
<Hash style={{ width: 18, height: 18 }} aria-hidden="true" />
<span className="label">Номер</span>
<span>{occurrence.number}</span>
</div>
@@ -63,10 +78,18 @@ export function ConferenceOccurrenceDialog({ occurrence, onClose }: ConferenceOc
<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>
<p className="field-hint" style={{ textAlign: 'center', marginTop: 'var(--space-3)' }}>
Управление и редактирование в <Link to="/my-conferences">«Моих конференциях»</Link>
</p>
{!ownedConference && (
<p className="field-hint" style={{ textAlign: 'center', marginTop: 'var(--space-3)' }}>
Управление и редактирование в <Link to="/my-conferences">«Моих конференциях»</Link>
</p>
)}
</div>
</div>
)

View File

@@ -0,0 +1,24 @@
import { CheckCircle2, Clock, Play, Repeat } from 'lucide-react'
import type { OccurrenceOut } from '@/api/conferences'
export type OccurrenceBadgeKind = 'scheduled' | 'pinned' | 'live' | 'ended'
/** Общая логика статус-бейджа вхождения — используется чипом календаря и информационным окном (`ConferenceOccurrenceDialog`). */
export function occurrenceBadge(
occ: OccurrenceOut,
start: Date | null,
end: Date | null,
): { kind: OccurrenceBadgeKind; label: string } {
const now = new Date()
if (start && end && now >= start && now <= end) return { kind: 'live', label: 'Идёт сейчас' }
if (end && now > end) return { kind: 'ended', label: 'Завершена' }
if (occ.is_pinned) return { kind: 'pinned', label: 'Закреплена' }
return { kind: 'scheduled', label: 'Запланирована' }
}
export const OCCURRENCE_BADGE_ICON: Record<OccurrenceBadgeKind, typeof Clock> = {
scheduled: Clock,
pinned: Repeat,
live: Play,
ended: CheckCircle2,
}