1338 lines
45 KiB
TypeScript
1338 lines
45 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
Archive,
|
||
ArrowLeft,
|
||
Bug,
|
||
ChevronDown,
|
||
Clock,
|
||
CommandIcon,
|
||
File,
|
||
Inbox,
|
||
Lightbulb,
|
||
MailOpen,
|
||
MessageSquare,
|
||
MoreHorizontal,
|
||
PanelRight,
|
||
Plus,
|
||
Search,
|
||
Send,
|
||
Sparkles,
|
||
Trash2,
|
||
User,
|
||
UserCheck,
|
||
Users,
|
||
} from "lucide-react";
|
||
import * as React from "react";
|
||
|
||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
CommandDialog,
|
||
CommandEmpty,
|
||
CommandGroup,
|
||
CommandInput,
|
||
CommandItem,
|
||
CommandList,
|
||
CommandSeparator,
|
||
} from "@/components/ui/command";
|
||
import {
|
||
Drawer,
|
||
DrawerContent,
|
||
DrawerHeader,
|
||
DrawerTitle,
|
||
} from "@/components/ui/drawer";
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuGroup,
|
||
DropdownMenuItem,
|
||
DropdownMenuLabel,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||
import {
|
||
Sidebar,
|
||
SidebarContent,
|
||
SidebarGroup,
|
||
SidebarGroupContent,
|
||
SidebarGroupLabel,
|
||
SidebarHeader,
|
||
SidebarInset,
|
||
SidebarMenu,
|
||
SidebarMenuBadge,
|
||
SidebarMenuButton,
|
||
SidebarMenuItem,
|
||
SidebarProvider,
|
||
useSidebar,
|
||
} from "@/components/ui/sidebar";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
type NavItemId =
|
||
| "inbox"
|
||
| "unassigned"
|
||
| "assigned"
|
||
| "drafts"
|
||
| "archived"
|
||
| "spam";
|
||
|
||
type BucketId = "support" | "bugs" | "features" | "internal";
|
||
|
||
type TicketStatus = "active" | "pending" | "closed";
|
||
|
||
type NavItem = {
|
||
id: NavItemId;
|
||
label: string;
|
||
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||
count?: number;
|
||
};
|
||
|
||
type Bucket = {
|
||
id: BucketId;
|
||
label: string;
|
||
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||
};
|
||
|
||
type User = {
|
||
id: string;
|
||
name: string;
|
||
email: string;
|
||
avatar: string;
|
||
role?: string;
|
||
};
|
||
|
||
type Customer = {
|
||
id: string;
|
||
name: string;
|
||
email: string;
|
||
company: string;
|
||
role: string;
|
||
avatar: string;
|
||
isHighValue: boolean;
|
||
};
|
||
|
||
type Message = {
|
||
id: string;
|
||
sender: User | Customer;
|
||
content: string;
|
||
timestamp: string;
|
||
date: string;
|
||
isStaff: boolean;
|
||
};
|
||
|
||
type Ticket = {
|
||
id: string;
|
||
subject: string;
|
||
preview: string;
|
||
timestamp: string;
|
||
read: boolean;
|
||
customer: Customer;
|
||
assignee?: User;
|
||
status: TicketStatus;
|
||
messages: Message[];
|
||
respondingUser?: User;
|
||
};
|
||
|
||
type PreviousConversation = {
|
||
id: string;
|
||
subject: string;
|
||
timestamp: string;
|
||
};
|
||
|
||
const navItems: NavItem[] = [
|
||
{ id: "inbox", label: "صندوق ورودی", icon: Inbox, count: 6 },
|
||
{ id: "unassigned", label: "بدون مسئول", icon: MailOpen, count: 10 },
|
||
{ id: "assigned", label: "محولشده", icon: UserCheck, count: 3 },
|
||
{ id: "drafts", label: "پیشنویسها", icon: File, count: 1 },
|
||
{ id: "archived", label: "بایگانی", icon: Archive },
|
||
{ id: "spam", label: "هرزنامه", icon: Trash2 },
|
||
];
|
||
|
||
const buckets: Bucket[] = [
|
||
{ id: "support", label: "درخواستهای پشتیبانی", icon: MessageSquare },
|
||
{ id: "bugs", label: "گزارش باگ", icon: Bug },
|
||
{ id: "features", label: "درخواست ویژگی", icon: Lightbulb },
|
||
{ id: "internal", label: "داخلی", icon: Users },
|
||
];
|
||
|
||
const staffUsers: User[] = [
|
||
{
|
||
id: "peter",
|
||
name: "رضا کریمی",
|
||
email: "peter.lann@cloudstar.com",
|
||
avatar:
|
||
"/block-media/shadcnblocks/block/avatar/avatar3.webp",
|
||
role: "کارشناس پشتیبانی",
|
||
},
|
||
{
|
||
id: "alex",
|
||
name: "امیر چناری",
|
||
email: "alex.chen@cloudstar.com",
|
||
avatar:
|
||
"/block-media/shadcnblocks/block/avatar/avatar3.webp",
|
||
role: "کارشناس پشتیبانی",
|
||
},
|
||
];
|
||
|
||
const mockCustomers: Customer[] = [
|
||
{
|
||
id: "sarah",
|
||
name: "مریم Tran",
|
||
email: "sarah.tran@example.com",
|
||
company: "BrightWave Marketing",
|
||
role: "مدیر عملیات",
|
||
avatar:
|
||
"/block-media/shadcnblocks/block/avatar/avatar2.webp",
|
||
isHighValue: true,
|
||
},
|
||
{
|
||
id: "mike",
|
||
name: "محمد جعفری",
|
||
email: "mike.j@techcorp.io",
|
||
company: "TechCorp",
|
||
role: "توسعهدهنده",
|
||
avatar:
|
||
"/block-media/shadcnblocks/block/avatar/avatar5.webp",
|
||
isHighValue: false,
|
||
},
|
||
];
|
||
|
||
const mockTickets: Ticket[] = [
|
||
{
|
||
id: "1",
|
||
subject: "مشکل اتصال یکپارچهسازی Slack",
|
||
preview:
|
||
"تیم ما میخواهد Slack را به CloudStar وصل کند، اما فرایند احراز هویت با خطا مواجه میشود...",
|
||
timestamp: "۶ ساعت پیش",
|
||
read: false,
|
||
customer: mockCustomers[0],
|
||
assignee: staffUsers[0],
|
||
status: "active",
|
||
respondingUser: staffUsers[0],
|
||
messages: [
|
||
{
|
||
id: "m1",
|
||
sender: mockCustomers[0],
|
||
content: `سلام پشتیبانی CloudStar،
|
||
|
||
تیم ما میخواهد پیامرسان تیمی را به CloudStar وصل کند، اما فرایند احراز هویت با این خطا متوقف میشود: «احراز هویت token invalid».
|
||
|
||
چند بار دوباره وصل شدیم و حتی workspace را ریاستارت کردیم، اما نتیجهای نگرفتیم.
|
||
|
||
میتوانید کمک کنید این یکپارچهسازی کار کند؟
|
||
|
||
ممنون،
|
||
مریم Tran
|
||
مدیر عملیات، BrightWave Marketing`,
|
||
timestamp: "۸:۰۳",
|
||
date: "۲۹ مرداد",
|
||
isStaff: false,
|
||
},
|
||
{
|
||
id: "m2",
|
||
sender: staffUsers[0],
|
||
content: `سلام مریم،
|
||
|
||
ممنون از پیامتان خوشحال میشوم کمک کنم! این خطا معمولاً وقتی رخ میدهد که پیامرسان تیمی هنگام اتصال، مجوزهای لازم را به CloudStar نمیدهد. این موارد را امتحان کنید:
|
||
|
||
۱ قبل از شروع اتصال، در workspace درست پیامرسان تیمی وارد شده باشید.
|
||
۲ هنگام درخواست، همه مجوزها را به CloudStar بدهید (گاهی «Deny» اشتباهی زده میشود).
|
||
۳. یک پنجره ناشناس/خصوصی امتحان کنید تا credentialهای قدیمی مزاحم نباشند.
|
||
|
||
اگر جواب داد یا هنوز خطا میبینید، خبر دهید!`,
|
||
timestamp: "۱۲:۵۶",
|
||
date: "۲۹ مرداد",
|
||
isStaff: true,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: "2",
|
||
subject: "فایلهای گمشده در فضای کار مشترک",
|
||
preview:
|
||
"دیروز مجموعهای از فایلهای پروژه را در فضای کار مشترک بارگذاری کردم. امروز دو فایل دیگر نیست...",
|
||
timestamp: "۶ ساعت پیش",
|
||
read: false,
|
||
customer: mockCustomers[1],
|
||
status: "active",
|
||
respondingUser: staffUsers[1],
|
||
messages: [],
|
||
},
|
||
{
|
||
id: "3",
|
||
subject: "مغایرت در صورتحساب آخر",
|
||
preview:
|
||
"فاکتور این ماه ۱۰ لایسنس Pro نشان میدهد، در حالی که فقط ۸ کاربر فعال داریم. لطفاً بررسی کنید...",
|
||
timestamp: "۸ ساعت پیش",
|
||
read: true,
|
||
customer: mockCustomers[0],
|
||
status: "pending",
|
||
messages: [],
|
||
},
|
||
{
|
||
id: "4",
|
||
subject: "امکان بازنشانی رمز عبور نیست",
|
||
preview:
|
||
'با لینک «فراموشی رمز عبور» سعی کردم رمز CloudStar را بازنشانی کنم، اما ایمیل بازنشانی هرگز نرسید...',
|
||
timestamp: "۱۲ ساعت پیش",
|
||
read: true,
|
||
customer: mockCustomers[1],
|
||
status: "active",
|
||
messages: [],
|
||
},
|
||
{
|
||
id: "5",
|
||
subject: "آمار داشبورد بهروز نمیشود",
|
||
preview:
|
||
"داشبورد آمار از دیروز حدود ساعت ۱۵ بهروزرسانی نمیشود. همه نمودارها روی همان مقادیر ماندهاند...",
|
||
timestamp: "۱ روز پیش",
|
||
read: true,
|
||
customer: mockCustomers[0],
|
||
status: "closed",
|
||
messages: [],
|
||
},
|
||
{
|
||
id: "6",
|
||
subject: "درخواست مستندات انطباق HIPAA",
|
||
preview:
|
||
"قبل از ادامه فرایند onboarding، تیم حقوقی ما به مستندات CloudStar درباره HIPAA نیاز دارد...",
|
||
timestamp: "۲ هفته پیش",
|
||
read: true,
|
||
customer: mockCustomers[1],
|
||
status: "pending",
|
||
messages: [],
|
||
},
|
||
];
|
||
|
||
const previousConversations: PreviousConversation[] = [
|
||
{
|
||
id: "prev1",
|
||
subject: "مشکل اتصال یکپارچهسازی Slack",
|
||
timestamp: "۶ ساعت پیش",
|
||
},
|
||
{
|
||
id: "prev2",
|
||
subject: "انقضای توکن API",
|
||
timestamp: "۲ هفته پیش",
|
||
},
|
||
];
|
||
|
||
function getInitials(name: string) {
|
||
return (
|
||
name
|
||
.split(" ")
|
||
.filter(Boolean)
|
||
.slice(0, 2)
|
||
.map((part) => part[0]?.toUpperCase())
|
||
.join("") || "U"
|
||
);
|
||
}
|
||
|
||
function getStatusColor(status: TicketStatus) {
|
||
switch (status) {
|
||
case "active":
|
||
return "bg-emerald-500";
|
||
case "pending":
|
||
return "bg-amber-500";
|
||
case "closed":
|
||
return "bg-muted-foreground";
|
||
default:
|
||
return "bg-muted-foreground";
|
||
}
|
||
}
|
||
|
||
function getStatusLabel(status: TicketStatus) {
|
||
switch (status) {
|
||
case "active":
|
||
return "فعال";
|
||
case "pending":
|
||
return "در انتظار";
|
||
case "closed":
|
||
return "بسته";
|
||
default:
|
||
return status;
|
||
}
|
||
}
|
||
|
||
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
||
activeNavItem: NavItemId;
|
||
activeBucket: BucketId | null;
|
||
onNavItemChange: (id: NavItemId) => void;
|
||
onBucketChange: (id: BucketId | null) => void;
|
||
onSearchClick: () => void;
|
||
}
|
||
|
||
function AppSidebar({
|
||
activeNavItem,
|
||
activeBucket,
|
||
onNavItemChange,
|
||
onBucketChange,
|
||
onSearchClick,
|
||
className,
|
||
...props
|
||
}: AppSidebarProps) {
|
||
const { state } = useSidebar();
|
||
const isCollapsed = state === "collapsed";
|
||
return (
|
||
<Sidebar
|
||
side="right"
|
||
collapsible="offcanvas"
|
||
variant="inset"
|
||
className={cn(className)}
|
||
{...props}
|
||
>
|
||
<SidebarHeader
|
||
className={cn("flex h-14 flex-row items-center justify-between")}
|
||
>
|
||
<SidebarMenu>
|
||
<SidebarMenuItem>
|
||
<DropdownMenu>
|
||
<div className="flex w-full items-center group-data-[collapsible=icon]:justify-center">
|
||
<DropdownMenuTrigger asChild>
|
||
<Button
|
||
variant="ghost"
|
||
className="flex h-auto items-center gap-3 p-0! hover:bg-transparent"
|
||
>
|
||
<div className="flex size-8 items-center justify-center rounded-sm bg-primary">
|
||
<img
|
||
src="/block-media/shadcnblocks/block/logos/NovaSales-logo.svg"
|
||
alt="شادسیان بلاکس"
|
||
className="size-5 invert dark:invert-0"
|
||
/>
|
||
</div>
|
||
{!isCollapsed && (
|
||
<>
|
||
<span className="font-semibold">ری یو آی</span>
|
||
<ChevronDown className="size-3 text-muted-foreground" />
|
||
</>
|
||
)}
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
</div>
|
||
<DropdownMenuContent className="w-56" align="start">
|
||
<DropdownMenuLabel>فضای کار</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuGroup>
|
||
<DropdownMenuItem>تنظیمات فضای کار</DropdownMenuItem>
|
||
<DropdownMenuItem>دعوت همتیمیها</DropdownMenuItem>
|
||
<DropdownMenuItem>صورتحساب</DropdownMenuItem>
|
||
</DropdownMenuGroup>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem>خروج</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</SidebarMenuItem>
|
||
</SidebarMenu>
|
||
<Button variant="ghost" size="icon" className="size-7">
|
||
<MoreHorizontal className="size-4" />
|
||
</Button>
|
||
</SidebarHeader>
|
||
|
||
<SidebarContent>
|
||
<SidebarGroup className="py-2">
|
||
<SidebarGroupContent>
|
||
<SidebarMenu>
|
||
<SidebarMenuItem>
|
||
<SidebarMenuButton onClick={onSearchClick} className="px-2">
|
||
<Search className="size-4" />
|
||
<span>جستجو</span>
|
||
<kbd className="ms-auto hidden items-center justify-center rounded-md font-mono font-medium text-muted-foreground/70 sm:flex">
|
||
<CommandIcon
|
||
className="size-3 font-medium"
|
||
strokeWidth={1.5}
|
||
/>
|
||
<span>K</span>
|
||
</kbd>
|
||
</SidebarMenuButton>
|
||
</SidebarMenuItem>
|
||
</SidebarMenu>
|
||
</SidebarGroupContent>
|
||
</SidebarGroup>
|
||
|
||
<SidebarGroup className="py-0">
|
||
<SidebarGroupContent>
|
||
<SidebarMenu>
|
||
{navItems.map((item) => {
|
||
const Icon = item.icon;
|
||
const isActive =
|
||
activeNavItem === item.id && activeBucket === null;
|
||
return (
|
||
<SidebarMenuItem key={item.id}>
|
||
<SidebarMenuButton
|
||
onClick={() => {
|
||
onNavItemChange(item.id);
|
||
onBucketChange(null);
|
||
}}
|
||
isActive={isActive}
|
||
className="px-2"
|
||
>
|
||
<Icon className="size-4" />
|
||
<span>{item.label}</span>
|
||
{item.count !== undefined && (
|
||
<SidebarMenuBadge>{item.count}</SidebarMenuBadge>
|
||
)}
|
||
</SidebarMenuButton>
|
||
</SidebarMenuItem>
|
||
);
|
||
})}
|
||
</SidebarMenu>
|
||
</SidebarGroupContent>
|
||
</SidebarGroup>
|
||
|
||
<SidebarGroup>
|
||
<SidebarGroupLabel>دستهها</SidebarGroupLabel>
|
||
<SidebarGroupContent>
|
||
<SidebarMenu>
|
||
{buckets.map((bucket) => {
|
||
const Icon = bucket.icon;
|
||
return (
|
||
<SidebarMenuItem key={bucket.id}>
|
||
<SidebarMenuButton
|
||
onClick={() => onBucketChange(bucket.id)}
|
||
isActive={activeBucket === bucket.id}
|
||
className="px-2"
|
||
>
|
||
<Icon className="size-4" />
|
||
<span>{bucket.label}</span>
|
||
</SidebarMenuButton>
|
||
</SidebarMenuItem>
|
||
);
|
||
})}
|
||
</SidebarMenu>
|
||
</SidebarGroupContent>
|
||
</SidebarGroup>
|
||
</SidebarContent>
|
||
</Sidebar>
|
||
);
|
||
}
|
||
|
||
interface TicketListPanelProps {
|
||
tickets: Ticket[];
|
||
selectedTicketId: string | null;
|
||
onTicketSelect: (ticketId: string) => void;
|
||
activeNavItem: NavItemId;
|
||
}
|
||
|
||
function TicketListPanel({
|
||
tickets,
|
||
selectedTicketId,
|
||
onTicketSelect,
|
||
activeNavItem,
|
||
}: TicketListPanelProps) {
|
||
const navItem = navItems.find((item) => item.id === activeNavItem);
|
||
const title = navItem?.label ?? "صندوق ورودی";
|
||
|
||
return (
|
||
<div className="flex h-full w-1/4 max-w-[320px] min-w-[240px] shrink-0 flex-col overflow-hidden border-e bg-background">
|
||
<div className="flex h-14 shrink-0 items-center justify-between border-b px-4">
|
||
<div className="truncate text-start text-base font-medium text-foreground">
|
||
{title} شما
|
||
</div>
|
||
</div>
|
||
<ScrollArea className="min-h-0 flex-1 [&>[data-slot=scroll-area-viewport]>div]:!block">
|
||
{tickets.map((ticket) => {
|
||
const isSelected = selectedTicketId === ticket.id;
|
||
return (
|
||
<button
|
||
type="button"
|
||
key={ticket.id}
|
||
onClick={() => onTicketSelect(ticket.id)}
|
||
className={cn(
|
||
"w-full border-b p-4 text-start text-sm leading-tight last:border-b-0 hover:bg-muted/50",
|
||
!ticket.read && "bg-muted/30",
|
||
isSelected && "bg-muted",
|
||
)}
|
||
>
|
||
<div className="min-w-0">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span
|
||
className={cn(
|
||
"truncate text-sm",
|
||
!ticket.read && "font-semibold",
|
||
)}
|
||
>
|
||
{ticket.subject}
|
||
</span>
|
||
</div>
|
||
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
|
||
{ticket.preview}
|
||
</p>
|
||
{ticket.respondingUser && (
|
||
<div className="mt-2 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||
<Avatar className="size-4">
|
||
<AvatarImage
|
||
src={ticket.respondingUser.avatar}
|
||
alt={ticket.respondingUser.name}
|
||
/>
|
||
<AvatarFallback className="text-[8px]">
|
||
{getInitials(ticket.respondingUser.name)}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<span>
|
||
{ticket.respondingUser.name.split(" ")[0]} در حال
|
||
پاسخ است...
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</ScrollArea>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface MessageBubbleProps {
|
||
message: Message;
|
||
}
|
||
|
||
function MessageBubble({ message }: MessageBubbleProps) {
|
||
const sender = message.sender;
|
||
const isCustomer = "company" in sender;
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<div className="flex items-start gap-3">
|
||
<Avatar className="mt-0.5 size-10 shrink-0">
|
||
<AvatarImage src={sender.avatar} alt={sender.name} />
|
||
<AvatarFallback className="bg-primary text-xs font-medium text-primary-foreground">
|
||
{getInitials(sender.name)}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-medium">{sender.name}</span>
|
||
{!isCustomer && (
|
||
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
|
||
پشتیبانی
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
<span>از</span>
|
||
<span>{sender.email}</span>
|
||
</div>
|
||
<span className="text-xs text-muted-foreground">
|
||
{message.date}, {message.timestamp}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div className="ps-13 text-sm leading-relaxed whitespace-pre-wrap">
|
||
{message.content}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface ReplyComposerProps {
|
||
recipientEmail: string;
|
||
respondingUser?: User;
|
||
}
|
||
|
||
function ReplyComposer({ recipientEmail, respondingUser }: ReplyComposerProps) {
|
||
return (
|
||
<div className="shrink-0 border-t bg-background px-6 py-4 lg:px-10">
|
||
<div className="mx-auto max-w-3xl rounded-lg border bg-muted/30">
|
||
<div className="flex items-center gap-2 border-b px-3 py-2">
|
||
<ArrowLeft className="size-4 shrink-0 rotate-[135deg] text-muted-foreground" />
|
||
<span className="truncate text-sm text-muted-foreground">
|
||
{recipientEmail}
|
||
</span>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="ms-auto h-6 shrink-0 px-2 text-xs"
|
||
>
|
||
رونوشت
|
||
</Button>
|
||
</div>
|
||
<Textarea
|
||
placeholder="پاسخ خود را بنویسید..."
|
||
className="min-h-[80px] resize-none border-0 bg-transparent focus-visible:ring-0 md:min-h-[100px]"
|
||
/>
|
||
<div className="flex flex-wrap items-center justify-between gap-2 border-t px-3 py-2">
|
||
<div className="flex items-center gap-2">
|
||
<Button variant="ghost" size="icon" className="size-8 shrink-0">
|
||
<Plus className="size-4" />
|
||
</Button>
|
||
<span className="hidden text-xs text-muted-foreground sm:inline">
|
||
از <kbd className="rounded bg-muted px-1">/</kbd> برای میانبرها
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{respondingUser && (
|
||
<div className="hidden items-center gap-1.5 text-xs text-muted-foreground sm:flex">
|
||
<Avatar className="size-5">
|
||
<AvatarImage
|
||
src={respondingUser.avatar}
|
||
alt={respondingUser.name}
|
||
/>
|
||
<AvatarFallback className="text-[8px]">
|
||
{getInitials(respondingUser.name)}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<span>
|
||
{respondingUser.name.split(" ")[0]} در حال پاسخ است...
|
||
</span>
|
||
</div>
|
||
)}
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="outline" size="sm" className="gap-1">
|
||
بستن
|
||
<ChevronDown className="size-3" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="start">
|
||
<DropdownMenuItem>بستن بهعنوان حلشده</DropdownMenuItem>
|
||
<DropdownMenuItem>بستن بهعنوان هرزنامه</DropdownMenuItem>
|
||
<DropdownMenuItem>بستن بدون پاسخ</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
<Button size="sm" className="gap-1">
|
||
<Send className="size-3" />
|
||
ارسال
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface InboxAgentPanelProps {
|
||
ticket: Ticket;
|
||
previousConversations: PreviousConversation[];
|
||
}
|
||
|
||
function InboxAgentPanel({
|
||
ticket,
|
||
previousConversations,
|
||
}: InboxAgentPanelProps) {
|
||
const customer = ticket.customer;
|
||
|
||
return (
|
||
<div className="flex h-full w-1/4 max-w-[360px] min-w-[280px] shrink-0 flex-col overflow-hidden border-s bg-background">
|
||
<div className="flex h-14 shrink-0 items-center gap-2 border-b px-4">
|
||
<Sparkles className="size-4" />
|
||
<span className="font-semibold">دستیار صندوق ورودی</span>
|
||
</div>
|
||
<ScrollArea className="min-h-0 flex-1 [&>[data-slot=scroll-area-viewport]>div]:!block">
|
||
<div className="space-y-6 p-4 text-start">
|
||
<section className="space-y-3">
|
||
<div className="flex items-start gap-3">
|
||
<Avatar className="size-10 shrink-0">
|
||
<AvatarImage src={customer.avatar} alt={customer.name} />
|
||
<AvatarFallback className="bg-primary font-medium text-primary-foreground">
|
||
{getInitials(customer.name)}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="font-medium">{customer.name}</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
{customer.role}, {customer.company}
|
||
</div>
|
||
{customer.isHighValue && (
|
||
<Badge
|
||
variant="outline"
|
||
className="mt-1.5 border-amber-500 bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-400"
|
||
>
|
||
مشتری ارزشمند
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="space-y-3">
|
||
<div className="rounded-lg bg-muted/50 p-3 text-sm">
|
||
<p className="text-muted-foreground">
|
||
مشتری آرام به نظر میرسد، اما ممکن است ناراضی شود. احتمالاً
|
||
فنی نیست چون راهنمای یکپارچهسازی پیامرسان تیمی را نخوانده است.
|
||
</p>
|
||
<p className="mt-3 text-muted-foreground">
|
||
رضا با جزئیات لازم پاسخ داده، اما هنوز تأیید نکرده که مشکل
|
||
حل شده است یا نه.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="space-y-3">
|
||
<p className="text-sm text-muted-foreground">
|
||
اگر مشتری در ۱۲ ساعت آینده پاسخ ندهد، پیگیری ارسال شود:
|
||
</p>
|
||
<div className="rounded-lg border bg-muted/30 p-3 text-sm">
|
||
<p>
|
||
سلام مریم، پیگیری میکنم ببینم توانستید یکپارچهسازی را راه
|
||
بیندازید؟
|
||
</p>
|
||
<p className="mt-2">
|
||
اگر هنوز مشکل دارید، خوشحال میشوم یک تماس هماهنگ کنیم تا
|
||
سریعتر راه بیفتید.
|
||
</p>
|
||
<p className="mt-2">با احترام، رضا کریمی</p>
|
||
</div>
|
||
<Button variant="outline" className="w-full gap-2">
|
||
<Clock className="size-4" />
|
||
زمانبندی پیگیری
|
||
</Button>
|
||
</section>
|
||
|
||
<section className="space-y-3">
|
||
<p className="text-xs font-medium tracking-wide text-muted-foreground">
|
||
گفتگوهای قبلی
|
||
</p>
|
||
<div className="space-y-2">
|
||
{previousConversations.map((conv) => (
|
||
<button
|
||
key={conv.id}
|
||
type="button"
|
||
className="flex w-full items-start justify-between gap-2 rounded-md p-2 text-start hover:bg-muted"
|
||
>
|
||
<span className="line-clamp-2 text-sm">{conv.subject}</span>
|
||
<span className="shrink-0 text-xs text-muted-foreground">
|
||
{conv.timestamp}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</ScrollArea>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
interface TicketCommandPaletteProps {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
tickets: Ticket[];
|
||
onTicketSelect: (ticketId: string) => void;
|
||
onNavItemSelect: (navItemId: NavItemId) => void;
|
||
}
|
||
|
||
function TicketCommandPalette({
|
||
open,
|
||
onOpenChange,
|
||
tickets,
|
||
onTicketSelect,
|
||
onNavItemSelect,
|
||
}: TicketCommandPaletteProps) {
|
||
return (
|
||
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
||
<CommandInput placeholder="جستجوی تیکت، مشتری یا ناوبری..." />
|
||
<CommandList>
|
||
<CommandEmpty>نتیجهای یافت نشد.</CommandEmpty>
|
||
<CommandGroup heading="تیکتها">
|
||
{tickets.slice(0, 5).map((ticket) => (
|
||
<CommandItem
|
||
key={ticket.id}
|
||
onSelect={() => {
|
||
onTicketSelect(ticket.id);
|
||
onOpenChange(false);
|
||
}}
|
||
>
|
||
<MessageSquare className="me-2 size-4" />
|
||
<span className="truncate">{ticket.subject}</span>
|
||
<span className="ms-auto text-xs text-muted-foreground">
|
||
{ticket.timestamp}
|
||
</span>
|
||
</CommandItem>
|
||
))}
|
||
</CommandGroup>
|
||
<CommandSeparator />
|
||
<CommandGroup heading="مشتریان">
|
||
{mockCustomers.map((customer) => (
|
||
<CommandItem key={customer.id}>
|
||
<User className="me-2 size-4" />
|
||
<span>{customer.name}</span>
|
||
<span className="ms-2 text-xs text-muted-foreground">
|
||
{customer.company}
|
||
</span>
|
||
</CommandItem>
|
||
))}
|
||
</CommandGroup>
|
||
<CommandSeparator />
|
||
<CommandGroup heading="ناوبری">
|
||
{navItems.map((item) => {
|
||
const Icon = item.icon;
|
||
return (
|
||
<CommandItem
|
||
key={item.id}
|
||
onSelect={() => {
|
||
onNavItemSelect(item.id);
|
||
onOpenChange(false);
|
||
}}
|
||
>
|
||
<Icon className="me-2 size-4" />
|
||
<span>{item.label}</span>
|
||
{item.count !== undefined && (
|
||
<Badge variant="secondary" className="ms-auto">
|
||
{item.count}
|
||
</Badge>
|
||
)}
|
||
</CommandItem>
|
||
);
|
||
})}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</CommandDialog>
|
||
);
|
||
}
|
||
|
||
export function ApplicationShell10() {
|
||
const [activeNavItem, setActiveNavItem] = React.useState<NavItemId>("inbox");
|
||
const [activeBucket, setActiveBucket] = React.useState<BucketId | null>(null);
|
||
const [selectedTicketId, setSelectedTicketId] = React.useState<string | null>(
|
||
mockTickets[0]?.id ?? null,
|
||
);
|
||
const [tickets, setTickets] = React.useState<Ticket[]>(mockTickets);
|
||
const [isAgentPanelOpen, setIsAgentPanelOpen] = React.useState(true);
|
||
const [isCommandOpen, setIsCommandOpen] = React.useState(false);
|
||
const [isMobileTicketListOpen, setIsMobileTicketListOpen] =
|
||
React.useState(false);
|
||
const [isMobileConversationOpen, setIsMobileConversationOpen] =
|
||
React.useState(false);
|
||
const [isMobileAgentOpen, setIsMobileAgentOpen] = React.useState(false);
|
||
|
||
const selectedTicket = tickets.find((t) => t.id === selectedTicketId) ?? null;
|
||
|
||
// Keyboard shortcut for command palette
|
||
React.useEffect(() => {
|
||
const down = (e: KeyboardEvent) => {
|
||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||
e.preventDefault();
|
||
setIsCommandOpen((open) => !open);
|
||
}
|
||
};
|
||
document.addEventListener("keydown", down);
|
||
return () => document.removeEventListener("keydown", down);
|
||
}, []);
|
||
|
||
const handleTicketSelect = (ticketId: string) => {
|
||
setSelectedTicketId(ticketId);
|
||
setTickets((prev) =>
|
||
prev.map((t) => (t.id === ticketId ? { ...t, read: true } : t)),
|
||
);
|
||
if (window.innerWidth < 768) {
|
||
setIsMobileConversationOpen(true);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<SidebarProvider dir="rtl" lang="fa" className="h-svh overflow-hidden">
|
||
<AppSidebar
|
||
activeNavItem={activeNavItem}
|
||
activeBucket={activeBucket}
|
||
onNavItemChange={setActiveNavItem}
|
||
onBucketChange={setActiveBucket}
|
||
onSearchClick={() => setIsCommandOpen(true)}
|
||
className="hidden md:flex"
|
||
/>
|
||
|
||
<div className="flex h-full flex-col overflow-hidden pb-16 md:hidden">
|
||
<div className="flex h-14 shrink-0 items-center justify-between border-b px-4">
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex size-8 items-center justify-center rounded-sm bg-primary">
|
||
<img
|
||
src="/block-media/shadcnblocks/block/logos/NovaSales-logo.svg"
|
||
alt="شادسیان بلاکس"
|
||
className="size-5 invert dark:invert-0"
|
||
/>
|
||
</div>
|
||
<span className="font-semibold">
|
||
{navItems.find((item) => item.id === activeNavItem)?.label ??
|
||
"صندوق ورودی"}{" "}
|
||
شما
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<ScrollArea className="min-h-0 flex-1 [&>[data-slot=scroll-area-viewport]>div]:!block">
|
||
{tickets.map((ticket) => {
|
||
const isSelected = selectedTicketId === ticket.id;
|
||
return (
|
||
<button
|
||
type="button"
|
||
key={ticket.id}
|
||
onClick={() => handleTicketSelect(ticket.id)}
|
||
className={cn(
|
||
"w-full border-b p-4 text-start text-sm leading-tight last:border-b-0 hover:bg-muted/50",
|
||
!ticket.read && "bg-muted/30",
|
||
isSelected && "bg-muted",
|
||
)}
|
||
>
|
||
<div className="min-w-0">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span
|
||
className={cn(
|
||
"truncate text-sm",
|
||
!ticket.read && "font-semibold",
|
||
)}
|
||
>
|
||
{ticket.subject}
|
||
</span>
|
||
<span className="shrink-0 text-xs text-muted-foreground">
|
||
{ticket.timestamp}
|
||
</span>
|
||
</div>
|
||
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
|
||
{ticket.preview}
|
||
</p>
|
||
{ticket.respondingUser && (
|
||
<div className="mt-2 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||
<Avatar className="size-4">
|
||
<AvatarImage
|
||
src={ticket.respondingUser.avatar}
|
||
alt={ticket.respondingUser.name}
|
||
/>
|
||
<AvatarFallback className="text-[8px]">
|
||
{getInitials(ticket.respondingUser.name)}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<span>
|
||
{ticket.respondingUser.name.split(" ")[0]} در حال
|
||
پاسخ است...
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</ScrollArea>
|
||
</div>
|
||
|
||
<SidebarInset className="hidden min-h-0 overflow-hidden md:flex">
|
||
<div className="flex h-full w-full">
|
||
<TicketListPanel
|
||
tickets={tickets}
|
||
selectedTicketId={selectedTicketId}
|
||
onTicketSelect={handleTicketSelect}
|
||
activeNavItem={activeNavItem}
|
||
/>
|
||
|
||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||
{selectedTicket ? (
|
||
<>
|
||
<header className="flex h-14 shrink-0 items-center justify-between border-b bg-background px-4">
|
||
<div className="flex items-center gap-3">
|
||
<MessageSquare className="size-5" />
|
||
<span className="font-medium">
|
||
پاسخ: {selectedTicket.subject}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="outline" size="sm" className="gap-1.5">
|
||
<span
|
||
className={cn(
|
||
"size-2 rounded-full",
|
||
getStatusColor(selectedTicket.status),
|
||
)}
|
||
/>
|
||
<span>{getStatusLabel(selectedTicket.status)}</span>
|
||
<ChevronDown className="size-3" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent align="start">
|
||
<DropdownMenuItem>فعال</DropdownMenuItem>
|
||
<DropdownMenuItem>در انتظار</DropdownMenuItem>
|
||
<DropdownMenuItem>بسته</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="size-8"
|
||
onClick={() => setIsAgentPanelOpen(!isAgentPanelOpen)}
|
||
>
|
||
<PanelRight
|
||
className={cn(
|
||
"size-4",
|
||
isAgentPanelOpen && "text-primary",
|
||
)}
|
||
/>
|
||
</Button>
|
||
</div>
|
||
</header>
|
||
|
||
<ScrollArea className="min-h-0 flex-1">
|
||
<div className="px-6 py-6 lg:px-10">
|
||
<div className="mx-auto max-w-3xl space-y-8">
|
||
{selectedTicket.messages.map((message) => (
|
||
<MessageBubble key={message.id} message={message} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
</ScrollArea>
|
||
|
||
<ReplyComposer
|
||
recipientEmail={selectedTicket.customer.email}
|
||
respondingUser={selectedTicket.respondingUser}
|
||
/>
|
||
</>
|
||
) : (
|
||
<div className="flex flex-1 items-center justify-center text-muted-foreground">
|
||
<div className="text-center">
|
||
<MessageSquare className="mx-auto size-12 opacity-50" />
|
||
<p className="mt-2 text-sm">یک تیکت را برای مشاهده انتخاب کنید</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{isAgentPanelOpen && selectedTicket && (
|
||
<InboxAgentPanel
|
||
ticket={selectedTicket}
|
||
previousConversations={previousConversations}
|
||
/>
|
||
)}
|
||
</div>
|
||
</SidebarInset>
|
||
|
||
<TicketCommandPalette
|
||
open={isCommandOpen}
|
||
onOpenChange={setIsCommandOpen}
|
||
tickets={tickets}
|
||
onTicketSelect={handleTicketSelect}
|
||
onNavItemSelect={(navItemId) => {
|
||
setActiveNavItem(navItemId);
|
||
setActiveBucket(null);
|
||
}}
|
||
/>
|
||
|
||
<Drawer
|
||
open={isMobileTicketListOpen}
|
||
onOpenChange={setIsMobileTicketListOpen}
|
||
dismissible
|
||
>
|
||
<DrawerContent dir="rtl" className="h-[85vh] text-start md:hidden">
|
||
<DrawerHeader>
|
||
<DrawerTitle className="text-start">تیکتها</DrawerTitle>
|
||
</DrawerHeader>
|
||
<ScrollArea className="min-h-0 flex-1 px-4">
|
||
{tickets.map((ticket) => (
|
||
<button
|
||
type="button"
|
||
key={ticket.id}
|
||
onClick={() => {
|
||
handleTicketSelect(ticket.id);
|
||
setIsMobileTicketListOpen(false);
|
||
}}
|
||
className={cn(
|
||
"w-full border-b p-4 text-start text-sm",
|
||
!ticket.read && "bg-muted/30",
|
||
)}
|
||
>
|
||
<div className="min-w-0">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span
|
||
className={cn(
|
||
"truncate",
|
||
!ticket.read && "font-semibold",
|
||
)}
|
||
>
|
||
{ticket.subject}
|
||
</span>
|
||
<span className="shrink-0 text-xs text-muted-foreground">
|
||
{ticket.timestamp}
|
||
</span>
|
||
</div>
|
||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||
{ticket.preview}
|
||
</p>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</ScrollArea>
|
||
</DrawerContent>
|
||
</Drawer>
|
||
|
||
<Drawer
|
||
open={isMobileConversationOpen}
|
||
onOpenChange={setIsMobileConversationOpen}
|
||
dismissible
|
||
>
|
||
<DrawerContent
|
||
dir="rtl"
|
||
className="h-[90vh] overflow-hidden text-start md:hidden"
|
||
>
|
||
<DrawerHeader className="sr-only">
|
||
<DrawerTitle>گفتگو</DrawerTitle>
|
||
</DrawerHeader>
|
||
{selectedTicket && (
|
||
<div className="flex h-full min-h-0 flex-col overflow-hidden">
|
||
<div className="flex shrink-0 items-center justify-between border-b px-4 py-3">
|
||
<span className="line-clamp-1 font-medium">
|
||
{selectedTicket.subject}
|
||
</span>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="size-8 shrink-0"
|
||
onClick={() => setIsMobileAgentOpen(true)}
|
||
>
|
||
<Sparkles className="size-4" />
|
||
</Button>
|
||
</div>
|
||
<ScrollArea className="min-h-0 flex-1">
|
||
<div className="space-y-6 p-4">
|
||
{selectedTicket.messages.map((message) => (
|
||
<MessageBubble key={message.id} message={message} />
|
||
))}
|
||
</div>
|
||
</ScrollArea>
|
||
<div className="shrink-0">
|
||
<ReplyComposer
|
||
recipientEmail={selectedTicket.customer.email}
|
||
respondingUser={selectedTicket.respondingUser}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</DrawerContent>
|
||
</Drawer>
|
||
|
||
<Drawer
|
||
open={isMobileAgentOpen}
|
||
onOpenChange={setIsMobileAgentOpen}
|
||
dismissible
|
||
>
|
||
<DrawerContent dir="rtl" className="h-[85vh] text-start md:hidden">
|
||
<DrawerHeader>
|
||
<DrawerTitle className="flex items-center gap-2 text-start">
|
||
<Sparkles className="size-4" />
|
||
دستیار صندوق ورودی
|
||
</DrawerTitle>
|
||
</DrawerHeader>
|
||
{selectedTicket && (
|
||
<ScrollArea className="min-h-0 flex-1 px-4 pb-6">
|
||
<div className="space-y-6">
|
||
<section className="space-y-3">
|
||
<div className="flex items-start gap-3">
|
||
<Avatar className="size-10 shrink-0">
|
||
<AvatarImage
|
||
src={selectedTicket.customer.avatar}
|
||
alt={selectedTicket.customer.name}
|
||
/>
|
||
<AvatarFallback>
|
||
{getInitials(selectedTicket.customer.name)}
|
||
</AvatarFallback>
|
||
</Avatar>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="font-medium">
|
||
{selectedTicket.customer.name}
|
||
</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
{selectedTicket.customer.role},{" "}
|
||
{selectedTicket.customer.company}
|
||
</div>
|
||
{selectedTicket.customer.isHighValue && (
|
||
<Badge
|
||
variant="outline"
|
||
className="mt-1.5 border-amber-500 bg-amber-50 text-amber-700"
|
||
>
|
||
مشتری ارزشمند
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="rounded-lg bg-muted/50 p-3 text-sm text-muted-foreground">
|
||
<p>
|
||
مشتری آرام به نظر میرسد، اما ممکن است ناراضی شود.
|
||
</p>
|
||
</section>
|
||
|
||
<section className="space-y-3">
|
||
<div className="rounded-lg border bg-muted/30 p-3 text-sm">
|
||
<p>سلام مریم، پیگیری میکنم...</p>
|
||
</div>
|
||
<Button variant="outline" className="w-full gap-2">
|
||
<Clock className="size-4" />
|
||
زمانبندی پیگیری
|
||
</Button>
|
||
</section>
|
||
</div>
|
||
</ScrollArea>
|
||
)}
|
||
</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">
|
||
{[
|
||
{
|
||
id: "inbox",
|
||
label: "صندوق ورودی",
|
||
icon: Inbox,
|
||
onClick: () => {
|
||
setActiveNavItem("inbox");
|
||
setIsMobileTicketListOpen(true);
|
||
},
|
||
},
|
||
{
|
||
id: "unassigned",
|
||
label: "بدون مسئول",
|
||
icon: MailOpen,
|
||
onClick: () => {
|
||
setActiveNavItem("unassigned");
|
||
setIsMobileTicketListOpen(true);
|
||
},
|
||
},
|
||
{
|
||
id: "assigned",
|
||
label: "محولشده",
|
||
icon: UserCheck,
|
||
onClick: () => {
|
||
setActiveNavItem("assigned");
|
||
setIsMobileTicketListOpen(true);
|
||
},
|
||
},
|
||
{
|
||
id: "search",
|
||
label: "جستجو",
|
||
icon: Search,
|
||
onClick: () => setIsCommandOpen(true),
|
||
},
|
||
{
|
||
id: "more",
|
||
label: "بیشتر",
|
||
icon: MoreHorizontal,
|
||
onClick: () => {},
|
||
},
|
||
].map((item) => {
|
||
const Icon = item.icon;
|
||
const isActive = activeNavItem === item.id;
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
onClick={item.onClick}
|
||
className={cn(
|
||
"flex flex-col items-center gap-1 py-2 text-xs",
|
||
isActive
|
||
? "text-foreground"
|
||
: "text-muted-foreground hover:text-foreground",
|
||
)}
|
||
>
|
||
<Icon className="size-5" />
|
||
<span>{item.label}</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</nav>
|
||
</SidebarProvider>
|
||
);
|
||
}
|