Files
TeleWave/frontend/src/routes/__root.tsx
T

144 lines
5.3 KiB
TypeScript

import { useEffect, useState } from 'react'
import { createRootRoute, Link, Outlet, useRouterState } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Menu, Radio, X } from 'lucide-react'
import { useAuthStore } from '@/features/auth/store'
import { bootstrapSession, logout, clearSession } from '@/features/auth/api'
import { useTheme } from '@/theme/ThemeProvider'
import { setLanguage } from '@/shared/lib/i18n'
import { cn } from '@/shared/lib/cn'
import { UploadSnackbar } from '@/features/admin/media/UploadSnackbar'
export const Route = createRootRoute({ component: RootLayout })
function RootLayout() {
const { t, i18n } = useTranslation()
const { user } = useAuthStore()
const { theme, setTheme } = useTheme()
const [menuOpen, setMenuOpen] = useState(false)
// Админка — с таблицами; ей даём больше ширины, публичные страницы оставляем компактными.
const pathname = useRouterState({ select: (s) => s.location.pathname })
const containerMax = pathname.startsWith('/admin') ? 'max-w-screen-2xl' : 'max-w-5xl'
useEffect(() => {
void bootstrapSession()
}, [])
const handleLogout = async () => {
try {
await logout()
} finally {
clearSession()
}
}
return (
<div className="flex min-h-screen flex-col">
<header className="border-b border-border">
<div className={cn('mx-auto flex items-center justify-between gap-4 px-4 py-3', containerMax)}>
<Link to="/" className="crt-glow flex items-center gap-2 text-lg font-bold tracking-widest">
<Radio className="h-5 w-5" />
{t('appName')}
</Link>
<nav className="hidden items-center gap-4 text-sm md:flex">
{user && (
<>
<Link to="/dashboard" className="hover:text-primary" activeProps={{ className: 'text-primary' }}>
{t('nav.dashboard')}
</Link>
<Link to="/settings" className="hover:text-primary" activeProps={{ className: 'text-primary' }}>
{t('nav.settings')}
</Link>
{user.role === 'admin' && (
<Link to="/admin" className="hover:text-primary" activeProps={{ className: 'text-primary' }}>
{t('nav.admin')}
</Link>
)}
</>
)}
</nav>
<div className="flex items-center gap-2">
<select
className="rounded-sm border border-border bg-transparent px-2 py-1 text-xs uppercase"
value={i18n.language}
onChange={(e) => setLanguage(e.target.value)}
>
<option value="ru">{t('lang.ru')}</option>
<option value="en">{t('lang.en')}</option>
</select>
<select
className="hidden rounded-sm border border-border bg-transparent px-2 py-1 text-xs uppercase sm:block"
value={theme}
onChange={(e) => setTheme(e.target.value as typeof theme)}
>
<option value="light">{t('theme.light')}</option>
<option value="dark">{t('theme.dark')}</option>
<option value="system">{t('theme.system')}</option>
</select>
{user ? (
<button
className="hidden rounded-sm border border-border px-3 py-1 text-xs uppercase tracking-wide hover:bg-muted md:block"
onClick={() => void handleLogout()}
>
{t('nav.logout')}
</button>
) : (
<Link
to="/login"
className="hidden rounded-sm border border-border px-3 py-1 text-xs uppercase tracking-wide hover:bg-muted md:block"
>
{t('nav.login')}
</Link>
)}
<button
className="rounded-sm border border-border p-1.5 md:hidden"
onClick={() => setMenuOpen((v) => !v)}
aria-label="Menu"
>
{menuOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
</button>
</div>
</div>
{menuOpen && (
<nav className="flex flex-col gap-1 border-t border-border px-4 py-3 text-sm md:hidden">
{user ? (
<>
<Link to="/dashboard" onClick={() => setMenuOpen(false)}>
{t('nav.dashboard')}
</Link>
<Link to="/settings" onClick={() => setMenuOpen(false)}>
{t('nav.settings')}
</Link>
{user.role === 'admin' && (
<Link to="/admin" onClick={() => setMenuOpen(false)}>
{t('nav.admin')}
</Link>
)}
<button className="py-1 text-left" onClick={() => void handleLogout()}>
{t('nav.logout')}
</button>
</>
) : (
<Link to="/login" onClick={() => setMenuOpen(false)}>
{t('nav.login')}
</Link>
)}
</nav>
)}
</header>
<main className={cn('mx-auto w-full flex-1 px-4 py-8', containerMax)}>
<Outlet />
</main>
<UploadSnackbar />
</div>
)
}