داشبورد
پیشنمایش تمامصفحهمنوی کناری، کارتهای آمار، نمودار فروش ماهانه با بازهٔ تاریخ و آخرین سفارشها.
۱. نصب کامپوننتها
این بلاک از ۱۹ آیتم دیگ استفاده میکند: avatarbadgebuttoncardchart-presetscheckboxcollapsiblecontrol-sizedropdown-menuiconslabelpaginationpersiansearch-fieldselectsidebarstat-tiletabletoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/collapsible.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/pagination.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/select.json https://docs.digdesign.ir/r/sidebar.json https://docs.digdesign.ir/r/stat-tile.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
dashboard.tsx
"use client"
import * as React from "react"
import {
AvatarGenderIcon,
BadgeStarIcon,
BarChartIcon,
BellIcon,
ChevronDownIcon,
DashboardIcon,
EnvelopeIcon,
DigLogo,
FilterIcon,
GuideIcon,
LogoutIcon,
MoonIcon,
SunIcon,
PlusIcon,
ReceiptIcon,
SettingsIcon,
ShieldCheckIcon,
ShopIcon,
TagIcon,
TicketIcon,
UsersIcon,
XIcon,
} from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ComboChart } from "@/components/ui/chart-presets"
import { Checkbox } from "@/components/ui/checkbox"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import { SearchField } from "@/components/ui/search-field"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
Sidebar,
SidebarContent,
SidebarBrand,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarSearch,
SidebarTrigger,
} from "@/components/ui/sidebar"
import {
StatTile,
StatTileAction,
StatTileHeader,
StatTileLabel,
StatTileList,
} from "@/components/ui/stat-tile"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { formatCurrency, formatJalali, formatNumber, formatRelativeTime, toFaDigits, toSearchKey } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
/* ------------------------------------------------------------------ داده */
/** دادهٔ نمونهٔ قطعی برای هر بازه؛ در پروژهٔ واقعی همین سه فراخوانی API است. */
function salesFor(period: string) {
const wave = (index: number, base: number, amp: number) => Math.round(base + amp * Math.sin(index * 0.9) + index * (amp / 6))
if (period === "7") {
return Array.from({ length: 7 }, (_, index) => ({
label: `${toFaDigits(3 + index)} مهر`,
revenue: wave(index, 52, 12),
orders: wave(index, 165, 28),
}))
}
if (period === "30") {
// ۳۰ روز در ۱۰ دستهٔ سهروزه، تا میلهها خوانا بمانند.
return Array.from({ length: 10 }, (_, index) => ({
label: `${toFaDigits(1 + index * 3)} ${index < 3 ? "شهریور" : "مهر"}`,
revenue: wave(index, 150, 30),
orders: wave(index, 480, 70),
}))
}
return Array.from({ length: 12 }, (_, index) => ({
label: `هفتهٔ ${toFaDigits(index + 1)}`,
revenue: wave(index, 360, 70),
orders: wave(index, 1150, 160),
}))
}
const QUICK = [
{ title: "محصولِ تازه", hint: "افزودن به فروشگاه", icon: <PlusIcon />, tone: "bg-primary-subtle text-primary-subtle-foreground" },
{ title: "ثبتِ سفارش", hint: "فروشِ حضوری", icon: <ReceiptIcon />, tone: "bg-info-subtle text-info-subtle-foreground" },
{ title: "کدِ تخفیف", hint: "کمپینِ تازه", icon: <TagIcon />, tone: "bg-warning-subtle text-warning-subtle-foreground" },
{ title: "گزارشِ فروش", hint: "خروجی اکسل", icon: <BarChartIcon />, tone: "bg-success-subtle text-success-subtle-foreground" },
]
const CUSTOMERS = [
{ name: "مهسا توکلی", gender: "female" as const, orders: 24, spent: "۱۲٫۴ میلیون" },
{ name: "کیان مرادی", gender: "male" as const, orders: 18, spent: "۹٫۸ میلیون" },
{ name: "نیلوفر احمدی", gender: "female" as const, orders: 11, spent: "۶٫۱ میلیون" },
{ name: "پرهام نوری", gender: "male" as const, orders: 7, spent: "۳٫۹ میلیون" },
]
type Status = "shipped" | "processing" | "delivered" | "pending"
const STATUS: Record<Status, { text: string; color: "warning" | "info" | "success" | "destructive" }> = {
shipped: { text: "ارسال شد", color: "warning" },
processing: { text: "در حال آمادهسازی", color: "info" },
delivered: { text: "تحویل شد", color: "success" },
pending: { text: "در انتظار پرداخت", color: "destructive" },
}
const ORDERS: { id: string; customer: string; date: string; total: number; status: Status }[] = [
{ id: "1098", customer: "آرمیتا رستگار", date: "2026-10-01", total: 2_450_000, status: "shipped" },
{ id: "1097", customer: "کیان مرادی", date: "2026-09-30", total: 1_265_000, status: "processing" },
{ id: "1096", customer: "نیلوفر احمدی", date: "2026-09-29", total: 5_100_000, status: "delivered" },
{ id: "1095", customer: "سارا محمدی", date: "2026-09-28", total: 789_000, status: "pending" },
{ id: "1094", customer: "علی کریمی", date: "2026-09-28", total: 3_320_000, status: "delivered" },
{ id: "1093", customer: "مهسا نوری", date: "2026-09-27", total: 1_980_000, status: "shipped" },
{ id: "1092", customer: "رضا امینی", date: "2026-09-27", total: 640_000, status: "delivered" },
{ id: "1091", customer: "پریسا کاظمی", date: "2026-09-26", total: 4_250_000, status: "processing" },
{ id: "1090", customer: "حامد یزدانی", date: "2026-09-25", total: 2_100_000, status: "delivered" },
{ id: "1089", customer: "الهام صادقی", date: "2026-09-25", total: 990_000, status: "pending" },
{ id: "1088", customer: "بهراد فرهی", date: "2026-09-24", total: 3_760_000, status: "delivered" },
{ id: "1087", customer: "شیما رحیمی", date: "2026-09-23", total: 1_430_000, status: "shipped" },
]
const PAGE_SIZE = 5
const NOTIFICATIONS = [
{ id: 1, title: "سفارش تازه", body: "سفارش ۱۰۹۹ ثبت شد", time: "2026-10-03T10:00:00", icon: <ReceiptIcon />, tone: "bg-info-subtle text-info" },
{ id: 2, title: "موجودی رو به اتمام", body: "فقط ۷ عدد از هدفون پرو مانده", time: "2026-10-03T09:00:00", icon: <ShopIcon />, tone: "bg-warning-subtle text-warning" },
{ id: 3, title: "نظر مثبت", body: "«ارسال سریع و بستهبندی عالی»", time: "2026-10-03T08:00:00", icon: <BadgeStarIcon />, tone: "bg-success-subtle text-success" },
]
/* ------------------------------------------------------------------ بلاک */
export default function DashboardBlock() {
const [period, setPeriod] = React.useState("30")
const [showRevenue, setShowRevenue] = React.useState(true)
const [showOrders, setShowOrders] = React.useState(true)
const [query, setQuery] = React.useState("")
const [selected, setSelected] = React.useState<string[]>([])
const [notifications, setNotifications] = React.useState(NOTIFICATIONS)
const [page, setPage] = React.useState(1)
const sales = React.useMemo(() => salesFor(period), [period])
const filtered = ORDERS.filter((order) => toSearchKey(order.customer + order.id).includes(toSearchKey(query)))
const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE))
const currentPage = Math.min(page, pageCount)
const orders = filtered.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE)
const allSelected = orders.length > 0 && orders.every((order) => selected.includes(order.id))
return (
<ControlSizeProvider size="xl" asChild>
<SidebarProvider>
<DashboardSidebar />
<SidebarInset className="flex min-w-0 flex-col gap-6 bg-muted p-4 sm:p-6">
<header className="flex flex-wrap items-center gap-3">
<SidebarTrigger />
<div className="flex flex-col gap-0.5">
<h1 className="text-xl font-bold">سلام آرمیتا</h1>
<p className="text-sm text-muted-foreground">امروز ۱۲ سفارشِ تازه منتظرِ آمادهسازی است.</p>
</div>
<SearchField
aria-label="جستوجو"
placeholder="جستوجوی محصول، مشتری یا سفارش…"
className="order-last w-full sm:order-none sm:ms-auto sm:w-80 [&_input]:bg-card [&>div]:bg-card"
/>
<div className="ms-auto flex items-center gap-2 sm:ms-0">
<Button variant="outline" size="icon" aria-label="اعلانها، ۳ مورد تازه" className="relative bg-card">
<BellIcon />
<span aria-hidden className="absolute end-2 top-2 size-2 rounded-full bg-destructive" />
</Button>
{/* پروفایل در نوارِ بالا؛ تنظیمات و خروج در منوی همین آواتار. */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="gap-2 px-1.5" aria-label="منوی حساب">
<Avatar size="sm" alt="آرمیتا رستگار" src={stockImages.personWoman1.src} icon={<AvatarGenderIcon gender="female" />} className="ring-1 ring-border" />
<span className="hidden flex-col items-start leading-snug md:flex">
<span className="text-sm font-semibold">آرمیتا رستگار</span>
<span className="text-xs text-muted-foreground">مدیر فروشگاه</span>
</span>
<ChevronDownIcon className="text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem>
<SettingsIcon />
تنظیمات حساب
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<LogoutIcon />
خروج
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
{/* دسترسی سریع: کارهای پرتکرار با یک کلیک، بالای همهٔ گزارشها. */}
<section aria-label="دسترسی سریع" className="grid grid-cols-2 gap-3 md:grid-cols-4">
{QUICK.map((item) => (
<Button
key={item.title}
variant="outline"
className="h-auto justify-start gap-3 rounded-xl bg-card! p-4 text-start shadow-none hover:border-input"
>
<span aria-hidden className={`flex size-11 shrink-0 items-center justify-center rounded-xl [&_svg]:size-5 ${item.tone}`}>
{item.icon}
</span>
<span className="flex min-w-0 flex-col gap-0.5">
<span className="truncate text-sm font-semibold">{item.title}</span>
<span className="truncate text-xs text-muted-foreground">{item.hint}</span>
</span>
</Button>
))}
</section>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_22rem]">
<Card>
{/* کنترلها روبهروی عنوان؛ در موبایل زیرِ آن میپیچند. */}
<CardHeader className="flex flex-wrap items-center justify-between gap-3">
<CardTitle>فروش و سفارشها</CardTitle>
<div className="flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2">
<Checkbox id="show-revenue" checked={showRevenue} onCheckedChange={(on) => setShowRevenue(on === true)} />
<Label htmlFor="show-revenue">فروش</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="show-orders" checked={showOrders} onCheckedChange={(on) => setShowOrders(on === true)} />
<Label htmlFor="show-orders">سفارش</Label>
</div>
<Select value={period} onValueChange={(next) => setPeriod(next as string)}>
<SelectTrigger size="sm" aria-label="بازهٔ زمانی" className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">۷ روز</SelectItem>
<SelectItem value="30">۳۰ روز</SelectItem>
<SelectItem value="90">۹۰ روز</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent>
<ComboChart
className="h-80"
data={sales.map((row) => ({
...row,
revenue: showRevenue ? row.revenue : null,
orders: showOrders ? row.orders : null,
}))}
categoryKey="label"
bar={{ key: "revenue", label: "فروش (میلیون تومان)", color: "var(--chart-1)" }}
barFormatter={(value) => formatNumber(value)}
line={{ key: "orders", label: "سفارش", color: "var(--chart-4)" }}
/>
</CardContent>
</Card>
<section className="flex flex-col gap-4" aria-label="خلاصه">
<StatTile>
<StatTileHeader>
<StatTileLabel className="text-base text-foreground">قیفِ خرید</StatTileLabel>
<StatTileAction aria-label="جزئیات قیف خرید" />
</StatTileHeader>
<StatTileList
items={[
{ label: "بازدید", value: 100, display: "٪۱۰۰" },
{ label: "دیدنِ محصول", value: 74, display: "٪۷۴" },
{ label: "افزودن به سبد", value: 62, display: "٪۶۲" },
{ label: "خریدِ موفق", value: 21, display: "٪۲۱" },
]}
/>
</StatTile>
<StatTile className="gap-3">
<StatTileHeader>
<StatTileLabel className="text-base text-foreground">مشتریانِ تازه</StatTileLabel>
<Button variant="ghost" className="ms-auto">
همه
</Button>
</StatTileHeader>
<ul className="flex flex-col gap-1">
{CUSTOMERS.map((customer) => (
<li key={customer.name} className="flex items-center gap-3 rounded-xl p-2 hover:bg-muted">
<Avatar size="sm" alt={customer.name} icon={<AvatarGenderIcon gender={customer.gender} />} className="ring-1 ring-border" />
<div className="flex min-w-0 flex-1 flex-col gap-1">
<span className="truncate text-sm font-medium">{customer.name}</span>
<span className="text-xs text-muted-foreground">
{toFaDigits(customer.orders)} سفارش · {customer.spent}
</span>
</div>
<Button variant="ghost" size="icon" aria-label={`پیام به ${customer.name}`}>
<EnvelopeIcon />
</Button>
</li>
))}
</ul>
</StatTile>
</section>
</div>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_22rem]">
<Card>
<CardHeader className="flex flex-wrap items-center justify-between gap-3">
<CardTitle>سفارشها</CardTitle>
{/* جستوجو کنارِ فیلتر، روبهروی عنوان؛ ابزارهای جدول یکجا. */}
<div className="flex w-full items-center gap-2 sm:w-auto">
<SearchField
aria-label="جستوجوی سفارش"
placeholder="شماره سفارش یا نام مشتری"
value={query}
onValueChange={(value) => {
setQuery(value)
setPage(1)
}}
className="min-w-0 flex-1 sm:w-64"
/>
<Button variant="outline" size="icon" aria-label="فیلتر سفارشها">
<FilterIcon />
</Button>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="overflow-hidden rounded-lg border border-border">
<Table stackOnMobile className="min-w-[36rem]">
<TableHeader className="bg-muted">
<TableRow>
<TableHead className="w-10">
<Checkbox
aria-label="انتخاب همه"
checked={allSelected}
onCheckedChange={(on) => setSelected(on === true ? orders.map((order) => order.id) : [])}
/>
</TableHead>
<TableHead>شماره</TableHead>
<TableHead>مشتری</TableHead>
<TableHead>تاریخ</TableHead>
<TableHead className="text-end">مبلغ</TableHead>
<TableHead>وضعیت</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{orders.map((order) => (
<TableRow key={order.id} data-state={selected.includes(order.id) ? "selected" : undefined}>
<TableCell>
<Checkbox
aria-label={`انتخاب سفارش ${toFaDigits(order.id)}`}
checked={selected.includes(order.id)}
onCheckedChange={(on) =>
setSelected((list) => (on === true ? [...list, order.id] : list.filter((id) => id !== order.id)))
}
/>
</TableCell>
<TableCell className="font-medium">
<bdi dir="ltr">#{toFaDigits(order.id)}</bdi>
</TableCell>
<TableCell>{order.customer}</TableCell>
<TableCell className="text-muted-foreground">{formatJalali(new Date(order.date))}</TableCell>
<TableCell className="text-end tabular-nums">{formatCurrency(order.total)}</TableCell>
<TableCell>
<Badge variant="soft" color={STATUS[order.status].color}>
{STATUS[order.status].text}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex flex-wrap items-center justify-between gap-3">
<span className="text-sm text-muted-foreground">
{toFaDigits(filtered.length)} سفارش · صفحهٔ {toFaDigits(currentPage)} از {toFaDigits(pageCount)}
</span>
<Pagination className="mx-0 w-auto">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
aria-disabled={currentPage === 1}
onClick={(event) => {
event.preventDefault()
setPage(Math.max(1, currentPage - 1))
}}
/>
</PaginationItem>
{Array.from({ length: pageCount }, (_, index) => index + 1).map((number) => (
<PaginationItem key={number}>
<PaginationLink
href="#"
isActive={number === currentPage}
onClick={(event) => {
event.preventDefault()
setPage(number)
}}
>
{toFaDigits(number)}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
aria-disabled={currentPage === pageCount}
onClick={(event) => {
event.preventDefault()
setPage(Math.min(pageCount, currentPage + 1))
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>اعلانها</CardTitle>
<CardAction>
<Badge variant="soft" color="destructive">
{toFaDigits(notifications.length)} تازه
</Badge>
</CardAction>
</CardHeader>
<CardContent className="flex flex-col gap-3">
{notifications.length === 0 && (
<p className="py-6 text-center text-sm text-muted-foreground">اعلانِ تازهای نیست.</p>
)}
{notifications.map((item) => (
<div key={item.id} className="flex items-start gap-3 rounded-lg border border-border bg-card p-3">
<span aria-hidden className={`flex size-9 shrink-0 items-center justify-center rounded-full [&_svg]:size-4 ${item.tone}`}>
{item.icon}
</span>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<span className="text-sm font-semibold">{item.title}</span>
<span className="truncate text-xs text-muted-foreground">{item.body}</span>
<span className="text-xs text-muted-foreground">{formatRelativeTime(item.time)}</span>
</div>
<Button
variant="ghost"
size="icon"
aria-label={`بستن اعلان ${item.title}`}
onClick={() => setNotifications((list) => list.filter((entry) => entry.id !== item.id))}
>
<XIcon />
</Button>
</div>
))}
<Button variant="outline" fullWidth>
مشاهدهٔ همه
</Button>
</CardContent>
</Card>
</div>
</SidebarInset>
</SidebarProvider>
</ControlSizeProvider>
)
}
/* ------------------------------------------------------------------ اجزا */
function DashboardSidebar() {
return (
<Sidebar collapsible="icon">
<SidebarHeader className="gap-3 p-3 group-data-[collapsible=icon]:p-2">
<SidebarBrand logo={<DigLogo />} title="فروشگاه دیگ" subtitle="پنل مدیریت" />
<SidebarSearch placeholder="جستوجو در منو…" aria-label="جستوجو در منو" />
</SidebarHeader>
<SidebarContent className="gap-0 pt-4">
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<NavItem icon={<DashboardIcon />} active>
پیشخوان
</NavItem>
<NavItem icon={<ReceiptIcon />} badge={toFaDigits(12)}>
سفارشها
</NavItem>
<NavItem icon={<ShopIcon />}>محصولات</NavItem>
<NavItem icon={<UsersIcon />}>مشتریان</NavItem>
{/* زیرمنو جمعشونده و پیشفرض بسته؛ همیشهباز منو را شلوغ میکرد. */}
<SidebarMenuItem>
<Collapsible>
<CollapsibleTrigger asChild>
<SidebarMenuButton className="group/collapsible">
<BarChartIcon />
<span>تحلیلها</span>
<Badge variant="soft" color="violet" size="sm" className="ms-auto">
ویژه
</Badge>
<ChevronDownIcon className="size-4! transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{["گزارش فروش", "عملکرد محصولات", "رفتار مشتریان"].map((title) => (
<SidebarMenuSubItem key={title}>
<SidebarMenuSubButton href="#">{title}</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<NavItem icon={<BellIcon />} badge={toFaDigits(6)} badgeTone="destructive">
هشدار موجودی
</NavItem>
<NavItem icon={<ShieldCheckIcon />}>نقشها و دسترسیها</NavItem>
<NavItem icon={<TicketIcon />}>پشتیبانی</NavItem>
<NavItem icon={<GuideIcon />}>راهنما</NavItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="gap-4 px-3 pt-3 pb-9 group-data-[collapsible=icon]:p-2">
<ThemeSwitch />
<p className="text-center text-xs text-placeholder group-data-[collapsible=icon]:hidden">نسخهٔ ۲٫۴٫۰ · بهروزرسانی ۱۲ مهر</p>
</SidebarFooter>
</Sidebar>
)
}
/**
* سوییچِ روشن/تیره در پایینِ نوار. کلاسِ dark روی <html> را عوض میکند؛ در پروژهای
* که next-themes دارد، بهجایش setTheme را صدا بزنید.
*/
function ThemeSwitch() {
return (
<>
<ThemeSegmented />
{/* حالتِ جمع: یک دکمهٔ آیکونی که بینِ دو پوسته جابهجا میکند. */}
<ThemeIconToggle />
</>
)
}
function useHtmlTheme() {
return React.useSyncExternalStore(
(notify) => {
const observer = new MutationObserver(notify)
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
return () => observer.disconnect()
},
() => (document.documentElement.classList.contains("dark") ? "dark" : "light"),
() => "light"
)
}
function applyTheme(next: "light" | "dark") {
document.documentElement.classList.toggle("dark", next === "dark")
document.documentElement.classList.toggle("light", next === "light")
}
function ThemeIconToggle() {
const theme = useHtmlTheme()
return (
<SidebarMenu className="hidden group-data-[collapsible=icon]:flex">
<SidebarMenuItem>
<SidebarMenuButton
tooltip={theme === "dark" ? "پوستهٔ روشن" : "پوستهٔ تیره"}
onClick={() => applyTheme(theme === "dark" ? "light" : "dark")}
>
{theme === "dark" ? <SunIcon /> : <MoonIcon />}
<span>{theme === "dark" ? "پوستهٔ روشن" : "پوستهٔ تیره"}</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
)
}
function ThemeSegmented() {
const theme = useHtmlTheme()
return (
<ToggleGroup
type="single"
variant="segmented"
fullWidth
value={theme}
onValueChange={(value) => {
if (!value) return
applyTheme(value as "light" | "dark")
}}
aria-label="پوستهٔ رنگی"
className="group-data-[collapsible=icon]:hidden"
>
<ToggleGroupItem value="light" aria-label="پوستهٔ روشن">
<SunIcon />
</ToggleGroupItem>
<ToggleGroupItem value="dark" aria-label="پوستهٔ تیره">
<MoonIcon />
</ToggleGroupItem>
</ToggleGroup>
)
}
function NavItem({
icon,
active = false,
badge,
badgeTone,
children,
}: {
icon: React.ReactNode
active?: boolean
badge?: string
badgeTone?: "destructive"
children: React.ReactNode
}) {
return (
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={active} tooltip={typeof children === "string" ? children : undefined}>
<a href="#">
{icon}
<span>{children}</span>
</a>
</SidebarMenuButton>
{badge && (
<SidebarMenuBadge tone={badgeTone === "destructive" ? "destructive" : "default"}>
{badge}
</SidebarMenuBadge>
)}
</SidebarMenuItem>
)
}
/** نمودارِ خطی کوچک برای کارتِ آمار؛ SVGِ سبک بهجای یک نمونهٔ کاملِ نمودار. */