فروشگاه موبایل
پیشنمایش تمامصفحهکارت محصول موبایل با علاقهمندی و افزودن به سبد، چیپهای دسته و باتمشیتِ فیلتر و مرتبسازی.
۱. نصب کامپوننتها
این بلاک از ۱۴ آیتم دیگ استفاده میکند: add-to-cartapp-barbadgebottom-navigationbuttoncheckboxcontrol-sizedrawericonslabelpersianproduct-cardradio-grouprating
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/bottom-navigation.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/drawer.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/bottom-navigation.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/drawer.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/bottom-navigation.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/drawer.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/add-to-cart.json https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/bottom-navigation.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/checkbox.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/drawer.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/label.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/product-card.json https://docs.digdesign.ir/r/radio-group.json https://docs.digdesign.ir/r/rating.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-shop.tsx
"use client"
import * as React from "react"
import { FilterIcon, HeartIcon, MenuIcon, SearchIcon, ShopIcon, ShoppingCartIcon, UserIcon } from "@/components/icons"
import { AppBar, AppBarActions, AppBarLeading, AppBarTitle } from "@/components/ui/app-bar"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Badge } from "@/components/ui/badge"
import { BottomNavigation, BottomNavigationItem } from "@/components/ui/bottom-navigation"
import { AddToCart } from "@/components/ui/add-to-cart"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { ProductCard, ProductCardAction, ProductCardBody, ProductCardFooter, ProductCardGrid, ProductCardMedia, ProductCardPrice, ProductCardTitle } from "@/components/ui/product-card"
import { Rating } from "@/components/ui/rating"
import { formatCurrency, toFaDigits } from "@/lib/persian"
const CATEGORIES = ["همه", "هدفون", "اسپیکر", "ساعت هوشمند", "لوازم جانبی"]
const PRODUCTS = [
{ id: 1, title: "هدفون بیسیم دیگ پرو", price: 3_490_000, oldPrice: 4_100_000, rating: 4.6, category: "هدفون" },
{ id: 2, title: "اسپیکر آوا بوم", price: 2_300_000, rating: 4.2, category: "اسپیکر" },
{ id: 3, title: "ساعت هوشمند دیگ فیت", price: 5_750_000, rating: 4.8, category: "ساعت هوشمند" },
{ id: 4, title: "کابل شارژ تایپسی", price: 180_000, rating: 4.1, category: "لوازم جانبی" },
{ id: 5, title: "ایربادز سونیک لایت", price: 1_250_000, oldPrice: 1_490_000, rating: 4.3, category: "هدفون" },
{ id: 6, title: "پاوربانک ۲۰ هزار", price: 1_890_000, rating: 4.5, category: "لوازم جانبی" },
]
const SORTS = [
{ value: "popular", label: "محبوبترین" },
{ value: "cheap", label: "ارزانترین" },
{ value: "expensive", label: "گرانترین" },
]
export default function MobileShopBlock() {
const [category, setCategory] = React.useState("همه")
const [sort, setSort] = React.useState("popular")
const [discountOnly, setDiscountOnly] = React.useState(false)
const [liked, setLiked] = React.useState<number[]>([])
const [cart, setCart] = React.useState<Record<number, number>>({})
const cartCount = Object.values(cart).reduce((sum, qty) => sum + qty, 0)
const changeQty = (id: number, delta: number) =>
setCart((current) => {
const next = { ...current, [id]: Math.max(0, (current[id] ?? 0) + delta) }
if (next[id] === 0) delete next[id]
return next
})
const [tab, setTab] = React.useState("shop")
const items = PRODUCTS.filter((item) => category === "همه" || item.category === category)
.filter((item) => !discountOnly || item.oldPrice)
.sort((a, b) => (sort === "cheap" ? a.price - b.price : sort === "expensive" ? b.price - a.price : b.rating - a.rating))
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="منو">
<MenuIcon />
</Button>
</AppBarLeading>
<AppBarTitle>فروشگاه دیگ</AppBarTitle>
<AppBarActions>
<Button variant="ghost" size="icon" aria-label="جستوجو">
<SearchIcon />
</Button>
</AppBarActions>
</AppBar>
<main className="flex flex-1 flex-col gap-4 p-4">
<div className="flex items-center gap-2">
{/* چیپهای دسته در یک ردیفِ پیمایشی؛ در موبایل جا برای پیچیدن نیست. */}
<div className="-ms-4 flex min-w-0 flex-1 gap-2 overflow-x-auto ps-4 [scrollbar-width:none]" role="tablist" aria-label="دستهها">
{CATEGORIES.map((item) => (
<Button
key={item}
variant={category === item ? "default" : "outline"}
role="tab"
aria-selected={category === item}
onClick={() => setCategory(item)}
className="shrink-0 rounded-full"
>
{item}
</Button>
))}
</div>
{/* باتمشیت: فیلتر و مرتبسازی از پایینِ صفحه، جایی که شست میرسد. */}
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline" size="icon" aria-label="فیلتر و مرتبسازی" className="relative shrink-0">
<FilterIcon />
{discountOnly && <span aria-hidden className="absolute end-1.5 top-1.5 size-2 rounded-full bg-primary" />}
</Button>
</DrawerTrigger>
<ControlSizeProvider size="xl" asChild>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>فیلتر و مرتبسازی</DrawerTitle>
</DrawerHeader>
<div className="flex flex-col gap-5 px-4">
<div className="flex flex-col gap-3">
<Label id="sort-label">مرتبسازی</Label>
<RadioGroup value={sort} onValueChange={setSort} aria-labelledby="sort-label" className="gap-3">
{SORTS.map((item) => (
<div key={item.value} className="flex items-center gap-2">
<RadioGroupItem id={`sort-${item.value}`} value={item.value} />
<Label htmlFor={`sort-${item.value}`} className="font-normal">
{item.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className="flex items-center gap-2">
<Checkbox id="discount" checked={discountOnly} onCheckedChange={(on) => setDiscountOnly(on === true)} />
<Label htmlFor="discount" className="font-normal">
فقط کالاهای تخفیفدار
</Label>
</div>
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button fullWidth>نمایش {toFaDigits(items.length)} کالا</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</ControlSizeProvider>
</Drawer>
</div>
<ProductCardGrid size="sm">
{items.map((item) => {
const isLiked = liked.includes(item.id)
return (
<ProductCard key={item.id} size="sm">
<ProductCardMedia>
<ProductCardAction>
<Button
variant="outline"
size="icon"
aria-label={isLiked ? `حذف ${item.title} از علاقهمندیها` : `افزودن ${item.title} به علاقهمندیها`}
aria-pressed={isLiked}
onClick={() => setLiked((list) => (isLiked ? list.filter((id) => id !== item.id) : [...list, item.id]))}
className={isLiked ? "rounded-full bg-background text-destructive" : "rounded-full bg-background"}
>
<HeartIcon filled={isLiked} />
</Button>
</ProductCardAction>
</ProductCardMedia>
<ProductCardBody>
<ProductCardTitle>{item.title}</ProductCardTitle>
<Rating value={Math.round(item.rating)} readOnly size="sm" aria-label={`امتیاز ${toFaDigits(item.rating)}`} />
<ProductCardPrice
amount={formatCurrency(item.price)}
originalAmount={item.oldPrice ? formatCurrency(item.oldPrice) : undefined}
discount={item.oldPrice ? `٪${toFaDigits(Math.round((1 - item.price / item.oldPrice) * 100))}` : undefined}
/>
</ProductCardBody>
{/* دکمهٔ تمامعرضِ ۴۰ پیکسلی (ناحیهٔ لمسِ کافی)؛ بعد از افزودن به شمارنده تبدیل میشود. */}
<ProductCardFooter>
<AddToCart
fullWidth
variant="default"
itemName={item.title}
quantity={cart[item.id] ?? 0}
onQuantityChange={(next) => changeQty(item.id, next - (cart[item.id] ?? 0))}
/>
</ProductCardFooter>
</ProductCard>
)
})}
</ProductCardGrid>
</main>
<BottomNavigation value={tab} onValueChange={setTab} indicator="pill" fixed={false} className="sticky bottom-0">
<BottomNavigationItem value="shop" icon={<ShopIcon />} label="فروشگاه" />
<BottomNavigationItem value="likes" icon={<HeartIcon />} label="علاقهمندی" badge={liked.length ? toFaDigits(liked.length) : undefined} />
<BottomNavigationItem
value="cart"
icon={<ShoppingCartIcon />}
label="سبد خرید"
badge={
cartCount ? (
<Badge color="destructive">{toFaDigits(cartCount)}</Badge>
) : undefined
}
/>
<BottomNavigationItem value="profile" icon={<UserIcon />} label="حساب" />
</BottomNavigation>
</div>
</ControlSizeProvider>
)
}