1071 lines
37 KiB
TypeScript
1071 lines
37 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
ArchiveX,
|
|
BadgeCheck,
|
|
Bell,
|
|
Calendar,
|
|
ChevronDown,
|
|
ChevronsUpDown,
|
|
CreditCard,
|
|
File,
|
|
FileSpreadsheet,
|
|
Inbox,
|
|
LogOut,
|
|
Mail,
|
|
MoreHorizontal,
|
|
Send,
|
|
Settings,
|
|
Sparkles,
|
|
StickyNote,
|
|
Trash2,
|
|
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 {
|
|
Drawer,
|
|
DrawerContent,
|
|
DrawerHeader,
|
|
DrawerTitle,
|
|
} from "@/components/ui/drawer";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { Label } from "@/components/ui/label";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarFooter,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarHeader,
|
|
SidebarInput,
|
|
SidebarInset,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarProvider,
|
|
useSidebar,
|
|
} from "@/components/ui/sidebar";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type FolderId = "inbox" | "sent" | "drafts" | "junk" | "trash";
|
|
|
|
type Folder = {
|
|
id: FolderId;
|
|
label: string;
|
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
count?: number;
|
|
};
|
|
|
|
const folders: Folder[] = [
|
|
{ id: "inbox", label: "صندوق ورودی", icon: Inbox },
|
|
{ id: "drafts", label: "پیشنویسها", icon: File },
|
|
{ id: "sent", label: "ارسالشده", icon: Send },
|
|
{ id: "junk", label: "هرزنامه", icon: ArchiveX },
|
|
{ id: "trash", label: "سطل زباله", icon: Trash2 },
|
|
];
|
|
|
|
const navTitleToFolderId: Record<string, FolderId> = {
|
|
"صندوق ورودی": "inbox",
|
|
"پیشنویسها": "drafts",
|
|
"ارسالشده": "sent",
|
|
"هرزنامه": "junk",
|
|
"سطل زباله": "trash",
|
|
};
|
|
|
|
type SidebarModule = {
|
|
id: string;
|
|
label: string;
|
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
|
};
|
|
|
|
const sidebarModules: SidebarModule[] = [
|
|
{ id: "emails", label: "ایمیلها", icon: Mail },
|
|
{ id: "calendar", label: "تقویم", icon: Calendar },
|
|
{ id: "contacts", label: "مخاطبین", icon: Users },
|
|
{ id: "tasks", label: "وظایف", icon: FileSpreadsheet },
|
|
{ id: "notes", label: "یادداشتها", icon: StickyNote },
|
|
];
|
|
|
|
const userData = {
|
|
name: "علی رضایی",
|
|
email: "ali@example.com",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar1.webp",
|
|
};
|
|
|
|
function getInitials(name: string) {
|
|
return (
|
|
name
|
|
.split(" ")
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((part) => part[0]?.toUpperCase())
|
|
.join("") || "U"
|
|
);
|
|
}
|
|
|
|
const data = {
|
|
user: {
|
|
name: "shadcn",
|
|
email: "m@example.com",
|
|
avatar: "/avatars/shadcn.jpg",
|
|
},
|
|
navMain: [
|
|
{
|
|
title: "صندوق ورودی",
|
|
url: "#",
|
|
icon: Inbox,
|
|
isActive: true,
|
|
},
|
|
{
|
|
title: "پیشنویسها",
|
|
url: "#",
|
|
icon: File,
|
|
isActive: false,
|
|
},
|
|
{
|
|
title: "ارسالشده",
|
|
url: "#",
|
|
icon: Send,
|
|
isActive: false,
|
|
},
|
|
{
|
|
title: "هرزنامه",
|
|
url: "#",
|
|
icon: ArchiveX,
|
|
isActive: false,
|
|
},
|
|
{
|
|
title: "سطل زباله",
|
|
url: "#",
|
|
icon: Trash2,
|
|
isActive: false,
|
|
},
|
|
],
|
|
mails: [
|
|
{
|
|
id: "1",
|
|
name: "مریم Mitchell",
|
|
email: "sarah.mitchell@acme.io",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar2.webp",
|
|
verified: true,
|
|
subject: "بررسی نقشه راه محصول Q4",
|
|
date: "۰۹:۳۰",
|
|
teaser:
|
|
"سلام تیم، پیشنویس نقشه راه Q4 را آماده کردم و قبل از نهاییسازی هفته آینده نظر شما را میخواهم...",
|
|
read: false,
|
|
starred: true,
|
|
},
|
|
{
|
|
id: "2",
|
|
name: "GitHub",
|
|
email: "noreply@github.com",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar2.webp",
|
|
verified: true,
|
|
subject: "[acme/dashboard] ادغام PR #847",
|
|
date: "دیروز",
|
|
teaser:
|
|
"درخواست ادغام شما در main ادغام شد. خط لوله CI/CD شروع شده و استقرار روی staging در جریان است...",
|
|
read: true,
|
|
starred: false,
|
|
},
|
|
{
|
|
id: "3",
|
|
name: "امیر احمدی",
|
|
email: "alex.t@designstudio.co",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar3.webp",
|
|
verified: true,
|
|
subject: "داراییهای برند جدید آماده بازبینی",
|
|
date: "دیروز",
|
|
teaser:
|
|
"سلام! راهنمای برند و کتابخانه دارایی بهروزرسانی شد. پالت رنگ جدید هم اضافه شده است...",
|
|
read: true,
|
|
starred: false,
|
|
},
|
|
{
|
|
id: "4",
|
|
name: "Stripe",
|
|
email: "notifications@stripe.com",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar4.webp",
|
|
verified: true,
|
|
subject: "واریز آذر ماه آغاز شد",
|
|
date: "۲ روز پیش",
|
|
teaser:
|
|
"مبلغ ۱۲٬۴۵۰ دلار آمریکا به حساب بانکی با پایان •••• 4521 در حال واریز است...",
|
|
read: true,
|
|
starred: true,
|
|
},
|
|
{
|
|
id: "5",
|
|
name: "مهدی جعفری",
|
|
email: "marcus@venturecap.fund",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar5.webp",
|
|
verified: false,
|
|
subject: "پیگیری: گفتگوی Series A",
|
|
date: "۳ روز پیش",
|
|
teaser:
|
|
"از ملاقات در TechCrunch Disrupt هفته گذشته خوشحال شدم. دوست دارم درباره رشد شما ادامه دهیم...",
|
|
read: true,
|
|
starred: false,
|
|
},
|
|
{
|
|
id: "6",
|
|
name: "Linear",
|
|
email: "notifications@linear.app",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar6.webp",
|
|
verified: true,
|
|
subject: "خلاصه هفتگی پروژه",
|
|
date: "۳ روز پیش",
|
|
teaser:
|
|
"خلاصه این هفته: ۲۳ مورد انجامشده، ۸ در جریان، ۵ مورد جدید ایجاد شده...",
|
|
read: true,
|
|
starred: false,
|
|
},
|
|
{
|
|
id: "7",
|
|
name: "Emma حسینی",
|
|
email: "emma.w@clientcorp.com",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar8.webp",
|
|
verified: true,
|
|
subject: "تمدید قرارداد اقدام لازم",
|
|
date: "۴ روز پیش",
|
|
teaser:
|
|
"قرارداد سالانه ماه آینده برای تمدید است. میخواستم درباره سطحهای قیمت جدید صحبت کنیم...",
|
|
read: false,
|
|
starred: false,
|
|
},
|
|
{
|
|
id: "8",
|
|
name: "Vercel",
|
|
email: "notifications@vercel.com",
|
|
avatar:
|
|
"/block-media/shadcnblocks/block/avatar/avatar8.webp",
|
|
verified: true,
|
|
subject: "خطای build: acme-dashboard",
|
|
date: "۴ روز پیش",
|
|
teaser:
|
|
"آخرین استقرار acme-dashboard ناموفق بود. خطا: Module not found: Can't resolve 'componen...",
|
|
read: true,
|
|
starred: false,
|
|
},
|
|
],
|
|
};
|
|
|
|
type NavItem = (typeof data.navMain)[number];
|
|
type MailItem = (typeof data.mails)[number];
|
|
|
|
function VerifiedIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg
|
|
viewBox="0 0 22 22"
|
|
className={cn("size-4 text-[#38bdf8]", className)}
|
|
fill="currentColor"
|
|
>
|
|
<path d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.136.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816zM9.662 14.85l-3.429-3.428 1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function NavUser({
|
|
user,
|
|
}: {
|
|
user: {
|
|
name: string;
|
|
email: string;
|
|
avatar: string;
|
|
};
|
|
}) {
|
|
const { isMobile } = useSidebar();
|
|
|
|
return (
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<SidebarMenuButton
|
|
size="lg"
|
|
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground md:h-8 md:p-0"
|
|
>
|
|
<Avatar className="h-8 w-8 rounded-lg">
|
|
<AvatarImage src={user.avatar} alt={user.name} />
|
|
<AvatarFallback className="rounded-lg">
|
|
{getInitials(user.name)}
|
|
</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">{user.email}</span>
|
|
</div>
|
|
<ChevronsUpDown className="ms-auto size-4" />
|
|
</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 rounded-lg">
|
|
<AvatarImage src={user.avatar} alt={user.name} />
|
|
<AvatarFallback className="rounded-lg">
|
|
{getInitials(user.name)}
|
|
</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">{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 FolderTabsProps {
|
|
className?: string;
|
|
folders: Folder[];
|
|
activeFolder: FolderId;
|
|
onFolderChange: (folderId: FolderId) => void;
|
|
activeEmailCount: number;
|
|
}
|
|
|
|
function FolderTabs({
|
|
className,
|
|
folders,
|
|
activeFolder,
|
|
onFolderChange,
|
|
activeEmailCount,
|
|
}: FolderTabsProps) {
|
|
const mainFolders = folders.slice(0, 5);
|
|
const additionalFolders = folders.slice(5);
|
|
|
|
return (
|
|
<div className={cn("flex items-center gap-2", className)}>
|
|
{/* Desktop: All folders visible at 2xl */}
|
|
<div className="hidden items-center gap-1 2xl:flex">
|
|
{folders.map((folder) => {
|
|
const Icon = folder.icon;
|
|
const isActive = activeFolder === folder.id;
|
|
return (
|
|
<Button
|
|
key={folder.id}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onFolderChange(folder.id)}
|
|
className={cn(
|
|
"h-[30px] gap-1.5",
|
|
isActive && "bg-muted text-foreground hover:bg-muted",
|
|
)}
|
|
>
|
|
<Icon className="size-4" />
|
|
<span className="text-[13px]">{folder.label}</span>
|
|
{isActive && (
|
|
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
|
|
{activeEmailCount.toLocaleString("fa-IR")}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Large: Main folders + dropdown for additional */}
|
|
<div className="hidden items-center gap-1 xl:flex 2xl:hidden">
|
|
{mainFolders.map((folder) => {
|
|
const Icon = folder.icon;
|
|
const isActive = activeFolder === folder.id;
|
|
return (
|
|
<Button
|
|
key={folder.id}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onFolderChange(folder.id)}
|
|
className={cn(
|
|
"h-[30px] gap-1.5",
|
|
isActive && "bg-muted text-foreground hover:bg-muted",
|
|
)}
|
|
>
|
|
<Icon className="size-4" />
|
|
<span className="text-[13px]">{folder.label}</span>
|
|
{isActive && (
|
|
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
|
|
{activeEmailCount.toLocaleString("fa-IR")}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
);
|
|
})}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="size-[30px]">
|
|
<MoreHorizontal className="size-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
{additionalFolders.map((folder) => {
|
|
const Icon = folder.icon;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={folder.id}
|
|
onClick={() => onFolderChange(folder.id)}
|
|
>
|
|
<Icon className="me-2 size-4" />
|
|
{folder.label}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Medium: First 3 folders + dropdown */}
|
|
<div className="hidden items-center gap-1 lg:flex xl:hidden">
|
|
{mainFolders.slice(0, 3).map((folder) => {
|
|
const Icon = folder.icon;
|
|
const isActive = activeFolder === folder.id;
|
|
return (
|
|
<Button
|
|
key={folder.id}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onFolderChange(folder.id)}
|
|
className={cn(
|
|
"h-[30px] gap-1.5",
|
|
isActive && "bg-muted text-foreground hover:bg-muted",
|
|
)}
|
|
>
|
|
<Icon className="size-4" />
|
|
<span className="text-[13px]">{folder.label}</span>
|
|
{isActive && (
|
|
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
|
|
{activeEmailCount.toLocaleString("fa-IR")}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
);
|
|
})}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="size-[30px]">
|
|
<MoreHorizontal className="size-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
{[...mainFolders.slice(3), ...additionalFolders].map((folder) => {
|
|
const Icon = folder.icon;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={folder.id}
|
|
onClick={() => onFolderChange(folder.id)}
|
|
>
|
|
<Icon className="me-2 size-4" />
|
|
{folder.label}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Small: First 2 folders + dropdown */}
|
|
<div className="hidden items-center gap-1 md:flex lg:hidden">
|
|
{mainFolders.slice(0, 2).map((folder) => {
|
|
const Icon = folder.icon;
|
|
const isActive = activeFolder === folder.id;
|
|
return (
|
|
<Button
|
|
key={folder.id}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => onFolderChange(folder.id)}
|
|
className={cn(
|
|
"h-[30px] gap-1.5",
|
|
isActive && "bg-muted text-foreground hover:bg-muted",
|
|
)}
|
|
>
|
|
<Icon className="size-4" />
|
|
<span className="text-[13px]">{folder.label}</span>
|
|
{isActive && (
|
|
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
|
|
{activeEmailCount.toLocaleString("fa-IR")}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
);
|
|
})}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="size-[30px]">
|
|
<MoreHorizontal className="size-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
{[...mainFolders.slice(2), ...additionalFolders].map((folder) => {
|
|
const Icon = folder.icon;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={folder.id}
|
|
onClick={() => onFolderChange(folder.id)}
|
|
>
|
|
<Icon className="me-2 size-4" />
|
|
{folder.label}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
{/* Mobile: Single dropdown */}
|
|
<div className="flex items-center gap-1 md:hidden">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-[30px] gap-1.5">
|
|
{(() => {
|
|
const ActiveIcon =
|
|
folders.find((f) => f.id === activeFolder)?.icon || Inbox;
|
|
return <ActiveIcon className="size-4" />;
|
|
})()}
|
|
<span className="text-[13px]">
|
|
{folders.find((f) => f.id === activeFolder)?.label}
|
|
</span>
|
|
<Badge variant="secondary" className="h-5 px-1.5 text-[10px]">
|
|
{activeEmailCount.toLocaleString()}
|
|
</Badge>
|
|
<ChevronDown className="size-3" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
{folders.map((folder) => {
|
|
const Icon = folder.icon;
|
|
const isActive = activeFolder === folder.id;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={folder.id}
|
|
onClick={() => onFolderChange(folder.id)}
|
|
>
|
|
<Icon className="me-2 size-4" />
|
|
{folder.label}
|
|
{isActive && (
|
|
<span className="ms-auto text-xs text-muted-foreground">
|
|
{activeEmailCount.toLocaleString("fa-IR")}
|
|
</span>
|
|
)}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
|
|
activeModule: string;
|
|
onModuleChange: (moduleId: string) => void;
|
|
}
|
|
|
|
export function AppSidebar({
|
|
activeModule,
|
|
onModuleChange,
|
|
...props
|
|
}: AppSidebarProps) {
|
|
return (
|
|
<Sidebar side="right" collapsible="icon" {...props}>
|
|
<SidebarHeader>
|
|
<SidebarMenu>
|
|
<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>
|
|
<div className="grid flex-1 text-start text-sm leading-tight">
|
|
<span className="truncate font-medium">ری یو آی</span>
|
|
<span className="truncate text-xs">سازمانی</span>
|
|
</div>
|
|
</a>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarHeader>
|
|
<SidebarContent>
|
|
<SidebarGroup>
|
|
<SidebarGroupContent className="px-1.5 md:px-0">
|
|
<SidebarMenu>
|
|
{sidebarModules.map((moduleItem) => (
|
|
<SidebarMenuItem key={moduleItem.id}>
|
|
<SidebarMenuButton
|
|
tooltip={{
|
|
children: moduleItem.label,
|
|
hidden: false,
|
|
}}
|
|
onClick={() => onModuleChange(moduleItem.id)}
|
|
isActive={activeModule === moduleItem.id}
|
|
className="px-2.5 md:px-2"
|
|
>
|
|
<moduleItem.icon />
|
|
<span>{moduleItem.label}</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
<SidebarFooter>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
tooltip={{
|
|
children: "تنظیمات",
|
|
hidden: false,
|
|
}}
|
|
className="px-2.5 md:px-2"
|
|
aria-label="تنظیمات"
|
|
>
|
|
<Settings />
|
|
<span>تنظیمات</span>
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarFooter>
|
|
</Sidebar>
|
|
);
|
|
}
|
|
|
|
interface MailSidebarProps {
|
|
activeItem: NavItem;
|
|
mails: MailItem[];
|
|
selectedEmailId: string | null;
|
|
onEmailSelect: (emailId: string) => void;
|
|
}
|
|
|
|
function MailSidebar({
|
|
activeItem,
|
|
mails,
|
|
selectedEmailId,
|
|
onEmailSelect,
|
|
}: MailSidebarProps) {
|
|
return (
|
|
<Sidebar
|
|
collapsible="none"
|
|
className="w-full shrink-0 border-e md:flex md:w-[320px]"
|
|
>
|
|
<SidebarHeader className="gap-3.5 border-b p-4">
|
|
<div className="flex w-full items-center justify-between">
|
|
<div className="text-start text-base font-medium text-foreground">
|
|
{activeItem?.title}
|
|
</div>
|
|
<Label className="flex items-center gap-2 text-sm">
|
|
<span>خواندهنشده</span>
|
|
<Switch className="shadow-none" />
|
|
</Label>
|
|
</div>
|
|
<SidebarInput placeholder="جستجو..." aria-label="جستجو" />
|
|
</SidebarHeader>
|
|
<SidebarContent className="overflow-hidden">
|
|
<ScrollArea className="min-h-0 flex-1 [&>[data-slot=scroll-area-viewport]>div]:!block">
|
|
<SidebarGroup className="px-0">
|
|
<SidebarGroupContent>
|
|
{mails.map((mail) => {
|
|
const isSelected = selectedEmailId === mail.id;
|
|
return (
|
|
<button
|
|
type="button"
|
|
key={mail.id}
|
|
onClick={() => onEmailSelect(mail.id)}
|
|
className={cn(
|
|
"flex w-full gap-3 border-b p-4 text-start text-sm leading-tight last:border-b-0 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
!mail.read && "bg-muted/30",
|
|
isSelected && "bg-sidebar-accent",
|
|
)}
|
|
>
|
|
<Avatar className="mt-0.5 size-9 shrink-0">
|
|
<AvatarImage src={mail.avatar} alt={mail.name} />
|
|
<AvatarFallback className="bg-primary text-xs font-medium text-primary-foreground">
|
|
{getInitials(mail.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex min-w-0 items-center gap-1">
|
|
<span
|
|
className={cn(
|
|
"truncate text-sm",
|
|
!mail.read && "font-semibold",
|
|
)}
|
|
>
|
|
{mail.name}
|
|
</span>
|
|
{mail.verified && (
|
|
<VerifiedIcon className="size-3.5 shrink-0" />
|
|
)}
|
|
</div>
|
|
<span className="shrink-0 text-xs text-muted-foreground">
|
|
{mail.date}
|
|
</span>
|
|
</div>
|
|
<p
|
|
className={cn(
|
|
"mt-0.5 truncate text-sm",
|
|
!mail.read && "font-medium",
|
|
)}
|
|
>
|
|
{mail.subject}
|
|
</p>
|
|
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
|
|
{mail.teaser}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</ScrollArea>
|
|
</SidebarContent>
|
|
</Sidebar>
|
|
);
|
|
}
|
|
|
|
export function ApplicationShell8() {
|
|
const [activeModule, setActiveModule] = React.useState("emails");
|
|
const [activeFolder, setActiveFolder] = React.useState<FolderId>("inbox");
|
|
const [mails, setMails] = React.useState<MailItem[]>(data.mails);
|
|
const [selectedEmailId, setSelectedEmailId] = React.useState<string | null>(
|
|
data.mails[0]?.id ?? null,
|
|
);
|
|
const [isMobileDetailOpen, setIsMobileDetailOpen] = React.useState(false);
|
|
|
|
const filteredEmails = mails;
|
|
const selectedEmail = mails.find((m) => m.id === selectedEmailId) ?? null;
|
|
|
|
const handleModuleChange = (moduleId: string) => {
|
|
setActiveModule(moduleId);
|
|
};
|
|
|
|
const handleFolderChange = (folderId: FolderId) => {
|
|
setActiveFolder(folderId);
|
|
const mail = [...data.mails].sort(() => Math.random() - 0.5);
|
|
setMails(mail.slice(0, Math.max(5, Math.floor(Math.random() * 10) + 1)));
|
|
};
|
|
|
|
const handleEmailSelect = (emailId: string) => {
|
|
setSelectedEmailId(emailId);
|
|
setMails((prev) =>
|
|
prev.map((m) => (m.id === emailId ? { ...m, read: true } : m)),
|
|
);
|
|
if (window.innerWidth < 768) {
|
|
setIsMobileDetailOpen(true);
|
|
}
|
|
};
|
|
|
|
const activeItem =
|
|
data.navMain.find(
|
|
(item) => navTitleToFolderId[item.title] === activeFolder,
|
|
) || data.navMain[0];
|
|
|
|
return (
|
|
<SidebarProvider
|
|
dir="rtl"
|
|
lang="fa"
|
|
defaultOpen={false}
|
|
className="h-svh overflow-hidden"
|
|
style={
|
|
{
|
|
"--sidebar-width": "var(--sidebar-width-icon)",
|
|
} as React.CSSProperties
|
|
}
|
|
>
|
|
<AppSidebar
|
|
activeModule={activeModule}
|
|
onModuleChange={handleModuleChange}
|
|
/>
|
|
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
<header className="flex h-14 shrink-0 items-center justify-between border-b bg-background px-4">
|
|
<div className="flex items-center">
|
|
<div className="ms-2 flex items-center md:hidden">
|
|
<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>
|
|
</div>
|
|
{activeModule === "emails" ? (
|
|
<FolderTabs
|
|
folders={folders}
|
|
activeFolder={activeFolder}
|
|
onFolderChange={handleFolderChange}
|
|
activeEmailCount={filteredEmails.length}
|
|
/>
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
{(() => {
|
|
const moduleData = sidebarModules.find(
|
|
(m) => m.id === activeModule,
|
|
);
|
|
if (!moduleData) return null;
|
|
const Icon = moduleData.icon;
|
|
return (
|
|
<>
|
|
<Icon className="size-5" />
|
|
<span className="text-base font-medium">
|
|
{moduleData.label}
|
|
</span>
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="flex h-auto items-center gap-2 px-2 py-1"
|
|
>
|
|
<Avatar className="size-7">
|
|
<AvatarImage src={userData.avatar} alt={userData.name} />
|
|
<AvatarFallback>{getInitials(userData.name)}</AvatarFallback>
|
|
</Avatar>
|
|
<span className="hidden font-medium sm:inline">
|
|
{userData.name}
|
|
</span>
|
|
<ChevronDown className="size-3 text-muted-foreground" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-56" align="start">
|
|
<DropdownMenuLabel className="font-normal">
|
|
<div className="flex flex-col space-y-1 text-start">
|
|
<p className="text-sm font-medium">{userData.name}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
{userData.email}
|
|
</p>
|
|
</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>
|
|
</header>
|
|
<div className="flex min-h-0 flex-1 overflow-hidden pb-14 md:pb-0">
|
|
{activeModule === "emails" && (
|
|
<MailSidebar
|
|
activeItem={activeItem}
|
|
mails={mails}
|
|
selectedEmailId={selectedEmailId}
|
|
onEmailSelect={handleEmailSelect}
|
|
/>
|
|
)}
|
|
<SidebarInset className="hidden min-h-0 md:flex">
|
|
<ScrollArea className="flex-1">
|
|
<div className="flex flex-1 flex-col gap-4 p-4">
|
|
{activeModule === "emails" ? (
|
|
Array.from({ length: 24 }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className="aspect-video h-12 w-full rounded-lg bg-muted/50"
|
|
/>
|
|
))
|
|
) : (
|
|
<div className="flex h-full items-center justify-center text-muted-foreground">
|
|
<div className="text-center">
|
|
{(() => {
|
|
const moduleData = sidebarModules.find(
|
|
(m) => m.id === activeModule,
|
|
);
|
|
if (!moduleData) return null;
|
|
const Icon = moduleData.icon;
|
|
return (
|
|
<>
|
|
<Icon className="mx-auto size-12 opacity-50" />
|
|
<p className="mt-2 text-sm">
|
|
محتوای {moduleData.label} بهزودی
|
|
</p>
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</SidebarInset>
|
|
</div>
|
|
</div>
|
|
|
|
<Drawer
|
|
open={isMobileDetailOpen}
|
|
onOpenChange={setIsMobileDetailOpen}
|
|
dismissible
|
|
>
|
|
<DrawerContent dir="rtl" className="h-[90vh] text-start md:hidden">
|
|
<DrawerHeader className="sr-only">
|
|
<DrawerTitle>جزئیات ایمیل</DrawerTitle>
|
|
</DrawerHeader>
|
|
<div className="flex h-full flex-col">
|
|
{selectedEmail ? (
|
|
<ScrollArea className="flex-1 p-4">
|
|
<div className="space-y-4">
|
|
<div className="flex items-start gap-3">
|
|
<Avatar className="size-10 shrink-0">
|
|
<AvatarImage
|
|
src={selectedEmail.avatar}
|
|
alt={selectedEmail.name}
|
|
/>
|
|
<AvatarFallback className="bg-primary font-medium text-primary-foreground">
|
|
{getInitials(selectedEmail.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-1">
|
|
<span className="font-medium">
|
|
{selectedEmail.name}
|
|
</span>
|
|
{selectedEmail.verified && (
|
|
<VerifiedIcon className="size-4" />
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">
|
|
{selectedEmail.email}
|
|
</p>
|
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
{selectedEmail.date}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<h1 className="text-xl font-medium">
|
|
{selectedEmail.subject}
|
|
</h1>
|
|
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
{selectedEmail.teaser}
|
|
</p>
|
|
</div>
|
|
</ScrollArea>
|
|
) : (
|
|
<div className="flex flex-1 items-center justify-center text-muted-foreground">
|
|
<Mail className="size-12 opacity-50" />
|
|
</div>
|
|
)}
|
|
</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">
|
|
{sidebarModules.map((moduleItem) => {
|
|
const Icon = moduleItem.icon;
|
|
const isActive = activeModule === moduleItem.id;
|
|
return (
|
|
<button
|
|
key={moduleItem.id}
|
|
type="button"
|
|
onClick={() => setActiveModule(moduleItem.id)}
|
|
className={cn(
|
|
"flex flex-col items-center justify-center py-3 text-xs",
|
|
isActive
|
|
? "text-foreground"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
aria-label={moduleItem.label}
|
|
>
|
|
<Icon className="size-5" />
|
|
<span className="sr-only">{moduleItem.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
</SidebarProvider>
|
|
);
|
|
}
|