winofy-front/src/components/store/product-grid-card.tsx
2026-08-31 16:03:11 +03:30

149 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useState, useTransition } from "react";
import Link from "next/link";
import { useRouter, usePathname } from "next/navigation";
import { Minus, Plus } from "lucide-react";
import {
addToCart,
updateCartItemQuantity,
removeCartItem,
} from "@/lib/api/cart";
import type { CartItem, ProductListItem } from "@/types/api";
export function ProductGridCard({
product,
cartItem,
}: {
product: ProductListItem;
cartItem: CartItem | null;
}) {
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
const [localItem, setLocalItem] = useState(cartItem);
const [error, setError] = useState<string | null>(null);
const quantity = localItem?.quantity ?? 0;
const disabled = !product.is_active || product.is_out_of_stock;
useEffect(() => {
if (!error) return;
const timeout = setTimeout(() => setError(null), 3000);
return () => clearTimeout(timeout);
}, [error]);
function handleAdd() {
startTransition(async () => {
const result = await addToCart(product.uuid, 1);
if (result.ok) {
setLocalItem(result.data);
router.refresh();
} else if (result.status === 401) {
router.push(`/login?next=${encodeURIComponent(pathname)}`);
} else {
setError(result.message);
}
});
}
function handleChange(nextQuantity: number) {
if (!localItem) return;
startTransition(async () => {
if (nextQuantity <= 0) {
const result = await removeCartItem(localItem.uuid);
if (result.ok) setLocalItem(null);
else setError(result.message);
} else {
const result = await updateCartItemQuantity(
localItem.uuid,
nextQuantity,
);
if (result.ok) setLocalItem(result.data);
else setError(result.message);
}
router.refresh();
});
}
return (
<article className="relative flex h-63.5 w-39.25 flex-col overflow-hidden rounded-xl border border-gray-300 bg-white shadow-[0_2px_10px_-6px_rgba(0,0,0,0.039),0_14px_28px_-12px_rgba(0,0,0,0.071)]">
{error && (
<div className="absolute inset-x-1 top-1 z-10 rounded-lg bg-error px-2 py-1 text-center text-[11px] leading-tight text-white">
{error}
</div>
)}
<Link href={`/products/${product.uuid}`} className="block shrink-0">
<div className="relative h-32.5 w-full overflow-hidden rounded-t-xl bg-muted">
{product.image_url && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={product.image_url}
alt={product.name}
className="h-full w-full object-cover"
/>
)}
<div className="absolute inset-x-0 bottom-0 h-1/2 bg-linear-to-t from-black/40 to-transparent" />
</div>
</Link>
<div className="flex flex-1 flex-col gap-1.5 p-3">
<Link href={`/products/${product.uuid}`}>
<h3 className="truncate text-right text-[14px] font-bold leading-none text-foreground">
{product.name}
</h3>
</Link>
<p className="text-right text-body-medium font-extrabold text-primary-action">
{product.price.toLocaleString("fa-IR")} تومان
</p>
{disabled && (
<span className="mt-auto text-center text-label text-gray-500">
ناموجود
</span>
)}
</div>
{!disabled && (
<div
className={`motion-reduce:transition-none absolute bottom-3 left-3 overflow-hidden rounded-full border transition-[width,background-color,border-color] duration-300 ease-out ${
quantity === 0
? "h-10 w-10 border-transparent bg-primary-action"
: "h-10 w-28 border-gray-300 bg-white"
}`}
dir="ltr"
>
<div className="relative flex h-full w-full items-center justify-between px-1">
<button
type="button"
onClick={
quantity === 0 ? handleAdd : () => handleChange(quantity + 1)
}
disabled={isPending}
aria-label={quantity === 0 ? "افزودن به سبد" : "افزایش تعداد"}
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-0 bg-primary-action p-0 text-white disabled:opacity-40"
>
<Plus className="h-4 w-4" />
</button>
{quantity > 0 && (
<>
<span className="flex h-full w-8 shrink-0 items-center justify-center text-center text-[18px] font-bold leading-none text-foreground">
{quantity.toLocaleString("fa-IR")}
</span>
<button
type="button"
onClick={() => handleChange(quantity - 1)}
disabled={isPending}
aria-label="کاهش تعداد"
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border-0 bg-blue-bg p-0 text-primary-action disabled:opacity-40"
>
<Minus className="h-4 w-4" />
</button>
</>
)}
</div>
</div>
)}
</article>
);
}