بلاک‌ها · موبایل

اعلان‌ها در اپ: سگمنتِ همه/خوانده‌نشده، گروه‌بندیِ زمانی، ردیف‌های لمسی با عکس یا آیکون، نقطه و زمینهٔ خوانده‌نشده و «همه خوانده شد».

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

این بلاک از ۷ آیتم دیگ استفاده می‌کند: app-baravatarbuttoncontrol-sizeempty-stateiconstoggle-group

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/toggle-group.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/toggle-group.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/toggle-group.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

mobile-notifications.tsx
"use client"

import * as React from "react"

import { ChevronRightIcon, ReceiptIcon, TicketIcon, TruckIcon } from "@/components/icons"
import { AppBar, AppBarActions, AppBarLeading, AppBarTitle } from "@/components/ui/app-bar"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { EmptyState, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { stockImages } from "@/lib/stock-images"

type Item = { id: string; group: "امروز" | "این هفته"; title: string; text: string; time: string; read: boolean; icon?: React.ReactNode; photo?: string }

const ITEMS: Item[] = [
  { id: "1", group: "امروز", title: "سفارشت در راه است", text: "پیک تا ساعتِ ۱۸ می‌رسد.", time: "۱۰ دقیقه پیش", read: false, icon: <TruckIcon /> },
  { id: "2", group: "امروز", title: "سارا محمدی", text: "برایت ۲۰۰ هزار تومان فرستاد.", time: "۲ ساعت پیش", read: false, photo: stockImages.personWoman2.src },
  { id: "3", group: "این هفته", title: "پاسخِ پشتیبانی", text: "درخواستِ ۵۰۲۱ پاسخ داده شد.", time: "دوشنبه", read: true, icon: <TicketIcon /> },
  { id: "4", group: "این هفته", title: "قبضِ برق", text: "تا پنجشنبه مهلتِ پرداخت داری.", time: "یکشنبه", read: true, icon: <ReceiptIcon /> },
]

/**
 * اعلان‌های اپ: سگمنتِ «همه/خوانده‌نشده»، گروه‌بندی زمانی، ردیف‌های کاملاً لمسی با عکس یا
 * آیکون، نقطهٔ خوانده‌نشده و زمینهٔ برندِ کم‌رنگ؛ «همه خوانده شد» در نوارِ بالا.
 */
export default function MobileNotificationsBlock() {
  const [items, setItems] = React.useState(ITEMS)
  const [filter, setFilter] = React.useState("all")
  const visible = items.filter((item) => filter === "all" || !item.read)
  const groups = ["امروز", "این هفته"] as const

  return (
    <ControlSizeProvider size="xl" asChild>
      <div className="mx-auto flex min-h-svh w-full max-w-md flex-col bg-background">
        <AppBar sticky>
          <AppBarLeading>
            <Button variant="ghost" size="icon" aria-label="بازگشت">
              <ChevronRightIcon className="ltr:rotate-180" />
            </Button>
          </AppBarLeading>
          <AppBarTitle>اعلان‌ها</AppBarTitle>
          <AppBarActions>
            <Button variant="ghost" disabled={items.every((item) => item.read)} onClick={() => setItems((list) => list.map((item) => ({ ...item, read: true })))}>
              همه خوانده شد
            </Button>
          </AppBarActions>
        </AppBar>

        <main className="flex flex-1 flex-col gap-4 p-4">
          <ToggleGroup type="single" variant="segmented" value={filter} onValueChange={(value) => value && setFilter(value as string)} aria-label="فیلترِ اعلان‌ها" className="w-full">
            <ToggleGroupItem value="all" className="flex-1">
              همه
            </ToggleGroupItem>
            <ToggleGroupItem value="unread" className="flex-1">
              خوانده‌نشده
            </ToggleGroupItem>
          </ToggleGroup>

          {visible.length === 0 ? (
            <EmptyState className="flex-1">
              <EmptyStateTitle>همه را خوانده‌ای</EmptyStateTitle>
              <EmptyStateDescription>اعلانِ تازه که بیاید همین‌جا می‌بینی.</EmptyStateDescription>
            </EmptyState>
          ) : (
            groups.map((group) => {
              const rows = visible.filter((item) => item.group === group)
              if (!rows.length) return null
              return (
                <section key={group} aria-labelledby={`mn-${group}`} className="flex flex-col gap-1">
                  <h2 id={`mn-${group}`} className="px-1 py-2 text-sm font-medium text-muted-foreground">
                    {group}
                  </h2>
                  <ul className="flex flex-col gap-1">
                    {rows.map((item) => (
                      <li key={item.id}>
                        <a
                          href="#"
                          onClick={() => setItems((list) => list.map((entry) => (entry.id === item.id ? { ...entry, read: true } : entry)))}
                          className={`flex min-h-16 items-start gap-3 rounded-xl p-3 ${item.read ? "hover:bg-subtle" : "bg-primary-subtle"}`}
                        >
                          {item.photo ? (
                            <Avatar alt="" src={item.photo} />
                          ) : (
                            <span aria-hidden className="flex size-10 shrink-0 items-center justify-center rounded-full bg-subtle text-brand-secondary [&>svg]:size-5">
                              {item.icon}
                            </span>
                          )}
                          <span className="flex flex-1 flex-col gap-0.5">
                            <span className={item.read ? "font-medium" : "font-bold"}>{item.title}</span>
                            <span className="text-sm leading-6 text-muted-foreground">{item.text}</span>
                            <span className="text-xs text-muted-foreground">{item.time}</span>
                          </span>
                          {!item.read && <span aria-label="خوانده‌نشده" className="mt-2 size-2.5 shrink-0 rounded-full bg-[var(--dig-blue-500)]" />}
                        </a>
                      </li>
                    ))}
                  </ul>
                </section>
              )
            })
          )}
        </main>
      </div>
    </ControlSizeProvider>
  )
}