This commit is contained in:
Andreas Wilms
2025-09-08 18:30:35 +02:00
commit f12cc8b2ce
130 changed files with 16911 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
"use client";
import { Footer } from "@/components/admin-panel/footer";
import { Sidebar } from "@/components/admin-panel/sidebar";
import { useSidebar } from "@/hooks/use-sidebar";
import { useStore } from "@/hooks/use-store";
import { cn } from "@/lib/utils";
export default function AdminPanelLayout({
children
}: {
children: React.ReactNode;
}) {
const sidebar = useStore(useSidebar, (x) => x);
if (!sidebar) return null;
const { getOpenState, settings } = sidebar;
return (
<>
<Sidebar />
<main
className={cn(
"min-h-[calc(100vh_-_56px)] bg-zinc-50 dark:bg-zinc-900 transition-[margin-left] ease-in-out duration-300",
!settings.disabled
? getOpenState()
? "ml-0 lg:ml-72" // Sidebar geöffnet
: "ml-0 lg:ml-[90px]" // Sidebar minimiert
: "ml-0" // Sidebar deaktiviert
)}
>
{children}
</main>
<footer
className={cn(
"transition-[margin-left] ease-in-out duration-300",
!settings.disabled
? getOpenState()
? "ml-0 lg:ml-72"
: "ml-0 lg:ml-[90px]"
: "ml-0"
)}
>
<Footer />
</footer>
</>
);
}

View File

