Initial commit: base slice (auth, roles, users, admin) scaffold

Backend: .NET 10 Clean Architecture + LiteCqrs.Net + EF Core/PostgreSQL +
Identity/JWT. Frontend: React 19 + Vite + TanStack Query/Router + Tailwind v4
with a retro CRT theme. Docker/compose deployment mirroring PnvPanel's
conventions, scoped down to the current base feature set.
This commit is contained in:
Leonid Pershin
2026-07-24 05:40:34 +03:00
commit 8a3eebc48f
156 changed files with 9335 additions and 0 deletions
+136
View File
@@ -0,0 +1,136 @@
import { useEffect, useState } from 'react'
import { createRootRoute, Link, Outlet } 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'
export const Route = createRootRoute({ component: RootLayout })
function RootLayout() {
const { t, i18n } = useTranslation()
const { user } = useAuthStore()
const { theme, setTheme } = useTheme()
const [menuOpen, setMenuOpen] = useState(false)
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="mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 py-3">
<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 max-w-5xl flex-1 px-4 py-8')}>
<Outlet />
</main>
</div>
)
}