512 lines
15 KiB
TypeScript
512 lines
15 KiB
TypeScript
"use client";
|
||||
|
|
|
|||
|
|
import { motion } from "framer-motion";
|
|||
|
|
import {
|
|||
|
|
Activity,
|
|||
|
|
Calendar,
|
|||
|
|
Clapperboard,
|
|||
|
|
FileText,
|
|||
|
|
Handshake,
|
|||
|
|
Home,
|
|||
|
|
Images,
|
|||
|
|
Inbox,
|
|||
|
|
Kanban,
|
|||
|
|
LayoutDashboard,
|
|||
|
|
ListTodo,
|
|||
|
|
Receipt,
|
|||
|
|
Settings,
|
|||
|
|
Wallet,
|
|||
|
|
} from "lucide-react";
|
|||
|
|
import * as React from "react";
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
Drawer,
|
|||
|
|
DrawerContent,
|
|||
|
|
DrawerHeader,
|
|||
|
|
DrawerTitle,
|
|||
|
|
} from "@/components/ui/drawer";
|
|||
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|||
|
|
import { Separator } from "@/components/ui/separator";
|
|||
|
|
import {
|
|||
|
|
Sidebar,
|
|||
|
|
SidebarContent,
|
|||
|
|
SidebarFooter,
|
|||
|
|
SidebarGroup,
|
|||
|
|
SidebarGroupContent,
|
|||
|
|
SidebarGroupLabel,
|
|||
|
|
SidebarHeader,
|
|||
|
|
SidebarInset,
|
|||
|
|
SidebarMenu,
|
|||
|
|
SidebarMenuBadge,
|
|||
|
|
SidebarMenuButton,
|
|||
|
|
SidebarMenuItem,
|
|||
|
|
SidebarProvider,
|
|||
|
|
SidebarTrigger,
|
|||
|
|
useSidebar,
|
|||
|
|
} from "@/components/ui/sidebar";
|
|||
|
|
import { cn } from "@/lib/utils";
|
|||
|
|
|
|||
|
|
type ModuleId =
|
|||
|
|
| "overview"
|
|||
|
|
| "sales"
|
|||
|
|
| "production"
|
|||
|
|
| "deliverables"
|
|||
|
|
| "finance";
|
|||
|
|
|
|||
|
|
type NavItem = {
|
|||
|
|
label: string;
|
|||
|
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|||
|
|
href: string;
|
|||
|
|
badge?: string;
|
|||
|
|
active?: boolean;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type NavSection = {
|
|||
|
|
title?: string;
|
|||
|
|
items: NavItem[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
type NavModule = {
|
|||
|
|
id: ModuleId;
|
|||
|
|
label: string;
|
|||
|
|
icon: NavItem["icon"];
|
|||
|
|
sections: NavSection[];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const navModules: NavModule[] = [
|
|||
|
|
{
|
|||
|
|
id: "overview",
|
|||
|
|
label: "نمای کلی",
|
|||
|
|
icon: LayoutDashboard,
|
|||
|
|
sections: [
|
|||
|
|
{
|
|||
|
|
title: "نمای کلی",
|
|||
|
|
items: [
|
|||
|
|
{ label: "خانه", icon: Home, href: "#", active: true },
|
|||
|
|
{ label: "داشبورد", icon: Activity, href: "#" },
|
|||
|
|
{ label: "صندوق ورودی", icon: Inbox, href: "#", badge: "۳" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "وظایف",
|
|||
|
|
items: [
|
|||
|
|
{ label: "وظایف من", icon: ListTodo, href: "#", badge: "۵" },
|
|||
|
|
{ label: "تقویم", icon: Calendar, href: "#" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "sales",
|
|||
|
|
label: "فروش",
|
|||
|
|
icon: Handshake,
|
|||
|
|
sections: [
|
|||
|
|
{
|
|||
|
|
title: "قیف فروش",
|
|||
|
|
items: [
|
|||
|
|
{ label: "سرنخهای جدید", icon: Inbox, href: "#", active: true },
|
|||
|
|
{ label: "پیشفاکتورها", icon: FileText, href: "#" },
|
|||
|
|
{ label: "رزروها", icon: Calendar, href: "#" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "گزارشها",
|
|||
|
|
items: [{ label: "داشبورد فروش", icon: Activity, href: "#" }],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "production",
|
|||
|
|
label: "تولید",
|
|||
|
|
icon: Clapperboard,
|
|||
|
|
sections: [
|
|||
|
|
{
|
|||
|
|
title: "مسیر تولید",
|
|||
|
|
items: [
|
|||
|
|
{ label: "تخته فیلمبرداری", icon: Kanban, href: "#", active: true },
|
|||
|
|
{ label: "لیست شاتها", icon: ListTodo, href: "#" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "تیم",
|
|||
|
|
items: [{ label: "تخصیصها", icon: Inbox, href: "#" }],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "deliverables",
|
|||
|
|
label: "تحویلها",
|
|||
|
|
icon: Images,
|
|||
|
|
sections: [
|
|||
|
|
{
|
|||
|
|
title: "گالریها",
|
|||
|
|
items: [
|
|||
|
|
{ label: "همه گالریها", icon: Images, href: "#", active: true },
|
|||
|
|
{ label: "تأییدها", icon: Inbox, href: "#", badge: "۲" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "خروجیها",
|
|||
|
|
items: [{ label: "مرکز دانلود", icon: FileText, href: "#" }],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: "finance",
|
|||
|
|
label: "مالی",
|
|||
|
|
icon: Wallet,
|
|||
|
|
sections: [
|
|||
|
|
{
|
|||
|
|
title: "صورتحساب",
|
|||
|
|
items: [
|
|||
|
|
{ label: "فاکتورها", icon: Receipt, href: "#", active: true },
|
|||
|
|
{ label: "پرداختها", icon: Wallet, href: "#" },
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: "گزارشها",
|
|||
|
|
items: [{ label: "سود و زیان", icon: Activity, href: "#" }],
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const utilities: NavItem[] = [{ label: "تنظیمات", icon: Settings, href: "#" }];
|
|||
|
|
|
|||
|
|
interface ModulePanelProps {
|
|||
|
|
className?: string;
|
|||
|
|
module: NavModule;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function ModulePanel({ className, module }: ModulePanelProps) {
|
|||
|
|
return (
|
|||
|
|
<div className={cn("space-y-4 px-4 py-3 text-start text-sm", className)}>
|
|||
|
|
{module.sections.map((section, index) => (
|
|||
|
|
<div className="space-y-2" key={section.title ?? index}>
|
|||
|
|
{section.title && (
|
|||
|
|
<SidebarGroupLabel className="mb-1 text-xs font-medium tracking-wide text-muted-foreground">
|
|||
|
|
{section.title}
|
|||
|
|
</SidebarGroupLabel>
|
|||
|
|
)}
|
|||
|
|
<SidebarMenu>
|
|||
|
|
{section.items.map((item) => {
|
|||
|
|
const Icon = item.icon;
|
|||
|
|
return (
|
|||
|
|
<SidebarMenuItem
|
|||
|
|
key={item.label}
|
|||
|
|
className="text-muted-foreground hover:text-foreground"
|
|||
|
|
>
|
|||
|
|
<SidebarMenuButton
|
|||
|
|
asChild
|
|||
|
|
isActive={item.active}
|
|||
|
|
className="px-2"
|
|||
|
|
>
|
|||
|
|
<a href={item.href}>
|
|||
|
|
<Icon className="size-4" />
|
|||
|
|
{item.badge && (
|
|||
|
|
<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
|
|||
|
|
)}
|
|||
|
|
<span>{item.label}</span>
|
|||
|
|
</a>
|
|||
|
|
</SidebarMenuButton>
|
|||
|
|
</SidebarMenuItem>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</SidebarMenu>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
<div className="border-t pt-3">
|
|||
|
|
<SidebarGroupLabel className="mb-2 text-xs font-medium tracking-wide text-muted-foreground">
|
|||
|
|
استودیو
|
|||
|
|
</SidebarGroupLabel>
|
|||
|
|
<SidebarMenu className="text-sm">
|
|||
|
|
{utilities.map((item) => {
|
|||
|
|
const Icon = item.icon;
|
|||
|
|
return (
|
|||
|
|
<SidebarMenuItem key={item.label}>
|
|||
|
|
<SidebarMenuButton
|
|||
|
|
asChild
|
|||
|
|
className="gap-2.5 px-2 text-muted-foreground hover:text-foreground"
|
|||
|
|
>
|
|||
|
|
<a href={item.href}>
|
|||
|
|
<Icon className="size-4" />
|
|||
|
|
<span>{item.label}</span>
|
|||
|
|
</a>
|
|||
|
|
</SidebarMenuButton>
|
|||
|
|
</SidebarMenuItem>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</SidebarMenu>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface AppSidebarProps extends Omit<
|
|||
|
|
React.ComponentProps<typeof Sidebar>,
|
|||
|
|
"className"
|
|||
|
|
> {
|
|||
|
|
className?: string;
|
|||
|
|
activeKey: ModuleId;
|
|||
|
|
onNavChange: (key: ModuleId) => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function AppSidebar({
|
|||
|
|
className,
|
|||
|
|
activeKey,
|
|||
|
|
onNavChange,
|
|||
|
|
...props
|
|||
|
|
}: AppSidebarProps) {
|
|||
|
|
const { state, setOpen, isMobile } = useSidebar();
|
|||
|
|
const isCollapsed = state === "collapsed";
|
|||
|
|
const showFooter = isCollapsed || isMobile;
|
|||
|
|
|
|||
|
|
const activeModule =
|
|||
|
|
navModules.find((module) => module.id === activeKey) ?? navModules[0];
|
|||
|
|
const activeNavLabel = activeModule.label;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Sidebar
|
|||
|
|
side="right"
|
|||
|
|
collapsible="icon"
|
|||
|
|
className={cn(
|
|||
|
|
"overflow-hidden *:data-[sidebar=sidebar]:flex-row",
|
|||
|
|
className,
|
|||
|
|
)}
|
|||
|
|
{...props}
|
|||
|
|
>
|
|||
|
|
<Sidebar
|
|||
|
|
collapsible="none"
|
|||
|
|
className="w-[calc(var(--sidebar-width-icon)+1px)]! border-e"
|
|||
|
|
>
|
|||
|
|
<SidebarHeader className="h-14 border-b">
|
|||
|
|
<SidebarMenu className="my-auto">
|
|||
|
|
<SidebarMenuItem>
|
|||
|
|
<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
|
|||
|
|
<a href="#">
|
|||
|
|
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-primary">
|
|||
|
|
<img
|
|||
|
|
src="/block-media/shadcnblocks/block/logos/ShadcnBlocks-logo.svg"
|
|||
|
|
alt="شادسیان بلاکس"
|
|||
|
|
className="size-6 text-primary-foreground invert dark:invert-0"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<span className="text-lg font-semibold tracking-tighter">
|
|||
|
|
ری یو آی
|
|||
|
|
</span>
|
|||
|
|
</a>
|
|||
|
|
</SidebarMenuButton>
|
|||
|
|
</SidebarMenuItem>
|
|||
|
|
</SidebarMenu>
|
|||
|
|
</SidebarHeader>
|
|||
|
|
|
|||
|
|
<SidebarContent>
|
|||
|
|
<SidebarGroup>
|
|||
|
|
<SidebarGroupContent className="px-1.5 md:px-0">
|
|||
|
|
<SidebarMenu>
|
|||
|
|
{" "}
|
|||
|
|
<SidebarMenuItem>
|
|||
|
|
<motion.div
|
|||
|
|
key={isCollapsed ? "h-collapsed" : "h-expanded"}
|
|||
|
|
className={cn(
|
|||
|
|
isCollapsed
|
|||
|
|
? "hidden items-center justify-center sm:flex"
|
|||
|
|
: "hidden",
|
|||
|
|
)}
|
|||
|
|
initial={{ opacity: 0 }}
|
|||
|
|
animate={{ opacity: 1 }}
|
|||
|
|
transition={{ duration: 0.8 }}
|
|||
|
|
>
|
|||
|
|
<SidebarTrigger />
|
|||
|
|
</motion.div>
|
|||
|
|
</SidebarMenuItem>
|
|||
|
|
{navModules.map((module) => {
|
|||
|
|
const Icon = module.icon;
|
|||
|
|
return (
|
|||
|
|
<SidebarMenuItem key={module.id}>
|
|||
|
|
<SidebarMenuButton
|
|||
|
|
tooltip={{
|
|||
|
|
children: module.label,
|
|||
|
|
hidden: isMobile,
|
|||
|
|
}}
|
|||
|
|
onClick={() => {
|
|||
|
|
onNavChange(module.id);
|
|||
|
|
setOpen(true);
|
|||
|
|
}}
|
|||
|
|
isActive={activeKey === module.id}
|
|||
|
|
aria-label={module.label}
|
|||
|
|
className="px-2.5 md:px-2"
|
|||
|
|
>
|
|||
|
|
<Icon className="size-4" />
|
|||
|
|
<span>{module.label}</span>
|
|||
|
|
</SidebarMenuButton>
|
|||
|
|
</SidebarMenuItem>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</SidebarMenu>
|
|||
|
|
</SidebarGroupContent>
|
|||
|
|
</SidebarGroup>
|
|||
|
|
</SidebarContent>
|
|||
|
|
|
|||
|
|
<SidebarFooter className={cn(showFooter ? "flex" : "hidden")}>
|
|||
|
|
<SidebarGroup className="p-0">
|
|||
|
|
<SidebarGroupContent className="px-1.5 md:px-0">
|
|||
|
|
<SidebarMenu>
|
|||
|
|
{utilities.map((item) => {
|
|||
|
|
const Icon = item.icon;
|
|||
|
|
return (
|
|||
|
|
<SidebarMenuItem key={item.label}>
|
|||
|
|
<SidebarMenuButton
|
|||
|
|
asChild
|
|||
|
|
tooltip={{
|
|||
|
|
children: item.label,
|
|||
|
|
hidden: isMobile,
|
|||
|
|
}}
|
|||
|
|
className="px-2.5 md:px-2"
|
|||
|
|
aria-label={item.label}
|
|||
|
|
>
|
|||
|
|
<a href={item.href}>
|
|||
|
|
<Icon className="size-4" />
|
|||
|
|
<span>{item.label}</span>
|
|||
|
|
</a>
|
|||
|
|
</SidebarMenuButton>
|
|||
|
|
</SidebarMenuItem>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</SidebarMenu>{" "}
|
|||
|
|
</SidebarGroupContent>
|
|||
|
|
</SidebarGroup>
|
|||
|
|
</SidebarFooter>
|
|||
|
|
</Sidebar>
|
|||
|
|
|
|||
|
|
<Sidebar collapsible="none" className="hidden flex-1 md:flex">
|
|||
|
|
<SidebarHeader className="h-14 gap-3.5 border-b px-4">
|
|||
|
|
<div className="my-auto flex w-full items-center justify-between">
|
|||
|
|
<div className="text-start text-base font-medium text-foreground">
|
|||
|
|
{activeNavLabel}
|
|||
|
|
</div>
|
|||
|
|
<div className="hidden sm:block">
|
|||
|
|
<SidebarTrigger />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</SidebarHeader>
|
|||
|
|
|
|||
|
|
<SidebarContent className="overflow-hidden">
|
|||
|
|
<ScrollArea className="min-h-0 flex-1">
|
|||
|
|
<SidebarGroup className="px-0">
|
|||
|
|
<SidebarGroupContent>
|
|||
|
|
<ModulePanel module={activeModule} />
|
|||
|
|
</SidebarGroupContent>
|
|||
|
|
</SidebarGroup>
|
|||
|
|
</ScrollArea>
|
|||
|
|
</SidebarContent>
|
|||
|
|
</Sidebar>
|
|||
|
|
</Sidebar>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface ApplicationShell6Props {
|
|||
|
|
className?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function ApplicationShell6({ className }: ApplicationShell6Props) {
|
|||
|
|
const [activeKey, setActiveKey] = React.useState<ModuleId>(navModules[0].id);
|
|||
|
|
const [isMobilePanelOpen, setIsMobilePanelOpen] = React.useState(false);
|
|||
|
|
|
|||
|
|
const activeModule =
|
|||
|
|
navModules.find((module) => module.id === activeKey) ?? navModules[0];
|
|||
|
|
const activeNavLabel = activeModule.label;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SidebarProvider
|
|||
|
|
dir="rtl"
|
|||
|
|
lang="fa"
|
|||
|
|
style={
|
|||
|
|
{
|
|||
|
|
"--sidebar-width": "350px",
|
|||
|
|
} as React.CSSProperties
|
|||
|
|
}
|
|||
|
|
className={cn(className)}
|
|||
|
|
>
|
|||
|
|
<div className="hidden md:block">
|
|||
|
|
<AppSidebar activeKey={activeKey} onNavChange={setActiveKey} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<SidebarInset className="pb-20 md:pb-0">
|
|||
|
|
<div className="flex flex-1 flex-col gap-4">
|
|||
|
|
<div className="sticky top-0 z-40 flex h-14 items-center gap-2 border-b bg-background px-4 md:hidden">
|
|||
|
|
<SidebarTrigger />
|
|||
|
|
<Separator
|
|||
|
|
orientation="vertical"
|
|||
|
|
className="ms-2 data-[orientation=vertical]:h-4"
|
|||
|
|
/>
|
|||
|
|
<a href="#">
|
|||
|
|
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-primary">
|
|||
|
|
<img
|
|||
|
|
src="/block-media/shadcnblocks/block/logos/ShadcnBlocks-logo.svg"
|
|||
|
|
alt="شادسیان بلاکس"
|
|||
|
|
className="size-6 text-primary-foreground invert dark:invert-0"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</a>
|
|||
|
|
<div className="ms-auto text-sm font-medium text-muted-foreground">
|
|||
|
|
{activeNavLabel}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex flex-1 flex-col gap-4 p-4">
|
|||
|
|
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</SidebarInset>
|
|||
|
|
|
|||
|
|
<Drawer
|
|||
|
|
open={isMobilePanelOpen}
|
|||
|
|
onOpenChange={setIsMobilePanelOpen}
|
|||
|
|
dismissible
|
|||
|
|
>
|
|||
|
|
<DrawerContent dir="rtl" className="text-start md:hidden">
|
|||
|
|
<DrawerHeader>
|
|||
|
|
<DrawerTitle className="text-start">{activeNavLabel}</DrawerTitle>
|
|||
|
|
</DrawerHeader>
|
|||
|
|
<div className="pb-6">
|
|||
|
|
<ModulePanel module={activeModule} />
|
|||
|
|
</div>
|
|||
|
|
</DrawerContent>
|
|||
|
|
</Drawer>
|
|||
|
|
|
|||
|
|
<nav
|
|||
|
|
aria-label="ناوبری پایین موبایل"
|
|||
|
|
className="fixed inset-x-0 bottom-0 z-40 border-t bg-background/95 backdrop-blur md:hidden"
|
|||
|
|
>
|
|||
|
|
<div className="grid grid-cols-5">
|
|||
|
|
{navModules.map((module) => {
|
|||
|
|
const Icon = module.icon;
|
|||
|
|
const isActive = module.id === activeKey;
|
|||
|
|
return (
|
|||
|
|
<button
|
|||
|
|
key={module.id}
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => {
|
|||
|
|
setActiveKey(module.id);
|
|||
|
|
setIsMobilePanelOpen(true);
|
|||
|
|
}}
|
|||
|
|
className={`flex flex-col items-center justify-center py-3 text-xs ${
|
|||
|
|
isActive
|
|||
|
|
? "text-foreground"
|
|||
|
|
: "text-muted-foreground hover:text-foreground"
|
|||
|
|
}`}
|
|||
|
|
aria-label={module.label}
|
|||
|
|
>
|
|||
|
|
<Icon className="h-5 w-5" />
|
|||
|
|
<span className="sr-only">{module.label}</span>
|
|||
|
|
</button>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</nav>
|
|||
|
|
</SidebarProvider>
|
|||
|
|
);
|
|||
|
|
}
|