بلاک‌ها · حساب کاربری

مرکزِ اعلان‌ها

پیش‌نمایش تمام‌صفحه

مرکزِ اعلان‌ها با نوع‌ها در ستونِ کنار و شمارِ خوانده‌نشده، انتخابِ چندتایی با انتخابِ همه و نوارِ اقدامِ گروهی (خوانده شد، بایگانی).

۱. نصب کامپوننت‌ها

این بلاک از ۶ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}