@@ -0,0 +1,189 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { ChevronDown, Dot, LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "@/components/ui/collapsible";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider
} from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuSeparator
} from "@/components/ui/dropdown-menu";
import { usePathname } from "next/navigation";
type Submenu = {
href: string;
label: string;
active?: boolean;
};
interface CollapseMenuButtonProps {
icon: LucideIcon;
label: string;
active: boolean;
submenus: Submenu[];
isOpen: boolean | undefined;
}
export function CollapseMenuButton({
icon: Icon,
label,
submenus,
isOpen
}: CollapseMenuButtonProps) {
const pathname = usePathname();
const isSubmenuActive = submenus.some((submenu) =>
submenu.active === undefined ? submenu.href === pathname : submenu.active
);
const [isCollapsed, setIsCollapsed] = useState<boolean>(isSubmenuActive);
return isOpen ? (
<Collapsible
open={isCollapsed}
onOpenChange={setIsCollapsed}
className="w-full"
>
<CollapsibleTrigger
className="[&[data-state=open]>div>div>svg]:rotate-180 mb-1"
asChild
>
<Button
variant={isSubmenuActive ? "secondary" : "ghost"}
className="w-full justify-start h-10"
>
<div className="w-full items-center flex justify-between">
<div className="flex items-center">
<span className="mr-4">
<Icon size={18} />
</span>
<p
className={cn(
"max-w-[150px] truncate",
isOpen
? "translate-x-0 opacity-100"
: "-translate-x-96 opacity-0"
)}
>
{label}
</p>
</div>
<div
className={cn(
"whitespace-nowrap",
isOpen
? "translate-x-0 opacity-100"
: "-translate-x-96 opacity-0"
)}
>
<ChevronDown
size={18}
className="transition-transform duration-200"
/>
</div>
</div>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
{submenus.map(({ href, label, active }, index) => (
<Button
key={index}
variant={
(active === undefined && pathname === href) || active
? "secondary"
: "ghost"
}
className="w-full justify-start h-10 mb-1"
asChild
>
<Link href={href}>
<span className="mr-4 ml-2">
<Dot size={18} />
</span>
<p
className={cn(
"max-w-[170px] truncate",
isOpen
? "translate-x-0 opacity-100"
: "-translate-x-96 opacity-0"
)}
>
{label}
</p>
</Link>
</Button>
))}
</CollapsibleContent>
</Collapsible>
) : (
<DropdownMenu>
<TooltipProvider disableHoverableContent>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant={isSubmenuActive ? "secondary" : "ghost"}
className="w-full justify-start h-10 mb-1"
>
<div className="w-full items-center flex justify-between">
<div className="flex items-center">
<span className={cn(isOpen === false ? "" : "mr-4")}>
<Icon size={18} />
</span>
<p
className={cn(
"max-w-[200px] truncate",
isOpen === false ? "opacity-0" : "opacity-100"
)}
>
{label}
</p>
</div>
</div>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="right" align="start" alignOffset={2}>
{label}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent side="right" sideOffset={25} align="start">
<DropdownMenuLabel className="max-w-[190px] truncate">
{label}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{submenus.map(({ href, label, active }, index) => (
<DropdownMenuItem key={index} asChild>
<Link
className={`cursor-pointer ${
((active === undefined && pathname === href) || active) &&
"bg-secondary"
}`}
href={href}
>
<p className="max-w-[180px] truncate">{label}</p>
</Link>
</DropdownMenuItem>
))}
<DropdownMenuArrow className="fill-border" />
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -0,0 +1,15 @@
import { Navbar } from "@/components/admin-panel/navbar";
interface ContentLayoutProps {
title: string;
children: React.ReactNode;
}
export function ContentLayout({ title, children }: ContentLayoutProps) {
return (
<div>
<Navbar title={title} />
<div className="container pt-8 pb-8 px-4 sm:px-8">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,12 @@
export function Footer() {
return (
<div className="z-20 w-full bg-background/95 shadow backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="mx-4 md:mx-8 flex h-14 items-center">
<p className="text-xs md:text-sm leading-loose text-muted-foreground text-left">
Built by{" "}
Andreas Wilms
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,155 @@
"use client";
import Link from "next/link";
import { Ellipsis, LogOut } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { getMenuList } from "@/lib/menu-list";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { CollapseMenuButton } from "@/components/admin-panel/collapse-menu-button";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "@/components/ui/tooltip";
import { logout } from "@/lib/session";
interface MenuProps {
isOpen: boolean | undefined;
}
export function Menu({ isOpen }: MenuProps) {
const pathname = usePathname();
const menuList = getMenuList(pathname);
const router = useRouter();
async function handleLogOut() {
await logout();
router.push("/login");
}
return (
<ScrollArea className="[&>div>div[style]]:!block">
<nav className="mt-8 h-full w-full">
<ul className="flex flex-col min-h-[calc(100vh-48px-36px-16px-32px)] lg:min-h-[calc(100vh-32px-40px-32px)] items-start space-y-1 px-2">
{menuList.map(({ groupLabel, menus }, index) => (
<li className={cn("w-full", groupLabel ? "pt-5" : "")} key={index}>
{(isOpen && groupLabel) || isOpen === undefined ? (
<p className="text-sm font-medium text-muted-foreground px-4 pb-2 max-w-[248px] truncate">
{groupLabel}
</p>
) : !isOpen && isOpen !== undefined && groupLabel ? (
<TooltipProvider>
<Tooltip delayDuration={100}>
<TooltipTrigger className="w-full">
<div className="w-full flex justify-center items-center">
<Ellipsis className="h-5 w-5" />
</div>
</TooltipTrigger>
<TooltipContent side="right">
<p>{groupLabel}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<p className="pb-2"></p>
)}
{menus.map(
({ href, label, icon: Icon, active, submenus }, index) =>
!submenus || submenus.length === 0 ? (
<div className="w-full" key={index}>
<TooltipProvider disableHoverableContent>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
variant={
(active === undefined &&
pathname.startsWith(href)) ||
active
? "secondary"
: "ghost"
}
className="w-full justify-start h-10 mb-1"
asChild
>
<Link href={href}>
<span
className={cn(isOpen === false ? "" : "mr-4")}
>
<Icon size={18} />
</span>
<p
className={cn(
"max-w-[200px] truncate",
isOpen === false
? "-translate-x-96 opacity-0"
: "translate-x-0 opacity-100"
)}
>
{label}
</p>
</Link>
</Button>
</TooltipTrigger>
{isOpen === false && (
<TooltipContent side="right">
{label}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
) : (
<div className="w-full" key={index}>
<CollapseMenuButton
icon={Icon}
label={label}
active={
active === undefined
? pathname.startsWith(href)
: active
}
submenus={submenus}
isOpen={isOpen}
/>
</div>
)
)}
</li>
))}
<li className="w-full grow flex items-end">
<TooltipProvider disableHoverableContent>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<Button
onClick={handleLogOut}
variant="outline"
className="w-full justify-center h-10 mt-5"
>
<span className={cn(isOpen === false ? "" : "mr-4")}>
<LogOut size={18} />
</span>
<p
className={cn(
"whitespace-nowrap",
isOpen === false ? "opacity-0 hidden" : "opacity-100"
)}
>
Abmelden
</p>
</Button>
</TooltipTrigger>
{isOpen === false && (
<TooltipContent side="right">Sign out</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</li>
</ul>
</nav>
</ScrollArea>
);
}

View File

@@ -0,0 +1,24 @@
import { ModeToggle } from "@/components/mode-toggle";
import { UserNav } from "@/components/admin-panel/user-nav";
import { SheetMenu } from "@/components/admin-panel/sheet-menu";
interface NavbarProps {
title: string;
}
export function Navbar({ title }: NavbarProps) {
return (
<header className="sticky top-0 z-10 w-full bg-background/95 shadow backdrop-blur supports-[backdrop-filter]:bg-background/60 dark:shadow-secondary">
<div className="mx-4 sm:mx-8 flex h-14 items-center">
<div className="flex items-center space-x-4 lg:space-x-0">
<SheetMenu />
<h1 className="font-bold">{title}</h1>
</div>
<div className="flex flex-1 items-center justify-end">
<ModeToggle />
<UserNav />
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,39 @@
import Link from "next/link";
import { MenuIcon, PanelsTopLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Menu } from "@/components/admin-panel/menu";
import {
Sheet,
SheetHeader,
SheetContent,
SheetTrigger,
SheetTitle
} from "@/components/ui/sheet";
export function SheetMenu() {
return (
<Sheet>
<SheetTrigger className="lg:hidden" asChild>
<Button className="h-8" variant="outline" size="icon">
<MenuIcon size={20} />
</Button>
</SheetTrigger>
<SheetContent className="sm:w-72 px-3 h-full flex flex-col" side="left">
<SheetHeader>
<Button
className="flex justify-center items-center pb-2 pt-1"
variant="link"
asChild
>
<Link href="/dashboard" className="flex items-center gap-2">
<PanelsTopLeft className="w-6 h-6 mr-1" />
<SheetTitle className="font-bold text-lg">Brand</SheetTitle>
</Link>
</Button>
</SheetHeader>
<Menu isOpen />
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,29 @@
import { ChevronLeft } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
interface SidebarToggleProps {
isOpen: boolean | undefined;
setIsOpen?: () => void;
}
export function SidebarToggle({ isOpen, setIsOpen }: SidebarToggleProps) {
return (
<div className="invisible lg:visible absolute top-[12px] -right-[16px] z-20">
<Button
onClick={() => setIsOpen?.()}
className="rounded-md w-8 h-8"
variant="outline"
size="icon"
>
<ChevronLeft
className={cn(
"h-4 w-4 transition-transform ease-in-out duration-700",
isOpen === false ? "rotate-180" : "rotate-0"
)}
/>
</Button>
</div>
);
}

View File

@@ -0,0 +1,55 @@
"use client";
import { Menu } from "@/components/admin-panel/menu";
import { SidebarToggle } from "@/components/admin-panel/sidebar-toggle";
import { Button } from "@/components/ui/button";
import { useSidebar } from "@/hooks/use-sidebar";
import { useStore } from "@/hooks/use-store";
import { cn } from "@/lib/utils";
import { PanelsTopLeft } from "lucide-react";
import Link from "next/link";
export function Sidebar() {
const sidebar = useStore(useSidebar, (x) => x);
if (!sidebar) return null;
const { isOpen, toggleOpen, getOpenState, setIsHover, settings } = sidebar;
return (
<aside
className={cn(
"fixed top-0 left-0 z-20 h-screen -translate-x-full lg:translate-x-0 transition-[width] ease-in-out duration-300",
!getOpenState() ? "w-[90px]" : "w-72",
settings.disabled && "hidden"
)}
>
<SidebarToggle isOpen={isOpen} setIsOpen={toggleOpen} />
<div
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
className="relative h-full flex flex-col px-3 py-4 overflow-y-auto shadow-md dark:shadow-zinc-800"
>
<Button
className={cn(
"transition-transform ease-in-out duration-300 mb-1",
!getOpenState() ? "translate-x-1" : "translate-x-0"
)}
variant="link"
asChild
>
<Link href="/" className="flex items-center gap-2">
<PanelsTopLeft className="w-6 h-6 mr-1" />
<h1
className={cn(
"font-bold text-lg whitespace-nowrap transition-[transform,opacity,display] ease-in-out duration-300",
!getOpenState()
? "-translate-x-96 opacity-0 hidden"
: "translate-x-0 opacity-100"
)}
>
ERP
</h1>
</Link>
</Button>
<Menu isOpen={getOpenState()} />
</div>
</aside>
);
}

View File

@@ -0,0 +1,78 @@
"use client";
import Link from "next/link";
import { LayoutGrid, LogOut, User } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function UserNav() {
return (
<DropdownMenu>
<TooltipProvider disableHoverableContent>
<Tooltip delayDuration={100}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="relative h-8 w-8 rounded-full"
>
<Avatar className="h-8 w-8">
<AvatarImage src="#" alt="Avatar" />
<AvatarFallback className="bg-transparent">JD</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">Profile</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">John Doe</p>
<p className="text-xs leading-none text-muted-foreground">
johndoe@example.com
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="hover:cursor-pointer" asChild>
<Link href="/dashboard" className="flex items-center">
<LayoutGrid className="w-4 h-4 mr-3 text-muted-foreground" />
Dashboard
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="hover:cursor-pointer" asChild>
<Link href="/account" className="flex items-center">
<User className="w-4 h-4 mr-3 text-muted-foreground" />
Account
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className="hover:cursor-pointer" onClick={() => {}}>
<LogOut className="w-4 h-4 mr-3 text-muted-foreground" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}