پرسشهای متداول
پیشنمایش تمامصفحهمعرفی با جستوجوی بزرگ، دستهها کنارِ فهرست (موبایل: نوارِ افقی)، آکاردئونِ نرم با نشانگرِ بهعلاوه، حالتِ خالی و کارتِ «هنوز سؤال دارید؟».
۱. نصب کامپوننتها
این بلاک از ۱۰ آیتم دیگ استفاده میکند: accordionavatarbadgebuttoncontrol-sizeempty-stateiconspersiansearch-fieldtoggle-group
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/accordion.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/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/accordion.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/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/accordion.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/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/accordion.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/persian.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/toggle-group.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
faq.tsx
"use client"
import * as React from "react"
import { AvatarGenderIcon, ChevronLeftIcon } from "@/components/icons"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Avatar, AvatarGroup } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { EmptyState, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { SearchField } from "@/components/ui/search-field"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { cn } from "@/lib/utils"
import { stockImages } from "@/lib/stock-images"
import { toFaDigits, toSearchKey } from "@/lib/persian"
const TOPICS = [
{
id: "general",
title: "عمومی",
faq: [
{ q: "نسخهٔ رایگان دارید؟", a: "بله؛ ۱۴ روز همهٔ امکانات را بدونِ کارتِ بانکی امتحان کنید. اگر خواستید، یک جلسهٔ ۳۰ دقیقهای راهاندازی هم رایگان برایتان میگذاریم." },
{ q: "برای کارِ تیمی هم مناسب است؟", a: "تا ۵ نفر در پلنِ پایه و بینهایت در پلنِ سازمانی؛ هر نفر نقش و دسترسی خودش را دارد." },
{ q: "اطلاعاتم کجا نگهداری میشود؟", a: "روی سرورهای داخلِ ایران با پشتیبانگیری روزانه و رمزنگاری." },
],
},
{
id: "billing",
title: "پرداخت و پلن",
faq: [
{ q: "میتوانم پلن را بعداً عوض کنم؟", a: "هر وقت خواستید؛ مبلغ به نسبتِ روزهای باقیمانده حساب میشود." },
{ q: "فاکتورِ رسمی میدهید؟", a: "بله؛ فاکتورِ رسمی با کدِ اقتصادی، بعد از هر پرداخت در بخشِ صورتحسابها." },
{ q: "سیاستِ لغو چیست؟", a: "تا ۷ روز پس از خرید، کلِ مبلغ برمیگردد؛ بعد از آن تا پایانِ دوره فعال میماند." },
],
},
{
id: "account",
title: "حساب کاربری",
faq: [
{ q: "ایمیلِ حسابم را چطور عوض کنم؟", a: "از تنظیمات › حساب، ایمیلِ جدید را بنویسید و لینکِ تأییدی را که میفرستیم باز کنید." },
{ q: "رمزم را فراموش کردهام", a: "در صفحهٔ ورود «فراموشی رمز» را بزنید؛ کدِ یکبارمصرف به موبایلتان پیامک میشود." },
],
},
{
id: "security",
title: "امنیت",
faq: [{ q: "ورودِ دومرحلهای دارید؟", a: "بله؛ با پیامک یا اپهای رمزِ یکبارمصرف، از تنظیمات › امنیت فعالش کنید." }],
},
]
/**
* صفحهٔ پرسشهای متداول: معرفی با جستوجو، دستهها کنارِ فهرست (در موبایل
* نوارِ افقی)، آکاردئونِ نرم با نشانگرِ بهعلاوه و کارتِ «هنوز سؤال دارید؟».
* با جستوجو، نتیجه از همهٔ دستهها میآید و دستهها کنار میروند.
*/
export default function FaqBlock() {
const [topic, setTopic] = React.useState(TOPICS[0].id)
const [query, setQuery] = React.useState("")
const key = toSearchKey(query)
const active = TOPICS.find((item) => item.id === topic) ?? TOPICS[0]
const items = query
? TOPICS.flatMap((item) => item.faq.filter((faq) => toSearchKey(faq.q + faq.a).includes(key)))
: active.faq
return (
<ControlSizeProvider size="xl" asChild>
<main className="flex flex-col">
{/* معرفی روی زمینهٔ تهرنگ با علامتهای سؤالِ بزرگ و تزئینی. */}
<section className="relative overflow-hidden bg-[color-mix(in_oklab,var(--brand-secondary)_8%,var(--background))] px-4 py-20 sm:px-6">
<span aria-hidden className="pointer-events-none absolute -top-10 start-[8%] text-[12rem] leading-none font-black text-[color-mix(in_oklab,var(--brand-secondary)_14%,var(--background))] select-none">
؟
</span>
<span aria-hidden className="pointer-events-none absolute end-[6%] -bottom-16 text-[16rem] leading-none font-black text-[color-mix(in_oklab,var(--brand-secondary)_14%,var(--background))] select-none">
؟
</span>
<div className="relative 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-bold sm:text-5xl sm:leading-snug">چطور کمکتان کنیم؟</h1>
<p className="text-lg leading-8 text-muted-foreground">پاسخِ پرسشهای رایج اینجاست؛ اگر پیدا نکردید، تیمِ پشتیبانی همان روز جواب میدهد.</p>
<ControlSizeProvider size="xl" className="mt-4 w-full max-w-lg">
<SearchField aria-label="جستوجو در پرسشها" placeholder="مثلاً «لغو اشتراک»" value={query} onValueChange={setQuery} className="w-full bg-background" />
</ControlSizeProvider>
</div>
</section>
<section aria-labelledby="faq-title" className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-12 px-4 py-20 sm:px-6 lg:grid-cols-[16rem_minmax(0,1fr)]">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-2">
<h2 id="faq-title" className="text-2xl font-bold">
پرسشهای متداول
</h2>
<p className="text-sm leading-7 text-muted-foreground">دسته را انتخاب کنید یا بالای صفحه جستوجو کنید.</p>
</div>
{/* دستهها: در دسکتاپ فهرستِ عمودی با تعداد، در موبایل نوارِ افقی. */}
{!query && (
<>
<nav aria-label="دستهبندی پرسشها" className="hidden flex-col gap-1 lg:flex">
{TOPICS.map((item) => (
<Button
key={item.id}
variant={item.id === topic ? "outline" : "ghost"}
fullWidth
aria-current={item.id === topic ? "true" : undefined}
onClick={() => setTopic(item.id)}
className={cn("justify-between rounded-xl", item.id === topic ? "border-foreground font-semibold" : "font-normal text-muted-foreground")}
>
{item.title}
<span className="flex items-center gap-2 text-xs text-muted-foreground">
{toFaDigits(item.faq.length)}
<ChevronLeftIcon aria-hidden className="size-4" />
</span>
</Button>
))}
</nav>
<div className="max-w-full overflow-x-auto lg:hidden">
<ToggleGroup type="single" variant="segmented" value={topic} onValueChange={(value) => value && setTopic(value as string)} aria-label="دستهبندی پرسشها">
{TOPICS.map((item) => (
<ToggleGroupItem key={item.id} value={item.id} className="whitespace-nowrap">
{item.title}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
</>
)}
</div>
<div aria-live="polite">
{query && <p className="mb-4 text-sm text-muted-foreground">{toFaDigits(items.length)} نتیجه برای «{query}»</p>}
{items.length === 0 ? (
<EmptyState variant="bordered">
<EmptyStateTitle>پاسخی برای «{query}» پیدا نشد</EmptyStateTitle>
<EmptyStateDescription>کلمهٔ دیگری امتحان کنید یا از پایینِ صفحه با پشتیبانی گفتوگو کنید.</EmptyStateDescription>
</EmptyState>
) : (
<Accordion key={query ? "search" : topic} type="single" collapsible variant="soft" indicator="plus" defaultValue={items[0].q}>
{items.map((item) => (
<AccordionItem key={item.q} value={item.q}>
<AccordionTrigger className="text-base font-semibold">{item.q}</AccordionTrigger>
<AccordionContent className="leading-8 text-muted-foreground">{item.a}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</div>
</section>
{/* هنوز سؤال دارید؟ آواتارِ تیم، متن و دو راهِ تماس. */}
<section className="mx-auto w-full max-w-6xl px-4 pb-20 sm:px-6">
<div className="flex flex-col items-center gap-6 rounded-xl bg-muted px-6 py-12 text-center">
<AvatarGroup>
<Avatar alt="نیما" src={stockImages.personMan1.src} icon={<AvatarGenderIcon gender="male" />} className="ring-2 ring-muted" />
<Avatar size="lg" alt="مهسا" src={stockImages.personWoman1.src} icon={<AvatarGenderIcon gender="female" />} className="ring-2 ring-muted" />
<Avatar alt="آرش" src={stockImages.personMan2.src} icon={<AvatarGenderIcon gender="male" />} className="ring-2 ring-muted" />
</AvatarGroup>
<div className="flex flex-col gap-2">
<h2 className="text-xl font-bold">هنوز سؤال دارید؟</h2>
<p className="text-sm leading-7 text-muted-foreground">جوابتان را پیدا نکردید؟ تیمِ پشتیبانی هر روز از ۸ تا ۲۲ پاسخگوست.</p>
</div>
{/* در موبایل دو دکمهٔ همعرض، اقدامِ اصلی بالا. */}
<ControlSizeProvider size="xl" className="flex w-full flex-col gap-3 sm:w-auto sm:flex-row-reverse">
<Button>گفتوگو با پشتیبانی</Button>
<Button variant="outline" className="bg-background">
ارسالِ ایمیل
</Button>
</ControlSizeProvider>
</div>
</section>
</main>
</ControlSizeProvider>
)
}