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>
|
||
);
|
||
}
|