معرفیِ وسطچین، مقالهٔ ویژهٔ افقی، فیلترِ دسته با جستوجو، گریدِ کارتِ مقاله با نویسنده و تاریخ، حالتِ خالی و خبرنامهٔ رنگی.
۱. نصب کامپوننتها
این بلاک از ۱۲ آیتم دیگ استفاده میکند: article-cardavatarbadgebuttoncontrol-sizeempty-stateiconsinputpersiansearch-fieldtoasttoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/article-card.json 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/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/article-card.json 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/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/article-card.json 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/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/article-card.json 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/control-size.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/input.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toast.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
blog.tsx
"use client"
import * as React from "react"
import { AvatarGenderIcon } from "@/components/icons"
import {
ArticleCard,
ArticleCardBody,
ArticleCardCategory,
ArticleCardExcerpt,
ArticleCardGrid,
ArticleCardMedia,
ArticleCardMeta,
ArticleCardTitle,
} from "@/components/ui/article-card"
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 { EmptyState, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { Input } from "@/components/ui/input"
import { SearchField } from "@/components/ui/search-field"
import { toast } from "@/components/ui/toast"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatJalali, isValidEmail, toFaDigits, toSearchKey } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"
type Post = {
id: string
category: string
title: string
excerpt: string
author: string
gender: "male" | "female"
date: string
minutes: number
cover: StockImageName
}
// عکسِ هر نویسنده از کتابخانهٔ تصویر؛ آیکونِ جنسیت فقط وقتی عکس نرسید.
const AUTHOR_PHOTOS: Record<string, StockImageName> = {
"سارا محمدی": "personWoman2",
"آرش نوری": "personMan1",
"مهسا کریمی": "personWoman1",
"نیما رضایی": "personMan2",
"لیلا احمدی": "personWoman3",
}
const CATEGORIES = ["همه", "محصول", "مالی شخصی", "اخبار", "تجربهٔ مشتری"]
const FEATURED: Post = {
id: "f",
category: "محصول",
title: "تازههای حسابیار: ابزارهای هوشمندتر برای پولِ هوشمندتر",
excerpt: "گزارشِ خودکارِ ماهانه، دستهبندی هوشمندِ هزینهها و یادآورِ قسط؛ همه در یک بهروزرسانی. اینجا توضیح دادهایم هر کدام چطور وقتتان را آزاد میکند.",
author: "سارا محمدی",
gender: "female",
date: "2026-09-28",
minutes: 7,
cover: "coverMinimalRoom",
}
const POSTS: Post[] = [
{ id: "1", category: "مالی شخصی", title: "قانونِ ۵۰/۳۰/۲۰ برای بودجهبندی ماهانه", excerpt: "یک روشِ ساده که بدونِ اکسل هم کار میکند؛ از حقوقِ این ماه شروع کنید.", author: "آرش نوری", gender: "male", date: "2026-09-24", minutes: 5, cover: "coverCalculator" },
{ id: "2", category: "محصول", title: "دستهبندی خودکارِ هزینهها چطور کار میکند؟", excerpt: "پشتِ صحنهٔ قابلیتی که هر تراکنش را خودش در دستهٔ درست میگذارد.", author: "مهسا کریمی", gender: "female", date: "2026-09-22", minutes: 4, cover: "coverWorkspace" },
{ id: "3", category: "اخبار", title: "مالیاتِ کسبوکارهای آنلاین در سالِ جدید", excerpt: "هر چه دربارهٔ تغییراتِ تازه باید بدانید، به زبانِ ساده و با مثال.", author: "نیما رضایی", gender: "male", date: "2026-09-20", minutes: 8, cover: "coverOfficeSupplies" },
{ id: "4", category: "تجربهٔ مشتری", title: "کافه رست چطور هزینههایش را ۲۰٪ کم کرد", excerpt: "روایتِ شش ماه استفاده از گزارشها در یک کافهٔ زنجیرهای.", author: "لیلا احمدی", gender: "female", date: "2026-09-18", minutes: 6, cover: "coverNotebook" },
{ id: "5", category: "مالی شخصی", title: "پساندازِ اضطراری: چقدر کافی است؟", excerpt: "عددِ درست برای شما به سه چیز بستگی دارد؛ با این حساب پیدایش کنید.", author: "آرش نوری", gender: "male", date: "2026-09-15", minutes: 5, cover: "coverFinanceDesk" },
{ id: "6", category: "اخبار", title: "کارمزدِ جدیدِ کارتبهکارت از مهر", excerpt: "جدولِ کارمزدها و راههایی که کمتر بپردازید.", author: "نیما رضایی", gender: "male", date: "2026-09-12", minutes: 3, cover: "coverMinimalRoom" },
]
function PostMeta({ post }: { post: Post }) {
return (
<ArticleCardMeta>
<span className="flex items-center gap-2">
<Avatar size="sm" alt={post.author} src={AUTHOR_PHOTOS[post.author] && stockImages[AUTHOR_PHOTOS[post.author]].src} icon={<AvatarGenderIcon gender={post.gender} />} />
<span className="font-medium text-foreground">{post.author}</span>
</span>
<span>
{formatJalali(new Date(post.date), "short")} · {toFaDigits(post.minutes)} دقیقه
</span>
</ArticleCardMeta>
)
}
/** صفحهٔ بلاگ: معرفی، مقالهٔ ویژه، فیلترِ دسته و جستوجو، گریدِ مقالهها و خبرنامه. */
export default function BlogBlock() {
const [category, setCategory] = React.useState("همه")
const [query, setQuery] = React.useState("")
const [email, setEmail] = React.useState("")
const [emailError, setEmailError] = React.useState("")
const visible = POSTS.filter(
(post) => (category === "همه" || post.category === category) && toSearchKey(post.title + post.excerpt).includes(toSearchKey(query))
)
const subscribe = (event: React.FormEvent) => {
event.preventDefault()
if (!isValidEmail(email)) {
setEmailError("یک ایمیلِ درست وارد کنید.")
return
}
setEmailError("")
setEmail("")
toast.success("عضو خبرنامه شدید؛ اولین نامه پنجشنبه میرسد.")
}
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-6xl flex-col gap-24 px-4 py-16 sm:px-6">
{/* معرفی: برچسب، تیتر و توضیح، وسطچین. */}
<header className="mx-auto flex max-w-2xl flex-col items-center gap-4 text-center">
<Badge variant="soft">مجلهٔ حسابیار</Badge>
<h1 className="text-4xl leading-snug font-black text-balance sm:text-5xl sm:leading-snug">ایدهها و روندهایی که آیندهٔ پول را میسازند</h1>
<p className="text-lg leading-8 text-muted-foreground">تازههای محصول، راهنمای مالی شخصی و تجربهٔ کسبوکارهایی که با ما رشد کردهاند.</p>
</header>
<ArticleCard orientation="horizontal" size="lg">
<ArticleCardMedia className="sm:w-1/2">
{/* eslint-disable-next-line @next/next/no-img-element -- کاورِ تزئینی ثابت */}
<img src={stockImages[FEATURED.cover].src} alt="" />
</ArticleCardMedia>
<ArticleCardBody className="gap-3">
<ArticleCardCategory>{FEATURED.category}</ArticleCardCategory>
<ArticleCardTitle href="#">{FEATURED.title}</ArticleCardTitle>
<ArticleCardExcerpt className="line-clamp-3">{FEATURED.excerpt}</ArticleCardExcerpt>
<PostMeta post={FEATURED} />
</ArticleCardBody>
</ArticleCard>
<section aria-labelledby="blog-latest" className="flex flex-col gap-8">
<div className="flex flex-col items-center gap-3 text-center">
<Badge variant="soft">مقالهها</Badge>
<h2 id="blog-latest" className="text-3xl font-black sm:text-4xl">
تازهترین مقالهها
</h2>
</div>
{/* نوارِ ابزار بیرون از گرید؛ فیلتر و جستوجو همردیف، با فاصلهٔ ۳۲ تا کارتها. */}
<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={category} onValueChange={(value) => value && setCategory(value as string)} aria-label="دستهبندی">
{CATEGORIES.map((item) => (
<ToggleGroupItem key={item} value={item} className="whitespace-nowrap">
{item}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<SearchField aria-label="جستوجو در مقالهها" placeholder="جستوجوی مقاله یا کلیدواژه" value={query} onValueChange={setQuery} className="w-full sm:max-w-xs" />
</div>
{visible.length === 0 ? (
<EmptyState variant="bordered">
<EmptyStateTitle>مقالهای پیدا نشد</EmptyStateTitle>
<EmptyStateDescription>کلیدواژهٔ دیگری امتحان کنید یا دسته را روی «همه» بگذارید.</EmptyStateDescription>
</EmptyState>
) : (
<ArticleCardGrid>
{visible.map((post) => (
<ArticleCard key={post.id}>
<ArticleCardMedia>
{/* eslint-disable-next-line @next/next/no-img-element -- کاورِ تزئینی ثابت */}
<img src={stockImages[post.cover].src} alt="" loading="lazy" />
</ArticleCardMedia>
<ArticleCardBody>
<ArticleCardCategory>{post.category}</ArticleCardCategory>
<ArticleCardTitle href="#">{post.title}</ArticleCardTitle>
<ArticleCardExcerpt>{post.excerpt}</ArticleCardExcerpt>
<PostMeta post={post} />
</ArticleCardBody>
</ArticleCard>
))}
</ArticleCardGrid>
)}
<Button variant="outline" className="mt-4 self-center">
مقالههای بیشتر
</Button>
</section>
{/* خبرنامه: کارتِ رنگی جسور با فرم؛ رنگها با بازتعریفِ متغیرهای پایه. */}
<section
aria-labelledby="blog-newsletter"
className="relative overflow-hidden rounded-xl bg-[var(--dig-violet-700)] p-8 text-[var(--dig-white)] sm:p-14 [--muted-foreground:var(--dig-violet-100)] [--primary-foreground:var(--dig-violet-800)] [--primary:var(--dig-white)]"
>
<span aria-hidden className="absolute -end-24 -bottom-24 size-80 rounded-full bg-[var(--dig-violet-600)]" />
<span aria-hidden className="absolute -end-4 -bottom-40 size-80 rounded-full bg-[var(--dig-violet-500)]" />
<div className="relative flex max-w-xl flex-col gap-4">
<h2 id="blog-newsletter" className="text-3xl leading-snug font-black sm:text-4xl">
هر پنجشنبه، یک نامهٔ کوتاه
</h2>
<p className="leading-8 text-muted-foreground">بهترین مقالههای هفته و یک نکتهٔ مالی؛ بدون تبلیغ، هر وقت خواستید لغو کنید.</p>
<form onSubmit={subscribe} noValidate className="mt-4 flex flex-col gap-2">
<div className="flex flex-col gap-3 sm:flex-row">
<Input
type="email"
dir="ltr"
aria-label="ایمیل"
placeholder="name@example.com"
value={email}
onChange={(event) => setEmail(event.target.value)}
aria-invalid={emailError ? true : undefined}
aria-describedby={emailError ? "blog-newsletter-error" : undefined}
className="flex-1 bg-[var(--dig-white)] text-[var(--dig-graphite-900)]"
/>
<Button type="submit">عضویت</Button>
</div>
{emailError && (
<p id="blog-newsletter-error" role="alert" className="text-sm font-medium text-[var(--dig-white)]">
{emailError}
</p>
)}
</form>
</div>
</section>
</main>
</ControlSizeProvider>
)
}