Files
reui-blocks/blocks/application-shell6.tsx
T

512 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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/NovaSales-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/NovaSales-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>
);
}