Files
reui-blocks/blocks/sidebar19.tsx
T

608 lines
17 KiB
TypeScript

"use client";
import {
Bell,
Check,
ChevronsUpDown,
Hash,
Headphones,
Lock,
Mic,
MoreHorizontal,
PencilLine,
Pin,
Plus,
Settings,
Trash2,
Volume2,
} 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,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
// Types
type TextChannel = {
id: string;
name: string;
type: "text";
locked: boolean;
unread: number;
isActive?: boolean;
};
type VoiceChannel = {
id: string;
name: string;
type: "voice";
users: string[];
};
type Channel = TextChannel | VoiceChannel;
type ChannelCategory = {
id: string;
name: string;
isOpen: boolean;
channels: Channel[];
};
type Member = {
id: string;
name: string;
avatar: string;
role: string;
status: "online" | "idle" | "dnd" | "offline";
};
type Workspace = {
id: string;
name: string;
logo: string;
plan: string;
};
type UserData = {
name: string;
email: string;
avatar: string;
};
// Data
const workspaces: Workspace[] = [
{
id: "1",
name: "ShadcnBlocks",
logo: "/block-media/shadcnblocks/block/logos/ShadcnBlocks-logo.svg",
plan: "سازمانی",
},
{
id: "2",
name: "قالب‌های پشتیبانی‌یار",
logo: "/block-media/shadcnblocks/block/logos/ShadcnBlocks-logo.svg",
plan: "تیم کوچک",
},
{
id: "3",
name: "میز‌های خدمت",
logo: "/block-media/shadcnblocks/block/logos/ShadcnBlocks-logo.svg",
plan: "آزمایشی",
},
];
const currentUser: UserData = {
name: "علی رضایی",
email: "ali@example.com",
avatar:
"/block-media/shadcnblocks/block/avatar-2.webp",
};
const channelCategories: ChannelCategory[] = [
{
id: "cat1",
name: "اطلاعیه‌ها",
isOpen: true,
channels: [
{ id: "1", name: "اعلان‌ها", type: "text", locked: true, unread: 0 },
{ id: "2", name: "SLA و قوانین", type: "text", locked: true, unread: 0 },
{ id: "3", name: "خوش‌آمدگویی", type: "text", locked: false, unread: 0 },
],
},
{
id: "cat2",
name: "عمومی",
isOpen: true,
channels: [
{
id: "4",
name: "عمومی",
type: "text",
locked: false,
unread: 12,
isActive: true,
},
{ id: "5", name: "گفت‌وگوی آزاد", type: "text", locked: false, unread: 3 },
{
id: "6",
name: "معرفی اعضا",
type: "text",
locked: false,
unread: 0,
},
],
},
{
id: "cat3",
name: "محصول",
isOpen: true,
channels: [
{ id: "7", name: "ورود مشتری", type: "text", locked: false, unread: 5 },
{ id: "8", name: "صورتحساب", type: "text", locked: false, unread: 0 },
{ id: "9", name: "زیرساخت", type: "text", locked: false, unread: 1 },
{ id: "10", name: "بازبینی کیفیت", type: "text", locked: false, unread: 0 },
],
},
{
id: "cat4",
name: "تماس‌های پشتیبانی",
isOpen: true,
channels: [
{
id: "v1",
name: "خط عمومی",
type: "voice",
users: ["سارا", "میکاییل", "آرش"],
},
{ id: "v2", name: "اتاق هماهنگی", type: "voice", users: [] },
{ id: "v3", name: "همکاری دونفره", type: "voice", users: ["نیلوفر"] },
],
},
];
const members: Member[] = [
{
id: "1",
name: "سارا احمدی",
avatar:
"/block-media/shadcnblocks/block/avatar-1.webp",
role: "مدیر",
status: "online",
},
{
id: "2",
name: "میکاییل رضایی",
avatar:
"/block-media/shadcnblocks/block/avatar-3.webp",
role: "ناظر",
status: "online",
},
{
id: "3",
name: "نیلوفر حسینی",
avatar:
"/block-media/shadcnblocks/block/avatar-3.webp",
role: "عضو",
status: "idle",
},
{
id: "4",
name: "آرش کیانی",
avatar:
"/block-media/shadcnblocks/block/avatar-5.webp",
role: "عضو",
status: "dnd",
},
{
id: "5",
name: "کاوه ویلسون",
avatar:
"/block-media/shadcnblocks/block/avatar-4.webp",
role: "عضو",
status: "offline",
},
{
id: "6",
name: "جواد لطفی",
avatar:
"/block-media/shadcnblocks/block/avatar-2.webp",
role: "عضو",
status: "offline",
},
];
// Helper
const getInitials = (name: string) =>
name
.split(" ")
.map((n) => n[0])
.join("");
const statusColors: Record<Member["status"], string> = {
online: "bg-green-500",
idle: "bg-yellow-500",
dnd: "bg-red-500",
offline: "bg-gray-400",
};
// Components
const WorkspaceSwitcher = () => {
const [selected, setSelected] = React.useState(workspaces[0]);
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-primary">
<img
src={selected.logo}
alt={selected.name}
className="size-6 invert dark:invert-0"
/>
</div>
<div className="grid flex-1 text-start text-sm leading-tight">
<span className="truncate font-medium">{selected.name}</span>
<span className="truncate text-xs text-muted-foreground">
{selected.plan}
</span>
</div>
<ChevronsUpDown className="ms-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="start"
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
فضاهای کاری
</DropdownMenuLabel>
{workspaces.map((workspace) => (
<DropdownMenuItem
key={workspace.id}
onClick={() => setSelected(workspace)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm bg-primary">
<img
src={workspace.logo}
alt={workspace.name}
className="size-4 invert dark:invert-0"
/>
</div>
<span>{workspace.name}</span>
{workspace.id === selected.id && (
<Check className="ms-auto size-4" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
};
const ChannelItem = ({ channel }: { channel: Channel }) => {
const isText = channel.type === "text";
const isVoice = channel.type === "voice";
return (
<SidebarMenuItem className="group/item">
<SidebarMenuButton isActive={isText && channel.isActive} className="ps-8">
{isVoice ? (
<Volume2 className="size-4 text-muted-foreground" />
) : channel.locked ? (
<Lock className="size-4 text-muted-foreground" />
) : (
<Hash className="size-4 text-muted-foreground" />
)}
<span
className={cn(
isText && channel.unread > 0 && "font-semibold text-foreground",
)}
>
{channel.name}
</span>
{isText && channel.unread > 0 && (
<Badge
variant="default"
className="ms-auto size-5 justify-center rounded-full p-0 text-[10px]"
>
{channel.unread}
</Badge>
)}
{isVoice && channel.users.length > 0 && (
<span className="ms-auto text-xs text-muted-foreground">
{channel.users.length}
</span>
)}
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction className="opacity-0 group-hover/item:opacity-100">
<MoreHorizontal className="size-3.5" />
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent side="left" align="start">
<DropdownMenuItem>
<Bell className="me-2 size-4" />
بی‌صدا کردن کانال
</DropdownMenuItem>
<DropdownMenuItem>
<Pin className="me-2 size-4" />
سنجاق کانال
</DropdownMenuItem>
<DropdownMenuItem>
<PencilLine className="me-2 size-4" />
ویرایش کانال
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
<Trash2 className="me-2 size-4" />
حذف کانال
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{isVoice && channel.users.length > 0 && (
<div className="mt-1 ms-6 space-y-1">
{channel.users.map((userName) => (
<div
key={userName}
className="flex items-center gap-2 py-0.5 text-xs text-muted-foreground"
>
<div className="size-4 rounded-full bg-muted" />
<span>{userName}</span>
</div>
))}
</div>
)}
</SidebarMenuItem>
);
};
const ChannelCategoryGroup = ({ category }: { category: ChannelCategory }) => {
const [isOpen, setIsOpen] = React.useState(category.isOpen);
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<SidebarGroup className="py-0">
<SidebarGroupLabel asChild className="group/label">
<CollapsibleTrigger className="flex w-full items-center justify-between">
<span className="truncate text-xs font-semibold tracking-wider uppercase">
{category.name}
</span>
<Plus className="size-3.5 opacity-0 transition-opacity group-hover/label:opacity-100" />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{category.channels.map((channel) => (
<ChannelItem key={channel.id} channel={channel} />
))}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
);
};
const MemberItem = ({
member,
showStatus = true,
}: {
member: Member;
showStatus?: boolean;
}) => (
<button
className={cn(
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 hover:bg-muted",
member.status === "offline" && "opacity-50 hover:opacity-100",
)}
>
<div className="relative">
<Avatar className="size-8">
<AvatarImage src={member.avatar} alt={member.name} />
<AvatarFallback className="text-xs">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
{showStatus && member.status !== "offline" && (
<span
className={cn(
"absolute -start-0.5 -bottom-0.5 size-3 rounded-full border-2 border-background",
statusColors[member.status],
)}
/>
)}
</div>
<div className="flex-1 text-start">
<p className="text-sm font-medium">{member.name}</p>
<p className="text-xs text-muted-foreground">{member.role}</p>
</div>
</button>
);
const MembersList = () => {
const online = members.filter((m) => m.status !== "offline");
const offline = members.filter((m) => m.status === "offline");
return (
<div className="flex flex-col gap-4 p-2">
<div>
<p className="mb-2 px-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase">
آنلاین {online.length}
</p>
<div className="space-y-0.5">
{online.map((member) => (
<MemberItem key={member.id} member={member} />
))}
</div>
</div>
<div>
<p className="mb-2 px-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase">
آفلاین {offline.length}
</p>
<div className="space-y-0.5">
{offline.map((member) => (
<MemberItem key={member.id} member={member} showStatus={false} />
))}
</div>
</div>
</div>
);
};
const NavUser = ({ user }: { user: UserData }) => (
<div className="flex items-center gap-2 rounded-md bg-sidebar-accent/50 p-2">
<Avatar className="size-8">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback>{getInitials(user.name)}</AvatarFallback>
</Avatar>
<div className="flex-1 truncate">
<p className="truncate text-sm font-medium">{user.name}</p>
<p className="truncate text-xs text-muted-foreground">آنلاین</p>
</div>
<TooltipProvider delayDuration={0}>
<div className="flex gap-0.5">
{[
{ icon: Mic, label: "بی‌صدا" },
{ icon: Headphones, label: "قطع صدا" },
{ icon: Settings, label: "تنظیمات" },
].map(({ icon: Icon, label }) => (
<Tooltip key={label}>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<Icon className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
))}
</div>
</TooltipProvider>
</div>
);
const SidebarLeft = (props: React.ComponentProps<typeof Sidebar>) => (
<Sidebar side="right" {...props}>
<SidebarHeader>
<WorkspaceSwitcher />
</SidebarHeader>
<SidebarContent className="overflow-hidden">
<ScrollArea className="min-h-0 flex-1">
{channelCategories.map((category) => (
<ChannelCategoryGroup key={category.id} category={category} />
))}
</ScrollArea>
</SidebarContent>
<SidebarFooter>
<NavUser user={currentUser} />
</SidebarFooter>
</Sidebar>
);
const SidebarRight = (props: React.ComponentProps<typeof Sidebar>) => (
<Sidebar
side="left"
collapsible="none"
className="sticky top-0 hidden h-svh w-60 border-s lg:flex"
{...props}
>
<SidebarHeader className="h-16 justify-center border-b">
<div className="px-2">
<span className="font-semibold">اعضا</span>
</div>
</SidebarHeader>
<SidebarContent className="overflow-hidden">
<ScrollArea className="min-h-0 flex-1">
<MembersList />
</ScrollArea>
</SidebarContent>
</Sidebar>
);
interface Sidebar19Props {
className?: string;
}
const Sidebar19 = ({ className }: Sidebar19Props) => (
<SidebarProvider dir="rtl" lang="fa" className={cn(className)}>
<SidebarLeft />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ms-1" />
<Separator
orientation="vertical"
className="ms-2 data-[orientation=vertical]:h-4"
/>
<Hash className="size-5 text-muted-foreground" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage className="font-semibold">عمومی</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
<div className="flex flex-1 flex-col gap-4 p-4">
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
</SidebarInset>
<SidebarRight />
</SidebarProvider>
);
export { Sidebar19 };