This commit is contained in:
amirhosein.ashourloo
2026-08-20 11:17:04 +03:30
parent 82e73c47cf
commit 08a729b3fc
7 changed files with 206 additions and 50 deletions
+10 -10
View File
@@ -4,12 +4,12 @@ import Link from "next/link";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Navigation } from "swiper/modules";
import "swiper/css";
import type { ApiProduct } from "@/lib/types";
import type { SpecialCarouselItem } from "@/lib/types";
import ProductCard from "./ui/ProductCard";
import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
export default function SpecialSaleCarousel({ products }: { products: ApiProduct[] }) {
if (products.length === 0) return null;
export default function SpecialSaleCarousel({ items }: { items: SpecialCarouselItem[] }) {
if (items.length === 0) return null;
return (
<section id="special-sale" className="boxed py-10 md:py-14">
@@ -40,13 +40,13 @@ export default function SpecialSaleCarousel({ products }: { products: ApiProduct
prevEl: ".sale-prev",
nextEl: ".sale-next",
}}
loop={products.length > 5}
loop={items.length > 5}
spaceBetween={14}
slidesPerView={2.15}
slidesPerView={1.8}
breakpoints={{
480: { slidesPerView: 2.4 },
640: { slidesPerView: 3.2 },
1024: { slidesPerView: 5, spaceBetween: 20 },
480: { slidesPerView: 2.2 },
640: { slidesPerView: 3 },
1024: { slidesPerView: 4.5, spaceBetween: 20 },
}}
>
<SwiperSlide>
@@ -56,9 +56,9 @@ export default function SpecialSaleCarousel({ products }: { products: ApiProduct
className="w-full h-full aspect-[4/5] object-cover rounded-lg"
/>
</SwiperSlide>
{products.map((product) => (
{items.map(({ product, discount }) => (
<SwiperSlide key={product.id}>
<ProductCard product={product} />
<ProductCard product={product} discount={discount} />
</SwiperSlide>
))}
</Swiper>
+9 -9
View File
@@ -4,13 +4,13 @@ import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import type { ApiProduct } from "@/lib/types";
import type { SpecialCarouselItem } from "@/lib/types";
import ProductCard from "./ui/ProductCard";
import SectionHeading from "./ui/SectionHeading";
import { ChevronLeftIcon, ChevronRightIcon } from "./icons";
export default function TopSellingCarousel({ products }: { products: ApiProduct[] }) {
if (products.length === 0) return null;
export default function TopSellingCarousel({ items }: { items: SpecialCarouselItem[] }) {
if (items.length === 0) return null;
return (
<section className="boxed py-10 md:py-14">
@@ -42,16 +42,16 @@ export default function TopSellingCarousel({ products }: { products: ApiProduct[
nextEl: ".top-selling-next",
}}
spaceBetween={16}
slidesPerView={2.2}
slidesPerView={1.8}
breakpoints={{
640: { slidesPerView: 3.2 },
1024: { slidesPerView: 4 },
1280: { slidesPerView: 5 },
640: { slidesPerView: 2.8 },
1024: { slidesPerView: 3.5 },
1280: { slidesPerView: 4.5 },
}}
>
{products.map((product) => (
{items.map(({ product, discount }) => (
<SwiperSlide key={product.id}>
<ProductCard product={product} />
<ProductCard product={product} discount={discount} />
</SwiperSlide>
))}
</Swiper>
+8
View File
@@ -155,6 +155,14 @@ export function CheckIcon({ className }: IconProps) {
);
}
export function PlusIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={className} {...base}>
<path d="M12 5v14M5 12h14" />
</svg>
);
}
export function CommentIcon({ className }: IconProps) {
return (
<svg viewBox="0 0 24 24" className={className} {...base}>
+59 -11
View File
@@ -1,12 +1,31 @@
import Link from "next/link";
import type { ApiProduct } from "@/lib/types";
import { formatToman } from "@/lib/format";
"use client";
export default function ProductCard({ product }: { product: ApiProduct }) {
import { useState } from "react";
import Link from "next/link";
import type { ApiProduct, ProductDiscount } from "@/lib/types";
import { formatToman } from "@/lib/format";
import { CheckIcon, PlusIcon } from "../icons";
export default function ProductCard({
product,
discount,
}: {
product: ApiProduct;
discount?: ProductDiscount;
}) {
const [added, setAdded] = useState(false);
const title = product.nameFa || product.title;
const image =
product.thumbnailUrl || product.image || product.thumbnail || "/images/products/product-1.svg";
const store = product.store;
const price = discount ? discount.price : store?.minPrice;
function handleQuickAdd(e: React.MouseEvent) {
e.preventDefault();
e.stopPropagation();
setAdded(true);
setTimeout(() => setAdded(false), 1500);
}
return (
<Link
@@ -26,15 +45,44 @@ export default function ProductCard({ product }: { product: ApiProduct }) {
</div>
<div className="flex flex-1 flex-col p-3.5">
<h3 className="text-[13px] md:text-sm text-ink line-clamp-2 min-h-[2.5em]">{title}</h3>
<div className="mt-2 flex items-center gap-2 flex-wrap">
<span className="font-bold text-sm md:text-[15px] text-brand">
{store?.minPrice != null ? formatToman(store.minPrice) : "تماس بگیرید"}
</span>
{store?.maxPrice != null && store.maxPrice !== store.minPrice && (
<span className="text-xs text-body/60">تا {formatToman(store.maxPrice)}</span>
{/* Fixed height regardless of 1 or 2 lines, so card heights stay
consistent across a row of mixed-length titles. */}
<h3 className="h-10 md:h-11 leading-tight text-[13px] md:text-sm text-ink text-right line-clamp-2">
{title}
</h3>
{/* Always reserves its height, even with no discount, so a
discounted card and a plain card end up the same height. Right
aligned to line up with the price row below; discount badge
sits to the left of the old price. */}
<div className="mt-2 h-6 flex items-center justify-start gap-2">
{discount && (
<>
<span className="text-xs text-body/60 line-through truncate">
{discount.oldPrice.toLocaleString("en-US")}
</span>
<span className="shrink-0 rounded-md bg-gold/20 text-gold-dark text-xs font-bold px-1.5 py-0.5">
%{discount.percent}
</span>
</>
)}
</div>
<div className="mt-2 flex items-center justify-between gap-2">
<span className="font-bold text-sm md:text-[15px] text-brand truncate">
{price != null ? formatToman(price) : "تماس بگیرید"}
</span>
<button
type="button"
onClick={handleQuickAdd}
aria-label="افزودن سریع به سبد خرید"
className={`shrink-0 flex items-center justify-center size-8 rounded-full transition-colors ${
added ? "bg-brand-dark text-white" : "bg-brand text-white hover:bg-brand-dark"
}`}
>
{added ? <CheckIcon className="size-4" /> : <PlusIcon className="size-4" />}
</button>
</div>
</div>
</Link>
);