جستوجوی موبایل
پیشنمایش تمامصفحهنوار جستوجوی تمامعرض، جستوجوهای اخیر و پرطرفدار، نتیجهها و حالت بینتیجه.
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: buttoncontrol-sizeempty-stateiconspersiansearch-field
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add 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.jsonpnpm dlx digdesign@latest add 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.jsonyarn dlx digdesign@latest add 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.jsonbunx --bun digdesign@latest add 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۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-search.tsx
"use client"
import * as React from "react"
import { ChevronRightIcon, ClockIcon, ImageIcon, XIcon } from "@/components/icons"
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 { formatCurrency, toSearchKey } from "@/lib/persian"
const PRODUCTS = [
{ id: 1, title: "هدفون بیسیم دیگ پرو", category: "هدفون", price: 3_490_000 },
{ id: 2, title: "هدفون گیمینگ دیگ", category: "هدفون", price: 4_750_000 },
{ id: 3, title: "اسپیکر آوا بوم", category: "اسپیکر", price: 2_300_000 },
{ id: 4, title: "ساعت هوشمند دیگ فیت", category: "ساعت هوشمند", price: 5_750_000 },
{ id: 5, title: "ایربادز سونیک لایت", category: "هدفون", price: 1_250_000 },
]
const POPULAR = ["هدفون", "ساعت هوشمند", "پاوربانک", "کیف لپتاپ"]
export default function MobileSearchBlock() {
const [query, setQuery] = React.useState("")
const [recent, setRecent] = React.useState(["هدفون بیسیم", "اسپیکر", "کابل تایپسی"])
const key = toSearchKey(query)
const results = key ? PRODUCTS.filter((item) => toSearchKey(item.title + item.category).includes(key)) : []
function search(term: string) {
setQuery(term)
setRecent((list) => [term, ...list.filter((item) => item !== term)].slice(0, 5))
}
return (
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex min-h-svh w-full max-w-md flex-col bg-background">
<h1 className="sr-only">جستوجو</h1>
{/* نوارِ جستوجوی تمامعرض جای عنوان را میگیرد؛ فوکوس از اول روی فیلد است. */}
<header className="sticky top-0 z-10 flex items-center gap-2 border-b border-border bg-background p-3">
<Button variant="ghost" size="icon" aria-label="بازگشت">
<ChevronRightIcon className="ltr:rotate-180" />
</Button>
<SearchField
aria-label="جستوجو در فروشگاه"
placeholder="جستوجوی کالا…"
value={query}
onValueChange={setQuery}
onSearch={(value) => value && search(value)}
className="flex-1"
/>
</header>
<main className="flex flex-1 flex-col gap-6 p-4" aria-live="polite">
{!query && (
<>
{recent.length > 0 && (
<section aria-labelledby="recent" className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h2 id="recent" className="font-semibold">
جستوجوهای اخیر
</h2>
<Button variant="ghost" onClick={() => setRecent([])}>
پاک کردن
</Button>
</div>
<ul className="flex flex-col">
{recent.map((term) => (
<li key={term} className="flex min-h-12 items-center gap-3">
<ClockIcon className="size-4 shrink-0 text-muted-foreground" />
<Button variant="ghost" className="flex-1 justify-start px-0 font-normal" onClick={() => search(term)}>
{term}
</Button>
<Button
variant="ghost"
size="icon"
aria-label={`حذف «${term}» از جستوجوهای اخیر`}
onClick={() => setRecent((list) => list.filter((item) => item !== term))}
>
<XIcon />
</Button>
</li>
))}
</ul>
</section>
)}
<section aria-labelledby="popular" className="flex flex-col gap-3">
<h2 id="popular" className="font-semibold">
جستوجوهای پرطرفدار
</h2>
<div className="flex flex-wrap gap-2">
{POPULAR.map((term) => (
<Button key={term} variant="secondary" className="rounded-full" onClick={() => search(term)}>
{term}
</Button>
))}
</div>
</section>
</>
)}
{query && results.length > 0 && (
<ul className="flex flex-col divide-y divide-border">
{results.map((item) => (
<li key={item.id}>
<a href="#" className="flex items-center gap-3 py-3">
<span className="flex size-14 shrink-0 items-center justify-center rounded-xl bg-muted text-placeholder">
<ImageIcon className="size-6" />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="truncate font-medium">{item.title}</span>
<span className="text-xs text-muted-foreground">{item.category}</span>
</span>
<span className="shrink-0 text-sm font-semibold">{formatCurrency(item.price)}</span>
</a>
</li>
))}
</ul>
)}
{query && results.length === 0 && (
<EmptyState>
<EmptyStateTitle>کالایی با «{query}» پیدا نشد</EmptyStateTitle>
<EmptyStateDescription>املای کلمه را بررسی کنید یا عبارتِ کوتاهتری بنویسید.</EmptyStateDescription>
</EmptyState>
)}
</main>
</div>
</ControlSizeProvider>
)
}