Files
vidconf/frontend/src/components/room/RoomTopbar.tsx
Max Ronzhin 3290eb5d07 feat(room): показывать название конференции вместо технического room_name
RoomTopbar рендерил `JoinOut.room_name` — техническое имя комнаты LiveKit
вида `hC-Dmos9KEM`, которое туда попало по ошибке (перепутано с
человекочитаемым title конференции). Теперь топбар получает `title`
(из резолва номера/ссылки или из карточки конференции в лобби) и падает
на «Конференция без названия», если организатор его не задал. Номер и
slug остаются на своих местах — в инвайт-чипе и в адресе, где они нужны
функционально.
2026-07-30 00:23:18 +03:00

70 lines
3.1 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 { Check, Copy, Users } from 'lucide-react'
import { useParticipants } from '@livekit/components-react'
import { useState } from 'react'
import { pluralizeParticipants } from '@/lib/pluralize'
interface RoomTopbarProps {
/**
* Название конференции (`ConferenceOut.title`/`ResolveOut.title`) —
* `null`/не задано, если организатор его не указал. НЕ путать с
* `JoinOut.room_name` (техническое имя комнаты LiveKit вида `hC-Dmos9KEM`,
* которое сюда раньше и попадало по ошибке, см. задачу A6) — тот нужен
* только серверу и в UI показывать нечего.
*/
title: string | null
/** Slug/номер конференции из адреса — для инвайт-чипа (копирование ссылки). */
slug?: string
number?: string
}
/**
* Верхняя панель комнаты: название, число участников и инвайт-чип
* (номер + копирование ссылки-приглашения `/j/:slug`).
*
* Примечание: в design/mockups/room.html этого чипа нет — он добавлен,
* чтобы ссылка и номер конференции были видны и копировались, в стиле
* тёмных токенов темы `room` (см. `--color-room-tile*`), без новых
* цветов и форм.
*/
export function RoomTopbar({ title, slug, number }: RoomTopbarProps) {
const participants = useParticipants()
const [copied, setCopied] = useState(false)
async function handleCopy() {
if (!slug) return
const link = `${window.location.origin}/j/${slug}`
try {
await navigator.clipboard.writeText(link)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch {
// Буфер обмена недоступен (нет разрешения/не https) — молча игнорируем,
// это некритичный вспомогательный элемент интерфейса.
}
}
return (
<header className="room-topbar">
<div className="room-title-block">
<h1>{title ?? 'Конференция без названия'}</h1>
<p>
<span className="rec-dot" aria-hidden="true" />
Конференция активна · <Users className="lucide" style={{ width: 14, height: 14 }} aria-hidden="true" />{' '}
{participants.length} {pluralizeParticipants(participants.length)}
</p>
</div>
{slug && (
<button type="button" className="room-invite-chip" onClick={handleCopy}>
<span>{number ? `${number}` : slug}</span>
{copied ? (
<Check style={{ width: 14, height: 14 }} aria-hidden="true" />
) : (
<Copy style={{ width: 14, height: 14 }} aria-hidden="true" />
)}
{copied ? 'Скопировано' : 'Пригласить'}
</button>
)}
</header>
)
}