From ed1699503a29e09a8bb726cde4133b3aea3b56ad Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 19 Jul 2026 02:13:08 +0700 Subject: [PATCH] feat(sidebar): integrate NavUser component into SidebarFooter and clean up SiteHeader - Added NavUser component to the SidebarFooter for user information display. - Refactored SiteHeader by removing unused imports and components related to user account management. - Updated breadcrumbs logic to include a new path for admin apps. - Adjusted SidebarMenuItem formatting for improved readability. --- apps/web/src/components/app-sidebar.tsx | 10 +- .../web/src/components/layout/site-header.tsx | 61 +----- apps/web/src/components/nav-user.tsx | 200 ++++++++++++++++++ docs/ui-design-contract.md | 3 +- 4 files changed, 213 insertions(+), 61 deletions(-) create mode 100644 apps/web/src/components/nav-user.tsx diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx index 832d35e..9e48dd0 100644 --- a/apps/web/src/components/app-sidebar.tsx +++ b/apps/web/src/components/app-sidebar.tsx @@ -2,6 +2,7 @@ import { Link, useRouterState } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' import { LayoutGridIcon, UsersIcon, AppWindowIcon } from 'lucide-react' import { AppSwitcher } from '@/components/app-switcher' +import { NavUser } from '@/components/nav-user' import { meQueryOptions } from '@/queries/auth' import { Sidebar, @@ -57,7 +58,10 @@ export function AppSidebar() { } > @@ -79,7 +83,9 @@ export function AppSidebar() { ) : null} - + + + ) } diff --git a/apps/web/src/components/layout/site-header.tsx b/apps/web/src/components/layout/site-header.tsx index 3401010..799da4d 100644 --- a/apps/web/src/components/layout/site-header.tsx +++ b/apps/web/src/components/layout/site-header.tsx @@ -1,7 +1,5 @@ import { Fragment, useMemo } from 'react' import { Link, useRouterState } from '@tanstack/react-router' -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { LogOutIcon } from 'lucide-react' import { Breadcrumb, BreadcrumbItem, @@ -10,24 +8,14 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from '@authportal/ui/components/breadcrumb' -import { Button } from '@authportal/ui/components/button' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@authportal/ui/components/dropdown-menu' import { Separator } from '@authportal/ui/components/separator' import { SidebarTrigger } from '@authportal/ui/components/sidebar' -import { Avatar, AvatarFallback } from '@authportal/ui/components/avatar' import { AppsMenu } from '@/components/layout/apps-menu' -import { ModeToggle } from '@/components/mode-toggle' -import { clearToken } from '@/lib/auth' -import { logout, meQueryOptions } from '@/queries/auth' function breadcrumbs(pathname: string) { + if (pathname.startsWith('/admin/apps')) { + return [{ label: 'Ссылки приложений', href: '/admin/apps' }] + } if (pathname.startsWith('/admin/users/')) { return [ { label: 'Пользователи', href: '/admin' }, @@ -46,26 +34,6 @@ function breadcrumbs(pathname: string) { export function SiteHeader() { const pathname = useRouterState({ select: (s) => s.location.pathname }) const crumbs = useMemo(() => breadcrumbs(pathname), [pathname]) - const { data: me } = useQuery(meQueryOptions) - const queryClient = useQueryClient() - - async function handleLogout() { - try { - await logout() - } catch { - /* ignore */ - } - clearToken() - queryClient.clear() - window.location.href = '/' - } - - const initials = (me?.name ?? me?.email ?? '?') - .split(/\s+/) - .map((p) => p[0]) - .join('') - .slice(0, 2) - .toUpperCase() return (
@@ -95,29 +63,6 @@ export function SiteHeader() {
- - - } - > - - {initials} - - - - -
- {me?.name} - {me?.email} -
-
- - - - Выйти - -
-
) diff --git a/apps/web/src/components/nav-user.tsx b/apps/web/src/components/nav-user.tsx new file mode 100644 index 0000000..06e51f6 --- /dev/null +++ b/apps/web/src/components/nav-user.tsx @@ -0,0 +1,200 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { + ChevronsUpDownIcon, + LogOutIcon, + MonitorIcon, + MoonIcon, + PaletteIcon, + SunIcon, +} from 'lucide-react' +import { useEffect, useState } from 'react' +import { useTheme } from 'next-themes' + +import { Avatar, AvatarFallback } from '@authportal/ui/components/avatar' +import { Button } from '@authportal/ui/components/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@authportal/ui/components/dropdown-menu' +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from '@authportal/ui/components/sidebar' +import { cn } from '@authportal/ui/lib/utils' + +import { clearToken } from '@/lib/auth' +import { logout, meQueryOptions } from '@/queries/auth' + +const THEMES = [ + { + value: 'light', + label: 'Светлая', + icon: , + }, + { + value: 'dark', + label: 'Тёмная', + icon: , + }, + { + value: 'system', + label: 'Системная', + icon: , + }, +] as const + +function ThemeSegmentedToggle() { + const { theme, setTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + const currentTheme = mounted ? (theme ?? 'system') : 'system' + + return ( +
e.stopPropagation()} + onPointerDown={(e) => e.stopPropagation()} + > + {THEMES.map(({ value, label, icon }) => { + const isActive = currentTheme === value + return ( + + ) + })} +
+ ) +} + +function initials(name: string, email: string): string { + const base = name.trim() || email.trim() + if (!base) return '?' + const parts = base.split(/\s+/).filter(Boolean) + if (parts.length >= 2) { + return `${parts[0]![0] ?? ''}${parts[1]![0] ?? ''}`.toUpperCase() + } + return base.slice(0, 2).toUpperCase() +} + +export function NavUser() { + const { isMobile } = useSidebar() + const { data: me } = useQuery(meQueryOptions) + const queryClient = useQueryClient() + + const name = me?.name?.trim() || 'Пользователь' + const email = me?.email?.trim() || '' + const fallback = initials(name, email) + + async function handleSignOut() { + try { + await logout() + } catch { + /* ignore */ + } + clearToken() + queryClient.clear() + window.location.href = '/' + } + + return ( + + + + + } + > + + + {fallback} + + +
+ {name} + + {email || '—'} + +
+ +
+ + + + + + {fallback} + + +
+ {name} + + {email || '—'} + +
+
+
+ + + + + + + Тема +
+ +
+
+
+ + + + + void handleSignOut()}> + + Выйти + + +
+
+
+
+ ) +} diff --git a/docs/ui-design-contract.md b/docs/ui-design-contract.md index 5e19761..343bc47 100644 --- a/docs/ui-design-contract.md +++ b/docs/ui-design-contract.md @@ -16,7 +16,8 @@ Surface lock: **`frame`** (ReUI Frame). Не смешивать shadcn Card и F | Sidebar / hover colors | theme `--sidebar` / `--sidebar-accent` — **без** AppShell `color-mix` override | | Header | `h-12`, `sticky`, `border-b`, `px-4 md:px-6` | | Header left | `SidebarTrigger` + `Separator` + Breadcrumb | -| Header right | **AppsMenu** → **ModeToggle** (+ account menu; SystemMonitor — stub/нет) | +| Header right | **AppsMenu** (аккаунт — в sidebar footer `NavUser`) | +| Sidebar footer | **NavUser** — аватар, имя, email, тема, выход · [app-shell-12](https://reui.io/preview/base/app-shell-12) | | Sidebar | AppSwitcher → groups → icons `size-4` → **пустой** `SidebarFooter` | | `main` | `gap-4 md:gap-6`, `px-4 py-4 md:px-6 md:py-5` |