مرکزِ اعلانها
پیشنمایش تمامصفحهمرکزِ اعلانها با نوعها در ستونِ کنار و شمارِ خواندهنشده، انتخابِ چندتایی با انتخابِ همه و نوارِ اقدامِ گروهی (خوانده شد، بایگانی).
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: buttoncheckboxcontrol-sizeiconspersiantoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
notifications-inbox.tsx
"use client"
import * as React from "react"
import { BellIcon, ReceiptIcon, TicketIcon, TruckIcon, UsersIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { toast } from "@/components/ui/toast"
import { toFaDigits } from "@/lib/persian"
type Kind = "orders" | "finance" | "team" | "support"
const KINDS: { value: Kind | "all"; label: string; icon: React.ReactNode }[] = [
{ value: "all", label: "همه", icon: <BellIcon /> },
{ value: "orders", label: "سفارشها", icon: <TruckIcon /> },
{ value: "finance", label: "مالی", icon: <ReceiptIcon /> },
{ value: "team", label: "تیم", icon: <UsersIcon /> },
{ value: "support", label: "پشتیبانی", icon: <TicketIcon /> },
]
const ITEMS: { id: string; kind: Kind; title: string; time: string; read: boolean }[] = [
{ id: "1", kind: "orders", title: "سفارشِ ۸۸۲۱۰۴۲ ارسال شد", time: "۱۰ دقیقه پیش", read: false },
{ id: "2", kind: "team", title: "سارا محمدی به تیمِ فروش پیوست", time: "۲ ساعت پیش", read: false },
{ id: "3", kind: "finance", title: "پرداختِ فاکتورِ ۱۴۰۵-۰۲۳۱ انجام شد", time: "دیروز", read: true },
{ id: "4", kind: "support", title: "درخواستِ ۵۰۲۱ پاسخ داده شد", time: "دوشنبه", read: true },
{ id: "5", kind: "orders", title: "سفارشِ ۸۸۲۱۰۳۹ مرجوع شد", time: "یکشنبه", read: false },
]
/**
* مرکزِ اعلانها برای پنل: فهرستِ نوعها در ستونِ کنار با شمارِ خواندهنشده، انتخابِ چندتایی
* با «انتخابِ همه» و نوارِ اقدامِ گروهی (خوانده شد، بایگانی) که فقط با انتخاب ظاهر میشود.
*/
export default function NotificationsInboxBlock() {
const [kind, setKind] = React.useState<Kind | "all">("all")
const [items, setItems] = React.useState(ITEMS)
const [selected, setSelected] = React.useState<string[]>([])
const visible = items.filter((item) => kind === "all" || item.kind === kind)
const allChecked = visible.length > 0 && visible.every((item) => selected.includes(item.id))
const unreadOf = (value: Kind | "all") => items.filter((item) => !item.read && (value === "all" || item.kind === value)).length
function act(action: "read" | "archive") {
setItems((list) => (action === "archive" ? list.filter((item) => !selected.includes(item.id)) : list.map((item) => (selected.includes(item.id) ? { ...item, read: true } : item))))
toast.success(action === "archive" ? `${toFaDigits(selected.length)} اعلان بایگانی شد.` : "خوانده شد.")
setSelected([])
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-5xl flex-col gap-8 p-4 sm:p-6">
<h1 className="text-2xl font-bold">اعلانها</h1>
<div className="grid grid-cols-1 items-start gap-8 md:grid-cols-[14rem_minmax(0,1fr)]">
<nav aria-label="نوعِ اعلان" className="flex gap-1 overflow-x-auto rounded-xl bg-muted p-1.5 md:flex-col">
{KINDS.map((item) => (
<Button
key={item.value}
variant="ghost"
aria-current={item.value === kind ? "true" : undefined}
onClick={() => {
setKind(item.value)
setSelected([])
}}
className={`shrink-0 justify-start gap-3 font-normal ${item.value === kind ? "bg-card font-semibold shadow-control hover:bg-card" : "hover:bg-card"}`}
>
<span aria-hidden className="text-muted-foreground [&>svg]:size-5">
{item.icon}
</span>
<span className="flex-1 text-start">{item.label}</span>
{unreadOf(item.value) > 0 && <span className="text-xs font-semibold text-brand-secondary tabular-nums">{toFaDigits(unreadOf(item.value))}</span>}
</Button>
))}
</nav>
<section className="flex flex-col gap-3" aria-label="فهرستِ اعلانها">
<div className={`flex min-h-14 flex-wrap items-center gap-3 rounded-xl px-4 py-2 ${selected.length ? "bg-primary-subtle" : "bg-subtle"}`} aria-live="polite">
<Checkbox
checked={allChecked}
onCheckedChange={(checked) => setSelected(checked ? visible.map((item) => item.id) : [])}
aria-label="انتخابِ همه"
disabled={visible.length === 0}
/>
<span className="flex-1 text-sm">{selected.length ? `${toFaDigits(selected.length)} مورد انتخاب شده` : `${toFaDigits(visible.length)} اعلان`}</span>
{selected.length > 0 && (
<div className="flex gap-2">
<Button variant="outline" className="bg-card" onClick={() => act("read")}>
خوانده شد
</Button>
<Button variant="outline" className="bg-card" onClick={() => act("archive")}>
بایگانی
</Button>
</div>
)}
</div>
{visible.length === 0 ? (
<p className="rounded-xl border border-dashed border-border p-10 text-center text-muted-foreground">در این دسته اعلانی نیست.</p>
) : (
<ul className="flex flex-col divide-y divide-border rounded-xl border border-border bg-card">
{visible.map((item) => (
<li key={item.id} className="flex min-h-16 items-center gap-3 px-4 py-3">
<Checkbox
checked={selected.includes(item.id)}
onCheckedChange={(checked) => setSelected((list) => (checked ? [...list, item.id] : list.filter((id) => id !== item.id)))}
aria-label={`انتخابِ ${item.title}`}
/>
<span aria-hidden className="text-muted-foreground [&>svg]:size-5">
{KINDS.find((entry) => entry.value === item.kind)?.icon}
</span>
<span className="flex min-w-0 flex-1 flex-col">
<span className={`truncate ${item.read ? "" : "font-bold"}`}>{item.title}</span>
<span className="text-xs text-muted-foreground">{item.time}</span>
</span>
{!item.read && <span aria-label="تازه" className="size-2.5 shrink-0 rounded-full bg-[var(--dig-blue-500)]" />}
</li>
))}
</ul>
)}
</section>
</div>
</main>
</ControlSizeProvider>
)
}