- Resolve product cards from group endpoints (store-specials,
brand-groups) that only return a productId, not a slug, via a new
/products/id/[id] route backed by GET /products/by-id/{id} — clicking
these was previously falling back to a name search instead of the
detail page.
- Derive discount price/percent generically in normalizeProduct so any
source with real old/new prices shows it, even without an explicit
discount field from the API.
- Product cards and the detail page buy-box: show the discount badge
beside the old (struck-through) price, current price alone on its
own line, matching the reference design.
- Equalize product card heights within a carousel row (Swiper's own
CSS doesn't stretch slides to the tallest sibling in slidesPerView:
"auto" + freeMode mode) so the add-to-cart button lines up across
cards regardless of whether a card has a discount line.
- Featured categories: 110px images with no border/radius, matching
the site's other category card treatment.
- Replace Vazirmatn with the client's IRANYekan font files — light
weight for body text, bold weight for headings only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
112 lines
4.1 KiB
TypeScript
112 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { Swiper, SwiperSlide } from "swiper/react";
|
|
import { FreeMode } from "swiper/modules";
|
|
import "swiper/css";
|
|
import "swiper/css/free-mode";
|
|
import { SectionTitle } from "@/components/ui/SectionTitle";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
import type { Category, StaticImageWithUrl } from "@/lib/meshkee";
|
|
|
|
function CategorySlotCard({ image }: { image: StaticImageWithUrl }) {
|
|
const label = image.titleFa ?? image.titleEn ?? "";
|
|
const content = (
|
|
<div className="group flex h-full flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md">
|
|
<div className="mb-3 w-[110px] overflow-hidden">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img src={image.url} alt={label} className="h-full w-full object-contain" />
|
|
</div>
|
|
{label && (
|
|
<span className="w-full truncate text-center text-xs font-medium leading-relaxed md:text-sm">
|
|
{label}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
return image.linkUrl ? <a href={image.linkUrl}>{content}</a> : content;
|
|
}
|
|
|
|
export function FeaturedCategories({
|
|
categories,
|
|
images = [],
|
|
}: {
|
|
categories: Category[];
|
|
images?: StaticImageWithUrl[];
|
|
}) {
|
|
return (
|
|
<section className="py-10 md:py-14">
|
|
<div className="mx-auto max-w-7xl px-4">
|
|
<SectionTitle
|
|
title="دستهبندیهای پرطرفدار"
|
|
enTitle="Featured Categories"
|
|
action="همه دستهها"
|
|
/>
|
|
{images.length > 0 ? (
|
|
<Swiper
|
|
modules={[FreeMode]}
|
|
dir="rtl"
|
|
slidesPerView={2.3}
|
|
spaceBetween={12}
|
|
breakpoints={{
|
|
640: { slidesPerView: 4.3, spaceBetween: 12 },
|
|
1024: { slidesPerView: 8, spaceBetween: 12 },
|
|
}}
|
|
freeMode={{ enabled: true, momentum: true, sticky: true }}
|
|
grabCursor
|
|
className="!px-1 !py-2"
|
|
>
|
|
{images.map((image, i) => (
|
|
<SwiperSlide key={image.id}>
|
|
<div className="animate-slide-in h-full" style={{ animationDelay: `${i * 60}ms` }}>
|
|
<CategorySlotCard image={image} />
|
|
</div>
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
) : categories.length === 0 ? (
|
|
<EmptyState message="هنوز دستهبندیای ثبت نشده است." />
|
|
) : (
|
|
<Swiper
|
|
modules={[FreeMode]}
|
|
dir="rtl"
|
|
slidesPerView={2.3}
|
|
spaceBetween={12}
|
|
breakpoints={{
|
|
640: { slidesPerView: 4.3, spaceBetween: 12 },
|
|
1024: { slidesPerView: 8, spaceBetween: 12 },
|
|
}}
|
|
freeMode={{ enabled: true, momentum: true, sticky: true }}
|
|
grabCursor
|
|
className="!px-1 !py-2"
|
|
>
|
|
{categories.map((cat, i) => (
|
|
<SwiperSlide key={cat.id}>
|
|
<a
|
|
href="#"
|
|
className="animate-slide-in group flex h-full flex-col items-center rounded-xl border border-border bg-surface p-3 transition-all hover:border-red hover:shadow-md"
|
|
style={{ animationDelay: `${i * 60}ms` }}
|
|
>
|
|
<div className="mb-3 w-[110px] overflow-hidden">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={cat.image ?? "/images/placeholder.svg"}
|
|
alt={cat.name}
|
|
className="h-full w-full object-contain"
|
|
/>
|
|
</div>
|
|
<span className="w-full truncate text-center text-xs font-medium leading-relaxed md:text-sm">
|
|
{cat.name}
|
|
</span>
|
|
{cat.count != null && (
|
|
<span className="mt-1 text-[11px] text-muted">{cat.count} محصول</span>
|
|
)}
|
|
</a>
|
|
</SwiperSlide>
|
|
))}
|
|
</Swiper>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|