Enhance root layout and admin interface for better usability
- Updated the root layout to include a mobile menu toggle, improving navigation on smaller screens. - Refactored navigation links and language/theme selection for better organization and accessibility. - Added overflow handling for admin navigation and tables to ensure proper display on smaller screens. - Improved translations for menu toggle accessibility in both Russian and English.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link, Outlet, createRootRoute } from '@tanstack/react-router'
|
import { Link, Outlet, createRootRoute, useRouterState } from '@tanstack/react-router'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Menu, X } from 'lucide-react'
|
||||||
import { useTheme, type Theme } from '@/theme/ThemeProvider'
|
import { useTheme, type Theme } from '@/theme/ThemeProvider'
|
||||||
import { setLanguage } from '@/shared/lib/i18n'
|
import { setLanguage } from '@/shared/lib/i18n'
|
||||||
import { Toaster } from '@/shared/ui/toaster'
|
import { Toaster } from '@/shared/ui/toaster'
|
||||||
@@ -15,11 +16,18 @@ function RootLayout() {
|
|||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const { user, isBootstrapping } = useAuthStore()
|
const { user, isBootstrapping } = useAuthStore()
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void bootstrapSession()
|
void bootstrapSession()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Закрываем мобильное меню при переходе на другую страницу.
|
||||||
|
useEffect(() => {
|
||||||
|
setMobileMenuOpen(false)
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
const themes: Theme[] = ['light', 'dark', 'system']
|
const themes: Theme[] = ['light', 'dark', 'system']
|
||||||
const langs = ['ru', 'en']
|
const langs = ['ru', 'en']
|
||||||
|
|
||||||
@@ -31,14 +39,7 @@ function RootLayout() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const navLinks = !isBootstrapping && user && (
|
||||||
<div className="flex min-h-svh flex-col">
|
|
||||||
<header className="flex items-center justify-between border-b border-border px-6 py-4">
|
|
||||||
<Link to="/" className="text-lg font-semibold text-primary">
|
|
||||||
{t('appName')}
|
|
||||||
</Link>
|
|
||||||
<nav className="flex items-center gap-4 text-sm">
|
|
||||||
{!isBootstrapping && user && (
|
|
||||||
<>
|
<>
|
||||||
<Link to="/dashboard" className="text-muted-foreground hover:text-foreground">
|
<Link to="/dashboard" className="text-muted-foreground hover:text-foreground">
|
||||||
{t('nav.dashboard')}
|
{t('nav.dashboard')}
|
||||||
@@ -54,14 +55,17 @@ function RootLayout() {
|
|||||||
{t('nav.admin')}
|
{t('nav.admin')}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Button variant="ghost" size="sm" onClick={handleLogout}>
|
<Button variant="ghost" size="sm" className="justify-start" onClick={handleLogout}>
|
||||||
{t('nav.logout')}
|
{t('nav.logout')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
|
||||||
|
const selects = (
|
||||||
|
<>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
className="rounded-md border border-border bg-muted px-2 py-1"
|
className="w-full rounded-md border border-border bg-muted px-2 py-1 sm:w-auto"
|
||||||
value={i18n.language}
|
value={i18n.language}
|
||||||
onChange={(e) => setLanguage(e.target.value)}
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
>
|
>
|
||||||
@@ -74,7 +78,7 @@ function RootLayout() {
|
|||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
className="rounded-md border border-border bg-muted px-2 py-1"
|
className="w-full rounded-md border border-border bg-muted px-2 py-1 sm:w-auto"
|
||||||
value={theme}
|
value={theme}
|
||||||
onChange={(e) => setTheme(e.target.value as Theme)}
|
onChange={(e) => setTheme(e.target.value as Theme)}
|
||||||
>
|
>
|
||||||
@@ -85,7 +89,38 @@ function RootLayout() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-svh flex-col">
|
||||||
|
<header className="border-b border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Link to="/" className="text-lg font-semibold text-primary">
|
||||||
|
{t('appName')}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav className="hidden items-center gap-4 text-sm sm:flex">
|
||||||
|
{navLinks}
|
||||||
|
{selects}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-foreground sm:hidden"
|
||||||
|
onClick={() => setMobileMenuOpen((v) => !v)}
|
||||||
|
aria-label={t('nav.toggleMenu')}
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<nav className="mt-3 flex flex-col items-stretch gap-3 text-sm sm:hidden">
|
||||||
|
{navLinks}
|
||||||
|
{selects}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<TelegramLinkWarningBanner />
|
<TelegramLinkWarningBanner />
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ function AdminLayout() {
|
|||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">{t('nav.admin')}</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">{t('nav.admin')}</h1>
|
||||||
<nav className="flex gap-1 border-b border-border">
|
<nav className="flex gap-1 overflow-x-auto border-b border-border">
|
||||||
{TABS.map((tab) => (
|
{TABS.map((tab) => (
|
||||||
<Link
|
<Link
|
||||||
key={tab.to}
|
key={tab.to}
|
||||||
to={tab.to}
|
to={tab.to}
|
||||||
activeOptions={{ exact: tab.to === '/admin' }}
|
activeOptions={{ exact: tab.to === '/admin' }}
|
||||||
className={cn('px-3 py-2 text-sm text-muted-foreground hover:text-foreground')}
|
className={cn('shrink-0 whitespace-nowrap px-3 py-2 text-sm text-muted-foreground hover:text-foreground')}
|
||||||
activeProps={{ className: 'border-b-2 border-primary text-foreground font-medium' }}
|
activeProps={{ className: 'border-b-2 border-primary text-foreground font-medium' }}
|
||||||
>
|
>
|
||||||
{t(`admin.tabs.${tab.key}`)}
|
{t(`admin.tabs.${tab.key}`)}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function AdminAuditPage() {
|
|||||||
|
|
||||||
{data && data.items.length > 0 && (
|
{data && data.items.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border text-muted-foreground">
|
<tr className="border-b border-border text-muted-foreground">
|
||||||
@@ -60,6 +61,7 @@ function AdminAuditPage() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-muted-foreground">{t('admin.users.total', { count: data.total })}</span>
|
<span className="text-muted-foreground">{t('admin.users.total', { count: data.total })}</span>
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ function AdminRolesPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{data && (
|
{data && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border text-muted-foreground">
|
<tr className="border-b border-border text-muted-foreground">
|
||||||
@@ -84,6 +85,7 @@ function AdminRolesPage() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{editing && <RoleFormDialog role={editing} open={!!editing} onOpenChange={(open) => !open && setEditing(null)} />}
|
{editing && <RoleFormDialog role={editing} open={!!editing} onOpenChange={(open) => !open && setEditing(null)} />}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ function AdminUsersPage() {
|
|||||||
|
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border text-muted-foreground">
|
<tr className="border-b border-border text-muted-foreground">
|
||||||
@@ -81,6 +82,7 @@ function AdminUsersPage() {
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
{data.items.length === 0 && <p className="text-sm text-muted-foreground">{t('admin.users.empty')}</p>}
|
{data.items.length === 0 && <p className="text-sm text-muted-foreground">{t('admin.users.empty')}</p>}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const resources = {
|
|||||||
settings: 'Настройки',
|
settings: 'Настройки',
|
||||||
admin: 'Админка',
|
admin: 'Админка',
|
||||||
logout: 'Выйти',
|
logout: 'Выйти',
|
||||||
|
toggleMenu: 'Меню',
|
||||||
},
|
},
|
||||||
|
|
||||||
activation: {
|
activation: {
|
||||||
@@ -315,6 +316,7 @@ const resources = {
|
|||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
admin: 'Admin',
|
admin: 'Admin',
|
||||||
logout: 'Log out',
|
logout: 'Log out',
|
||||||
|
toggleMenu: 'Menu',
|
||||||
},
|
},
|
||||||
|
|
||||||
activation: {
|
activation: {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function DialogContent({ className, children, ...props }: DialogPrimitive
|
|||||||
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/60" />
|
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/60" />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-background p-6 shadow-lg',
|
'fixed left-1/2 top-1/2 z-50 w-[calc(100vw-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-background p-6 shadow-lg',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in New Issue
Block a user