This commit is contained in:
2026-04-07 06:32:40 +03:00
parent 24a44481a0
commit 11db1e6941
101 changed files with 54585 additions and 39160 deletions
@@ -1,11 +1,15 @@
"use client"
import { useCallback } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { ChevronRight, Circle } from "lucide-react"
import { usePathname, useRouter } from "next/navigation"
import { ChevronRight, Circle, LogOutIcon, UserIcon } from "lucide-react"
import type { NavGroup, NavItem } from "@/base/types/navigation"
import type { UserInfo } from "@/base/types/navigation"
import { useAuthStore } from "@/shared/stores/auth-store"
import { cn } from "@/shared/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar"
import {
Collapsible,
CollapsibleContent,
@@ -22,6 +26,7 @@ import {
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
@@ -38,11 +43,19 @@ import {
type AppSidebarProps = React.ComponentProps<typeof Sidebar> & {
navGroups: NavGroup[]
logo?: React.ReactNode
user?: UserInfo
}
export function AppSidebar({ navGroups, logo, ...props }: AppSidebarProps) {
export function AppSidebar({ navGroups, logo, user, ...props }: AppSidebarProps) {
const { state, isMobile } = useSidebar()
const isCollapsed = state === "collapsed" && !isMobile
const { logout } = useAuthStore((s) => s)
const router = useRouter()
const handleLogout = useCallback(async () => {
await logout()
router.push("/login")
}, [logout, router])
return (
<Sidebar collapsible="icon" {...props} className="bg-card">
@@ -71,6 +84,58 @@ export function AppSidebar({ navGroups, logo, ...props }: AppSidebarProps) {
</SidebarGroup>
))}
</SidebarContent>
<SidebarFooter className="p-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="size-8 rounded-lg">
{user?.avatar && <AvatarImage src={user.avatar as string} alt={user.name} />}
<AvatarFallback className="rounded-lg">
{user?.initials ?? user?.name?.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
{!isCollapsed && (
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user?.name}</span>
{user?.email && (
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
)}
</div>
)}
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
sideOffset={4}
className="w-56"
>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col">
<span className="text-sm font-medium">{user?.name}</span>
{user?.email && (
<span className="text-xs text-muted-foreground">{user.email}</span>
)}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/profile">
<UserIcon />
Profile
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onSelect={handleLogout}>
<LogOutIcon />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
@@ -1,22 +1,15 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useTheme } from "next-themes"
import {
BellIcon,
LogOutIcon,
MoonIcon,
SearchIcon,
SunIcon,
UserIcon,
} from "lucide-react"
import type { UserInfo } from "@/base/types/navigation"
import { useAuthStore } from "@/shared/stores/auth-store"
import { cn } from "@/shared/lib/utils"
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/components/ui/avatar"
import { Button } from "@/shared/components/ui/button"
import { SidebarTrigger } from "@/shared/components/ui/sidebar"
import {
@@ -28,33 +21,17 @@ import {
CommandGroup,
CommandItem,
} from "@/shared/components/ui/command"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import { Separator } from "@/shared/components/ui/separator"
export type DashboardHeaderProps = {
user?: UserInfo
actions?: React.ReactNode
className?: string
title?:string
}
export function DashboardHeader({ actions, className }: DashboardHeaderProps) {
export function DashboardHeader({ actions, className, title }: DashboardHeaderProps) {
const { resolvedTheme, setTheme } = useTheme()
const [searchOpen, setSearchOpen] = useState(false)
const { logout, user } = useAuthStore((s) => s)
const router = useRouter()
const handleLogout = useCallback(async () => {
await logout()
router.push("/login")
}, [logout, router])
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
@@ -78,75 +55,16 @@ export function DashboardHeader({ actions, className }: DashboardHeaderProps) {
className,
)}
>
{title && <h1 className="text-lg font-semibold">{title}</h1>}
{/* Sidebar toggle — mobile: hamburger, desktop: collapse */}
<SidebarTrigger className="-ms-2" />
<Separator orientation="vertical" />
<SidebarTrigger className="-ms-2 md:hidden" />
<Separator orientation="vertical" className="md:hidden" />
{/* Left side — default actions */}
<div className="flex items-center gap-1">
{/* User dropdown */}
{/* {user && ( */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-2 px-2">
<Avatar >
{user?.avatar && <AvatarImage src={user?.avatar as string} alt={user?.name} />}
<AvatarFallback>
{user?.initials ?? user?.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className="hidden text-sm font-medium md:inline-block">
{user?.name}
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{/* User info header */}
<DropdownMenuLabel className="font-normal">
<div className="flex items-center gap-3 py-1">
<Avatar size="lg">
{user?.avatar && <AvatarImage src={user?.avatar as string} alt={user?.name} />}
<AvatarFallback className="text-base">
{user?.initials ?? user?.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className="text-sm font-medium text-foreground">{user?.name}</span>
{user?.email && (
<span className="text-xs text-muted-foreground">{user?.email}</span>
)}
{user?.role && (
<span className="mt-0.5 text-xs font-medium text-primary">{user?.role}</span>
)}
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href="/profile">
<UserIcon />
Profile
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onSelect={handleLogout}>
<LogOutIcon />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* )} */}
{/* Search trigger */}
<Button
{/* <Button
variant="outline"
className="hidden h-8 w-56 justify-start gap-2 text-muted-foreground md:flex"
onClick={() => setSearchOpen(true)}
@@ -156,7 +74,7 @@ export function DashboardHeader({ actions, className }: DashboardHeaderProps) {
<kbd className="pointer-events-none ms-auto inline-flex h-5 select-none items-center gap-0.5 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
⌘K
</kbd>
</Button>
</Button> */}
{/* Mobile search icon */}
<Button
@@ -31,11 +31,11 @@ export function DashboardLayout({
return (
<TooltipProvider>
<SidebarProvider defaultOpen={defaultOpen}>
<AppSidebar navGroups={navGroups} logo={logo} />
<SidebarInset>
<div>
<AppSidebar navGroups={navGroups} logo={logo} user={user} />
<SidebarInset className="min-w-0">
{children}
</div>
</SidebarInset>
</SidebarProvider>
</TooltipProvider>
@@ -6,39 +6,25 @@ type DashboardPageProps = {
children: React.ReactNode
header?: React.ReactNode | null
headerProps?: DashboardHeaderProps
toolbar?: React.ReactNode
title?: string
fullscreen?: boolean
fullscreen?: boolean
}
export default function DashboardPage({ children, header, headerProps, title, fullscreen, toolbar }: DashboardPageProps) {
export default function DashboardPage({ children, header, headerProps, fullscreen, }: DashboardPageProps) {
const resolvedHeader = header !== undefined
? header
: <DashboardHeader {...headerProps} />
return (
<div className='page'>
<div className={cn("flex flex-col gap-4", fullscreen && "gap-0")}>
{resolvedHeader !== null && (
<header>
{resolvedHeader}
</header>
)}
<main className={cn('p-4 w-full h-full ', fullscreen && 'h-screen p-0 lg:p-0')}>
{(title || toolbar) && <div className='flex items-center justify-between gap-4 mb-4'>
{
title &&
<h2 className='text-lg lg:text-2xl font-bold '> {title}</h2>
}
{
toolbar &&
<div className=''>
{toolbar}
</div>
}
</div>}
{children}
<main >
<div className={cn(fullscreen ? "w-full px-0" : "p-4")}>
{children}
</div>
</main>
</div>
)