بلاک‌ها · موبایل

فروشگاه موبایل

پیش‌نمایش تمام‌صفحه

کارت محصول موبایل با علاقه‌مندی و افزودن به سبد، چیپ‌های دسته و باتم‌شیتِ فیلتر و مرتب‌سازی.

۱. نصب کامپوننت‌ها

این بلاک از ۱۴ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}