گزارش فعالیت
پیشنمایش تمامصفحهخطِ زمانیِ روزبهروز با عکسِ فرد و آیکونِ رنگیِ نوعِ کار، «چه کسی، چه کاری، روی چه چیزی» با جزئیاتِ تغییر، فیلترِ نوع با تب، جستوجو و خروجیِ اکسل.
۱. نصب کامپوننتها
این بلاک از ۷ آیتم دیگ استفاده میکند: avatarbuttoncontrol-sizeiconspersiansearch-fieldtoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add 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/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.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/button.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/search-field.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/button.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/search-field.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/button.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/search-field.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
activity-log.tsx
"use client"
import * as React from "react"
import { DownloadIcon, EditIcon, FileTextIcon, ShieldCheckIcon, TrashIcon, UsersIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { SearchField } from "@/components/ui/search-field"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { toFaDigits, toSearchKey } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
type Kind = "edit" | "create" | "delete" | "access" | "security"
const KIND: Record<Kind, { icon: React.ReactNode; tone: string }> = {
create: { icon: <FileTextIcon />, tone: "green" },
edit: { icon: <EditIcon />, tone: "blue" },
delete: { icon: <TrashIcon />, tone: "red" },
access: { icon: <UsersIcon />, tone: "violet" },
security: { icon: <ShieldCheckIcon />, tone: "amber" },
}
const DAYS: { day: string; events: { who: string; photo: StockImageName; kind: Kind; action: string; target: string; time: string; detail?: string }[] }[] = [
{
day: "امروز",
events: [
{ who: "نیما رضایی", photo: "personMan1", kind: "edit", action: "مبلغِ فاکتور را عوض کرد", target: "فاکتورِ ۱۴۰۵-۰۷۱۸", time: "۱۰:۴۲", detail: "۴۹۰٬۰۰۰ ← ۵۲۰٬۰۰۰ تومان" },
{ who: "مهسا کریمی", photo: "personWoman1", kind: "create", action: "مشتری تازه اضافه کرد", target: "کافه رست", time: "۰۹:۱۵" },
{ who: "سارا محمدی", photo: "personWoman3", kind: "security", action: "ورودِ دومرحلهای را فعال کرد", target: "حسابِ خودش", time: "۰۸:۰۲" },
],
},
{
day: "دیروز",
events: [
{ who: "سارا محمدی", photo: "personWoman3", kind: "access", action: "نقشِ عضو را تغییر داد", target: "لیلا احمدی", time: "۱۷:۳۰", detail: "عضو ← بیننده" },
{ who: "آرش نوری", photo: "personMan2", kind: "delete", action: "پیشنویس را حذف کرد", target: "فاکتورِ پیشنویسِ ۱۲", time: "۱۴:۱۰" },
{ who: "نیما رضایی", photo: "personMan1", kind: "create", action: "کلیدِ API ساخت", target: "سرورِ فروشگاه", time: "۱۱:۰۵" },
],
},
]
const FILTERS: { value: "all" | Kind; label: string }[] = [
{ value: "all", label: "همه" },
{ value: "edit", label: "ویرایش" },
{ value: "create", label: "ساخت" },
{ value: "delete", label: "حذف" },
{ value: "access", label: "دسترسی" },
]
/**
* گزارشِ فعالیت: خطِ زمانی روزبهروز با عکسِ فرد، آیکونِ رنگی نوعِ کار، «چه کسی، چه
* کاری، روی چه چیزی» و جزئیاتِ تغییر؛ فیلترِ نوع با تب، جستوجو و خروجی اکسل.
*/
export default function ActivityLogBlock() {
const [filter, setFilter] = React.useState<"all" | Kind>("all")
const [query, setQuery] = React.useState("")
const key = toSearchKey(query)
const days = DAYS.map((day) => ({
...day,
events: day.events.filter((event) => (filter === "all" || event.kind === filter) && toSearchKey(event.who + event.action + event.target).includes(key)),
})).filter((day) => day.events.length > 0)
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-4xl flex-col gap-8 p-4 sm:p-6">
<header className="flex flex-wrap items-end justify-between gap-x-4 gap-y-8">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">گزارش فعالیت</h1>
<p className="text-muted-foreground">هر تغییری که اعضای تیم در حساب دادهاند.</p>
</div>
<Button variant="outline">
<DownloadIcon />
خروجی اکسل
</Button>
</header>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="max-w-full overflow-x-auto">
<ToggleGroup type="single" variant="segmented" value={filter} onValueChange={(value) => value && setFilter(value as "all" | Kind)} aria-label="نوعِ فعالیت">
{FILTERS.map((item) => (
<ToggleGroupItem key={item.value} value={item.value} className="whitespace-nowrap">
{item.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<SearchField aria-label="جستوجو در فعالیتها" placeholder="نام، کار یا مورد" value={query} onValueChange={setQuery} className="w-full sm:w-64" />
</div>
{days.length === 0 ? (
<p className="rounded-xl border border-dashed border-border p-10 text-center text-muted-foreground">فعالیتی با این فیلتر نیست.</p>
) : (
<div className="flex flex-col gap-10">
{days.map((day) => (
<section key={day.day} aria-label={day.day} className="flex flex-col gap-4">
<h2 className="text-sm font-semibold text-muted-foreground">
{day.day} · {toFaDigits(day.events.length)} فعالیت
</h2>
<ol className="flex flex-col">
{day.events.map((event, index) => (
<li key={`${event.time}-${event.target}`} className="relative flex gap-4 pb-6 last:pb-0">
{/* خطِ اتصال بینِ رویدادهای یک روز */}
{index < day.events.length - 1 && <span aria-hidden className="absolute start-6 top-14 bottom-1 w-px bg-border" />}
<span className="relative shrink-0">
<Avatar alt={event.who} src={stockImages[event.photo].src} size="lg" />
<span
aria-hidden
className="absolute -end-1 -bottom-1 flex size-5 items-center justify-center rounded-full ring-2 ring-background [&>svg]:size-3"
style={{ background: `color-mix(in oklab, var(--dig-${KIND[event.kind].tone}-500) 16%, var(--background))`, color: `var(--dig-${KIND[event.kind].tone}-600)` }}
>
{KIND[event.kind].icon}
</span>
</span>
<div className="flex min-w-0 flex-1 flex-col gap-1.5 pt-1">
<p className="text-sm leading-6">
<span className="font-semibold">{event.who}</span> {event.action}: <span className="font-medium">{event.target}</span>
</p>
{event.detail && <span className="self-start rounded-md bg-subtle px-2.5 py-1 text-xs text-muted-foreground">{event.detail}</span>}
</div>
<time className="shrink-0 pt-1 text-xs text-muted-foreground tabular-nums">{event.time}</time>
</li>
))}
</ol>
</section>
))}
</div>
)}
</main>
</ControlSizeProvider>
)
}