`AppFooter` — версия инстанса из публичного `/api/health` (не требует авторизации, поэтому работает и на экранах входа; поля `version` может не быть — тогда версия не показывается) плюс копирайт с ТЕКУЩИМ годом и ссылкой на профиль автора. Подключён во все раскладки: `page-shell` (календарь, админка, профиль, мои конференции), `AuthLayout` (вход, регистрация, подтверждение email), `join-shell` и `lobby-shell`. В комнате конференции футера нет — там полноэкранное видео. Прежний футер админки (`.admin-footer`) удалён вместе со стилем — дубля не осталось. В брендовой панели экранов auth строка «© 2026 VidConf · self-hosted» ужата до «self-hosted»: год там был захардкожен, а два копирайта на одном экране — дубль.
121 lines
5.4 KiB
TypeScript
121 lines
5.4 KiB
TypeScript
import { useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { Calendar, Link2, User, Video } from 'lucide-react'
|
||
import { ShellTopbar } from '@/components/layout/ShellTopbar'
|
||
import { AppFooter } from '@/components/layout/AppFooter'
|
||
import { LogoMedallion } from '@/components/ui/LogoMark'
|
||
import { createConference } from '@/api/conferences'
|
||
import { useToast } from '@/components/ui/ToastProvider'
|
||
import '@/styles/lobby.css'
|
||
|
||
/**
|
||
* Лобби — «хаб действий» (см. design/mockups/lobby.html).
|
||
*
|
||
* Предустановленных переговорных комнат больше нет: конференции создаются
|
||
* динамически. Мозаика из 4 плиток — единственный вход в остальные сценарии:
|
||
* мгновенное создание, календарь плановых конференций, вход по ссылке/номеру,
|
||
* «Мои конференции».
|
||
*
|
||
* Отклонение от макета: топбар здесь — переиспользуемый `ShellTopbar` (прямые
|
||
* ссылки Лобби/Календарь/Админка + пилюля пользователя), а не бургер-меню из
|
||
* lobby.html. `ShellTopbar` — общий компонент светлой оболочки (используется
|
||
* также в календаре/админке), поэтому бургер-меню из макета здесь
|
||
* сознательно не воспроизводится.
|
||
*/
|
||
export function LobbyPage() {
|
||
const navigate = useNavigate()
|
||
const toast = useToast()
|
||
const [creating, setCreating] = useState(false)
|
||
|
||
async function handleCreate() {
|
||
if (creating) return
|
||
setCreating(true)
|
||
try {
|
||
const conference = await createConference({})
|
||
if (!conference.join) {
|
||
toast.show('Не удалось создать конференцию. Попробуйте ещё раз', 'error')
|
||
return
|
||
}
|
||
navigate(`/room/${conference.slug}`, {
|
||
state: {
|
||
livekitUrl: conference.join.livekit_url,
|
||
token: conference.join.token,
|
||
roomName: conference.join.room_name,
|
||
conferenceId: conference.join.conference_id,
|
||
chatEnabled: conference.join.chat_enabled,
|
||
number: conference.number,
|
||
slug: conference.slug,
|
||
},
|
||
})
|
||
} catch {
|
||
toast.show('Не удалось создать конференцию. Попробуйте ещё раз', 'error')
|
||
} finally {
|
||
setCreating(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="lobby-shell">
|
||
<ShellTopbar />
|
||
<main className="hub-main">
|
||
<div className="hub-wrap">
|
||
<div className="hub-intro">
|
||
<p className="eyebrow">[ ДОБРО ПОЖАЛОВАТЬ В VIDCONF ]</p>
|
||
<h1>Что делаем сегодня?</h1>
|
||
</div>
|
||
|
||
<div className="hub">
|
||
<button
|
||
type="button"
|
||
className="action action--create"
|
||
onClick={handleCreate}
|
||
disabled={creating}
|
||
aria-busy={creating}
|
||
>
|
||
<span className="action-icon" aria-hidden="true">
|
||
<Video className="icon" aria-hidden="true" />
|
||
</span>
|
||
<h2 className="action-title">Создать</h2>
|
||
<p className="action-object">конференцию</p>
|
||
<p className="action-note">{creating ? 'Создаём…' : 'начнётся сразу — вы первый участник'}</p>
|
||
</button>
|
||
|
||
<button type="button" className="action action--calendar" onClick={() => navigate('/calendar')}>
|
||
<span className="action-icon" aria-hidden="true">
|
||
<Calendar className="icon" aria-hidden="true" />
|
||
</span>
|
||
<h2 className="action-title">Календарь</h2>
|
||
<p className="action-object">конференций</p>
|
||
<p className="action-note">запланировать · закрепить · повторять</p>
|
||
</button>
|
||
|
||
<button type="button" className="action action--join" onClick={() => navigate('/join')}>
|
||
<span className="action-icon" aria-hidden="true">
|
||
<Link2 className="icon" aria-hidden="true" />
|
||
</span>
|
||
<h2 className="action-title">Подключиться</h2>
|
||
<p className="action-object">к конференции</p>
|
||
<p className="action-note">по ссылке или номеру</p>
|
||
</button>
|
||
|
||
<button type="button" className="action action--mine" onClick={() => navigate('/my-conferences')}>
|
||
<span className="action-icon" aria-hidden="true">
|
||
<User className="icon" aria-hidden="true" />
|
||
</span>
|
||
<h2 className="action-title">Мои</h2>
|
||
<p className="action-object">конференции</p>
|
||
<p className="action-note">закреплённые и предстоящие</p>
|
||
</button>
|
||
|
||
<div className="hub-logo" aria-hidden="true">
|
||
<LogoMedallion className="logo-mark" />
|
||
<span className="logo-word">VidConf</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
<AppFooter />
|
||
</div>
|
||
)
|
||
}
|