1075 lines
33 KiB
TypeScript
1075 lines
33 KiB
TypeScript
"use client";
|
||
|
||
import { motion } from "framer-motion";
|
||
import {
|
||
BadgeCheck,
|
||
Bell,
|
||
Bookmark,
|
||
ChevronDown,
|
||
ClipboardCheck,
|
||
Clock,
|
||
CreditCard,
|
||
FileText,
|
||
Folder,
|
||
Hash,
|
||
Inbox,
|
||
Layers,
|
||
LayoutGrid,
|
||
LogOut,
|
||
MessageSquare,
|
||
MessageSquareText,
|
||
MessagesSquare,
|
||
PhoneCall,
|
||
Pin,
|
||
Settings,
|
||
Sparkles,
|
||
UserPlus,
|
||
Users,
|
||
} from "lucide-react";
|
||
import * as React from "react";
|
||
|
||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
Breadcrumb,
|
||
BreadcrumbItem,
|
||
BreadcrumbLink,
|
||
BreadcrumbList,
|
||
BreadcrumbPage,
|
||
BreadcrumbSeparator,
|
||
} from "@/components/ui/breadcrumb";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Drawer,
|
||
DrawerContent,
|
||
DrawerHeader,
|
||
DrawerTitle,
|
||
} from "@/components/ui/drawer";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuGroup,
|
||
DropdownMenuItem,
|
||
DropdownMenuLabel,
|
||
DropdownMenuPortal,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuShortcut,
|
||
DropdownMenuSub,
|
||
DropdownMenuSubContent,
|
||
DropdownMenuSubTrigger,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||
import {
|
||
Sheet,
|
||
SheetContent,
|
||
SheetHeader,
|
||
SheetTitle,
|
||
SheetTrigger,
|
||
} from "@/components/ui/sheet";
|
||
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 = "messages" | "spaces" | "threads" | "calls" | "files";
|
||
|
||
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[];
|
||
};
|
||
|
||
function getItemKey(moduleId: ModuleId, item: NavItem) {
|
||
return `${moduleId}:${item.label}`;
|
||
}
|
||
|
||
function getDefaultItemKey(module: NavModule) {
|
||
const items = module.sections.flatMap((section) => section.items);
|
||
const activeItem = items.find((item) => item.active) ?? items[0];
|
||
return activeItem ? getItemKey(module.id, activeItem) : `${module.id}:`;
|
||
}
|
||
|
||
function getActiveItemLabel(module: NavModule, activeItemKey: string) {
|
||
const items = module.sections.flatMap((section) => section.items);
|
||
const match = items.find(
|
||
(item) => getItemKey(module.id, item) === activeItemKey,
|
||
);
|
||
return match?.label ?? module.sections[0]?.items[0]?.label ?? module.label;
|
||
}
|
||
|
||
function getInitials(name: string) {
|
||
return (
|
||
name
|
||
.split(" ")
|
||
.filter(Boolean)
|
||
.slice(0, 2)
|
||
.map((part) => part[0]?.toUpperCase())
|
||
.join("") || "U"
|
||
);
|
||
}
|
||
|
||
const navModules: NavModule[] = [
|
||
{
|
||
id: "messages",
|
||
label: "پیامها",
|
||
icon: MessagesSquare,
|
||
sections: [
|
||
{
|
||
title: "صندوق ورودی",
|
||
items: [
|
||
{ label: "همه پیامها", icon: Inbox, href: "#", active: true },
|
||
{ label: "ارجاعها", icon: UserPlus, href: "#", badge: "۴" },
|
||
{ label: "اعلانها", icon: Bell, href: "#", badge: "۱۲" },
|
||
{ label: "ذخیرهشده", icon: Bookmark, href: "#" },
|
||
],
|
||
},
|
||
{
|
||
title: "نماها",
|
||
items: [
|
||
{ label: "خواندهنشده", icon: Bell, href: "#" },
|
||
{ label: "زمانبندیشده", icon: Clock, href: "#" },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "spaces",
|
||
label: "فضاها",
|
||
icon: LayoutGrid,
|
||
sections: [
|
||
{
|
||
title: "فضاها",
|
||
items: [
|
||
{ label: "بهروزرسانی محصول", icon: Hash, href: "#", active: true },
|
||
{ label: "موفقیت مشتری", icon: Users, href: "#", badge: "۲" },
|
||
{ label: "عملیات انتشار", icon: Layers, href: "#" },
|
||
],
|
||
},
|
||
{
|
||
title: "مستقیم",
|
||
items: [
|
||
{ label: "Kai", icon: Users, href: "#", badge: "۱" },
|
||
{ label: "Mina", icon: Users, href: "#" },
|
||
{ label: "Diego", icon: Users, href: "#" },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "threads",
|
||
label: "گفتگوها",
|
||
icon: MessageSquareText,
|
||
sections: [
|
||
{
|
||
title: "گفتگوها",
|
||
items: [
|
||
{ label: "دنبالشده", icon: Bookmark, href: "#", active: true },
|
||
{ label: "محولشده", icon: Users, href: "#" },
|
||
{ label: "بستهشده", icon: MessageSquareText, href: "#" },
|
||
],
|
||
},
|
||
{
|
||
title: "برجستهها",
|
||
items: [
|
||
{ label: "ارجاعها", icon: UserPlus, href: "#", badge: "۳" },
|
||
{ label: "سنجاقشده", icon: Bookmark, href: "#" },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "calls",
|
||
label: "تماسها",
|
||
icon: PhoneCall,
|
||
sections: [
|
||
{
|
||
title: "جلسات",
|
||
items: [
|
||
{ label: "پیشرو", icon: Clock, href: "#", active: true },
|
||
{ label: "ضبطها", icon: FileText, href: "#" },
|
||
],
|
||
},
|
||
{
|
||
title: "دستگاهها",
|
||
items: [
|
||
{ label: "صدا و تصویر", icon: PhoneCall, href: "#" },
|
||
{ label: "اعلانها", icon: Bell, href: "#" },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "files",
|
||
label: "فایلها",
|
||
icon: Folder,
|
||
sections: [
|
||
{
|
||
title: "مرور",
|
||
items: [
|
||
{ label: "اخیر", icon: Clock, href: "#", active: true },
|
||
{ label: "اشتراکی با من", icon: Users, href: "#" },
|
||
{ label: "بارگذاریها", icon: Folder, href: "#" },
|
||
{ label: "مجموعهها", icon: Layers, href: "#" },
|
||
],
|
||
},
|
||
{
|
||
title: "فیلترهای سریع",
|
||
items: [
|
||
{ label: "اسناد", icon: FileText, href: "#" },
|
||
{ label: "نظرات خواندهنشده", icon: Bell, href: "#", badge: "۵" },
|
||
],
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
const utilities: NavItem[] = [
|
||
{ label: "اعلانها", icon: Bell, href: "#" },
|
||
{ label: "تنظیمات", icon: Settings, href: "#" },
|
||
];
|
||
|
||
const data = {
|
||
user: {
|
||
name: "علی رضایی",
|
||
email: "ali@example.com",
|
||
avatar:
|
||
"/block-media/shadcnblocks/block/avatar/avatar3.webp",
|
||
},
|
||
participants: [
|
||
{
|
||
id: "you",
|
||
name: "علی رضایی",
|
||
title: "شما",
|
||
status: "online",
|
||
},
|
||
{
|
||
id: "riley",
|
||
name: "ریحانه Park",
|
||
title: "محصول",
|
||
status: "online",
|
||
},
|
||
{
|
||
id: "sam",
|
||
name: "Sam Rivera",
|
||
title: "طراحی",
|
||
status: "away",
|
||
},
|
||
{
|
||
id: "noah",
|
||
name: "Noah Patel",
|
||
title: "مهندسی",
|
||
status: "offline",
|
||
},
|
||
{
|
||
id: "maya",
|
||
name: "نگار Singh",
|
||
title: "پشتیبانی",
|
||
status: "online",
|
||
},
|
||
{
|
||
id: "jules",
|
||
name: "Jules Tan",
|
||
title: "عملیات",
|
||
status: "offline",
|
||
},
|
||
] as const,
|
||
activity: [
|
||
{
|
||
id: "a1",
|
||
icon: MessageSquare,
|
||
title: "گفتگوی جدید شروع شد",
|
||
detail: "Riley در بهروزرسانی محصول پست گذاشت.",
|
||
time: "۲ دقیقه",
|
||
},
|
||
{
|
||
id: "a2",
|
||
icon: UserPlus,
|
||
title: "ارجاع جدید",
|
||
detail: "سام شما را به تیکت پشتیبانی مشتری ارجاع داد.",
|
||
time: "۱۸ دقیقه",
|
||
},
|
||
{
|
||
id: "a3",
|
||
icon: Pin,
|
||
title: "پیام سنجاق شد",
|
||
detail: "Maya «چکلیست انتشار نسخه ۳» را سنجاق کرد.",
|
||
time: "۱ ساعت",
|
||
},
|
||
{
|
||
id: "a4",
|
||
icon: ClipboardCheck,
|
||
title: "وظیفه تکمیل شد",
|
||
detail: "Noah «پاکسازی API» را انجامشده علامت زد.",
|
||
time: "امروز",
|
||
},
|
||
{
|
||
id: "a5",
|
||
icon: PhoneCall,
|
||
title: "تماس پایان یافت",
|
||
detail: "همگامسازی هفتگی • ۲۴ دقیقه",
|
||
time: "دیروز",
|
||
},
|
||
{
|
||
id: "a6",
|
||
icon: FileText,
|
||
title: "فایل بارگذاری شد",
|
||
detail: "Jules «incident-notes.pdf» را به اشتراک گذاشت.",
|
||
time: "دیروز",
|
||
},
|
||
] as const,
|
||
};
|
||
|
||
interface ShellBreadcrumbProps {
|
||
className?: string;
|
||
moduleLabel: string;
|
||
itemLabel: string;
|
||
}
|
||
|
||
function ShellBreadcrumb({
|
||
className,
|
||
moduleLabel,
|
||
itemLabel,
|
||
}: ShellBreadcrumbProps) {
|
||
return (
|
||
<Breadcrumb className={cn(className)}>
|
||
<BreadcrumbList>
|
||
<BreadcrumbItem className="hidden md:block">
|
||
<BreadcrumbLink href="#">{moduleLabel}</BreadcrumbLink>
|
||
</BreadcrumbItem>
|
||
<BreadcrumbSeparator className="hidden md:block" />
|
||
<BreadcrumbItem>
|
||
<BreadcrumbPage>{itemLabel}</BreadcrumbPage>
|
||
</BreadcrumbItem>
|
||
</BreadcrumbList>
|
||
</Breadcrumb>
|
||
);
|
||
}
|
||
|
||
interface ModulePanelProps {
|
||
className?: string;
|
||
module: NavModule;
|
||
activeItemKey: string;
|
||
onSelectItem: (key: string) => void;
|
||
}
|
||
|
||
function ModulePanel({
|
||
className,
|
||
module,
|
||
activeItemKey,
|
||
onSelectItem,
|
||
}: 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;
|
||
const itemKey = getItemKey(module.id, item);
|
||
return (
|
||
<SidebarMenuItem
|
||
key={item.label}
|
||
className="text-muted-foreground hover:text-foreground"
|
||
>
|
||
<SidebarMenuButton
|
||
asChild
|
||
isActive={itemKey === activeItemKey}
|
||
className="px-2"
|
||
>
|
||
<a
|
||
href={item.href}
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
onSelectItem(itemKey);
|
||
}}
|
||
>
|
||
<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;
|
||
activeItemKey: string;
|
||
onNavChange: (key: ModuleId) => void;
|
||
onItemChange: (key: string) => void;
|
||
}
|
||
|
||
function AppSidebar({
|
||
className,
|
||
activeKey,
|
||
activeItemKey,
|
||
onNavChange,
|
||
onItemChange,
|
||
...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];
|
||
|
||
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={cn(
|
||
"flex h-14 border-b px-2",
|
||
isCollapsed
|
||
? "flex-row items-center justify-between gap-y-4 md:flex-col md:items-center md:justify-start"
|
||
: "flex-row items-center justify-between",
|
||
)}
|
||
>
|
||
<SidebarMenu>
|
||
<SidebarMenuItem>
|
||
<DropdownMenu>
|
||
<div className="flex w-full items-center px-2 group-data-[collapsible=icon]:justify-center">
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
className="flex h-auto items-center gap-1.5 p-0! hover:bg-transparent"
|
||
>
|
||
<span className="font-semibold">ری یو آی</span>
|
||
<ChevronDown className="size-3 text-muted-foreground" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
</div>
|
||
<DropdownMenuContent
|
||
className="min-w-60 rounded-lg"
|
||
side="bottom"
|
||
align="center"
|
||
>
|
||
<DropdownMenuGroup>
|
||
<DropdownMenuItem asChild>
|
||
<a href="#">
|
||
تنظیمات فضای کار
|
||
<DropdownMenuShortcut>⌥⇧S</DropdownMenuShortcut>
|
||
</a>
|
||
</DropdownMenuItem>
|
||
<DropdownMenuSub>
|
||
<DropdownMenuSubTrigger>
|
||
دعوت همتیمیها
|
||
</DropdownMenuSubTrigger>
|
||
<DropdownMenuPortal>
|
||
<DropdownMenuSubContent className="min-w-64 rounded-lg">
|
||
<DropdownMenuLabel>دعوت به Pulse</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem className="flex items-center gap-2">
|
||
<div className="flex aspect-square size-6 items-center justify-center rounded bg-blue-500 text-[10px] font-semibold text-white">
|
||
EM
|
||
</div>
|
||
دعوت با ایمیل
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem className="flex items-center gap-2">
|
||
<div className="flex aspect-square size-6 items-center justify-center rounded bg-emerald-500 text-[10px] font-semibold text-white">
|
||
LK
|
||
</div>
|
||
کپی لینک دعوت
|
||
<DropdownMenuShortcut>⌘C</DropdownMenuShortcut>
|
||
</DropdownMenuItem>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem>دعوتهای در انتظار</DropdownMenuItem>
|
||
<DropdownMenuItem>نقشها و دسترسی</DropdownMenuItem>
|
||
</DropdownMenuSubContent>
|
||
</DropdownMenuPortal>
|
||
</DropdownMenuSub>
|
||
</DropdownMenuGroup>
|
||
<DropdownMenuSeparator />
|
||
|
||
<DropdownMenuItem>
|
||
خروج
|
||
<DropdownMenuShortcut>⌥⇧Q</DropdownMenuShortcut>
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</SidebarMenuItem>
|
||
</SidebarMenu>
|
||
<motion.div
|
||
key={isCollapsed ? "h-collapsed" : "h-expanded"}
|
||
className={cn(
|
||
"flex items-center gap-2",
|
||
isCollapsed ? "flex-row md:flex-col-reverse" : "flex-row",
|
||
)}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ duration: 0.8 }}
|
||
>
|
||
<SidebarTrigger />
|
||
</motion.div>
|
||
</SidebarHeader>
|
||
|
||
<SidebarContent className="overflow-hidden">
|
||
<ScrollArea className="min-h-0 flex-1">
|
||
<SidebarGroup className="px-0">
|
||
<SidebarGroupContent>
|
||
<ModulePanel
|
||
module={activeModule}
|
||
activeItemKey={activeItemKey}
|
||
onSelectItem={onItemChange}
|
||
/>
|
||
</SidebarGroupContent>
|
||
</SidebarGroup>
|
||
</ScrollArea>
|
||
</SidebarContent>
|
||
</Sidebar>
|
||
</Sidebar>
|
||
);
|
||
}
|
||
|
||
interface NavUserProps {
|
||
className?: string;
|
||
user: {
|
||
name: string;
|
||
email: string;
|
||
avatar: string;
|
||
};
|
||
}
|
||
|
||
function NavUser({ className, user }: NavUserProps) {
|
||
const { isMobile } = useSidebar();
|
||
const initials = getInitials(user.name);
|
||
return (
|
||
<SidebarMenu className={cn("w-full", className)}>
|
||
<SidebarMenuItem className="w-full">
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<SidebarMenuButton
|
||
size="default"
|
||
className="h-10 w-full data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||
>
|
||
<Avatar className="h-7 w-7">
|
||
<AvatarImage src={user.avatar} alt={user.name} />
|
||
<AvatarFallback className="bg-sidebar-primary text-xs font-medium text-sidebar-primary-foreground">
|
||
{initials}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<span className="min-w-0 flex-1 truncate text-sm font-medium">
|
||
{user.name}
|
||
</span>
|
||
<ChevronDown className="ms-auto size-4 text-muted-foreground" />
|
||
</SidebarMenuButton>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent
|
||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||
side={isMobile ? "bottom" : "left"}
|
||
align="start"
|
||
sideOffset={4}
|
||
>
|
||
<DropdownMenuLabel className="p-0 font-normal">
|
||
<div className="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
|
||
<Avatar className="h-8 w-8">
|
||
<AvatarImage src={user.avatar} alt={user.name} />
|
||
<AvatarFallback className="bg-sidebar-primary font-medium text-sidebar-primary-foreground">
|
||
{initials}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div className="grid flex-1 text-start text-sm leading-tight">
|
||
<span className="truncate font-medium">{user.name}</span>
|
||
<span className="truncate text-xs text-muted-foreground">
|
||
{user.email}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuGroup>
|
||
<DropdownMenuItem>
|
||
<Sparkles className="me-2 size-4" />
|
||
ارتقا به حرفهای
|
||
</DropdownMenuItem>
|
||
</DropdownMenuGroup>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuGroup>
|
||
<DropdownMenuItem>
|
||
<BadgeCheck className="me-2 size-4" />
|
||
حساب کاربری
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem>
|
||
<CreditCard className="me-2 size-4" />
|
||
صورتحساب
|
||
</DropdownMenuItem>
|
||
<DropdownMenuItem>
|
||
<Bell className="me-2 size-4" />
|
||
اعلانها
|
||
</DropdownMenuItem>
|
||
</DropdownMenuGroup>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem>
|
||
<LogOut className="me-2 size-4" />
|
||
خروج
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</SidebarMenuItem>
|
||
</SidebarMenu>
|
||
);
|
||
}
|
||
|
||
interface SidebarRightProps extends Omit<
|
||
React.ComponentProps<typeof Sidebar>,
|
||
"className"
|
||
> {
|
||
className?: string;
|
||
}
|
||
|
||
function RightPanelContent() {
|
||
return (
|
||
<div className="space-y-6 p-4 text-start">
|
||
<section className="space-y-3">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<SidebarGroupLabel className="text-xs font-medium tracking-wide text-muted-foreground">
|
||
شرکتکنندگان
|
||
</SidebarGroupLabel>
|
||
<Button variant="outline" size="sm" className="h-8 gap-2">
|
||
<UserPlus className="size-4" />
|
||
دعوت
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{data.participants.map((participant) => {
|
||
const statusClassName =
|
||
participant.status === "online"
|
||
? "bg-emerald-500"
|
||
: participant.status === "away"
|
||
? "bg-amber-500"
|
||
: "bg-muted-foreground/50";
|
||
const initials = getInitials(participant.name);
|
||
const isYou = participant.id === "you";
|
||
|
||
return (
|
||
<button
|
||
key={participant.id}
|
||
type="button"
|
||
className="flex w-full items-center gap-3 rounded-md px-2 py-1.5 text-start hover:bg-accent"
|
||
>
|
||
<span
|
||
className={cn(
|
||
"mt-0.5 size-2 shrink-0 rounded-full",
|
||
statusClassName,
|
||
)}
|
||
aria-hidden="true"
|
||
/>
|
||
<Avatar className="h-7 w-7">
|
||
<AvatarFallback className="bg-primary text-xs font-medium text-primary-foreground">
|
||
{initials}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="truncate text-sm font-medium">
|
||
{participant.name}
|
||
</span>
|
||
{isYou && (
|
||
<Badge
|
||
variant="secondary"
|
||
className="h-5 px-1.5 text-[10px]"
|
||
>
|
||
شما
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<div className="truncate text-xs text-muted-foreground">
|
||
{participant.title}
|
||
</div>
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<SidebarGroupLabel className="text-xs font-medium tracking-wide text-muted-foreground">
|
||
فعالیت تیمها
|
||
</SidebarGroupLabel>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-8 px-2 text-muted-foreground hover:text-foreground"
|
||
>
|
||
مشاهده همه
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
{data.activity.map((event) => {
|
||
const Icon = event.icon;
|
||
return (
|
||
<div
|
||
key={event.id}
|
||
className="flex gap-3 rounded-md p-2 hover:bg-accent/50"
|
||
>
|
||
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md bg-accent/50">
|
||
<Icon className="size-4 text-foreground" />
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex items-start justify-between gap-2">
|
||
<div className="truncate text-sm font-medium">
|
||
{event.title}
|
||
</div>
|
||
<div className="shrink-0 text-xs text-muted-foreground">
|
||
{event.time}
|
||
</div>
|
||
</div>
|
||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||
{event.detail}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function SidebarRight({ className, ...props }: SidebarRightProps) {
|
||
return (
|
||
<Sidebar
|
||
collapsible="none"
|
||
side="left"
|
||
className={cn("sticky top-0 hidden h-svh border-s lg:flex", className)}
|
||
{...props}
|
||
>
|
||
<SidebarHeader className="flex h-14 items-center border-b px-2">
|
||
<NavUser user={data.user} />
|
||
</SidebarHeader>
|
||
<SidebarContent className="p-0">
|
||
<ScrollArea className="h-[calc(100svh-3.5rem)]">
|
||
<RightPanelContent />
|
||
</ScrollArea>
|
||
</SidebarContent>
|
||
</Sidebar>
|
||
);
|
||
}
|
||
|
||
interface ApplicationShell7Props {
|
||
className?: string;
|
||
}
|
||
|
||
export function ApplicationShell7({ className }: ApplicationShell7Props) {
|
||
const [activeKey, setActiveKey] = React.useState<ModuleId>(navModules[0].id);
|
||
const [isMobilePanelOpen, setIsMobilePanelOpen] = React.useState(false);
|
||
const [activeItemKey, setActiveItemKey] = React.useState(() =>
|
||
getDefaultItemKey(navModules[0]),
|
||
);
|
||
|
||
const activeModule =
|
||
navModules.find((module) => module.id === activeKey) ?? navModules[0];
|
||
const activeItemLabel = getActiveItemLabel(activeModule, activeItemKey);
|
||
|
||
React.useEffect(() => {
|
||
setActiveItemKey(getDefaultItemKey(activeModule));
|
||
}, [activeModule]);
|
||
|
||
return (
|
||
<SidebarProvider
|
||
dir="rtl"
|
||
lang="fa"
|
||
style={
|
||
{
|
||
"--sidebar-width": "350px",
|
||
} as React.CSSProperties
|
||
}
|
||
className={cn(className)}
|
||
>
|
||
<div className="hidden md:block">
|
||
<AppSidebar
|
||
activeKey={activeKey}
|
||
activeItemKey={activeItemKey}
|
||
onNavChange={setActiveKey}
|
||
onItemChange={setActiveItemKey}
|
||
/>
|
||
</div>
|
||
|
||
<SidebarInset>
|
||
<header className="sticky top-0 flex h-14 shrink-0 items-center gap-2 border-b bg-background px-3">
|
||
<a href="#" className="md:hidden">
|
||
<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="flex flex-1 items-center gap-2">
|
||
<ShellBreadcrumb
|
||
moduleLabel={activeModule.label}
|
||
itemLabel={activeItemLabel}
|
||
/>
|
||
</div>
|
||
<Sheet>
|
||
<SheetTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="lg:hidden"
|
||
aria-label="تیم"
|
||
>
|
||
<Users className="size-5" />
|
||
</Button>
|
||
</SheetTrigger>
|
||
<SheetContent side="left" dir="rtl" className="w-80 p-0 text-start">
|
||
<SheetHeader className="px-4 pt-4">
|
||
<SheetTitle className="text-start">تیم</SheetTitle>
|
||
</SheetHeader>
|
||
<ScrollArea className="min-h-0 flex-1">
|
||
<RightPanelContent />
|
||
</ScrollArea>
|
||
</SheetContent>
|
||
</Sheet>
|
||
</header>
|
||
<div className="flex flex-1 flex-col gap-4 p-4">
|
||
<div className="h-[50vh] w-full rounded-xl bg-muted/50" />
|
||
<div className="h-[100vh] w-full rounded-xl bg-muted/50" />
|
||
</div>
|
||
</SidebarInset>
|
||
|
||
<SidebarRight />
|
||
|
||
<Drawer
|
||
open={isMobilePanelOpen}
|
||
onOpenChange={setIsMobilePanelOpen}
|
||
dismissible
|
||
>
|
||
<DrawerContent dir="rtl" className="text-start md:hidden">
|
||
<DrawerHeader>
|
||
<DrawerTitle className="text-start">{activeModule.label}</DrawerTitle>
|
||
</DrawerHeader>
|
||
<div className="pb-6">
|
||
<ModulePanel
|
||
module={activeModule}
|
||
activeItemKey={activeItemKey}
|
||
onSelectItem={(key) => {
|
||
setActiveItemKey(key);
|
||
setIsMobilePanelOpen(false);
|
||
}}
|
||
/>
|
||
</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>
|
||
);
|
||
}
|