123 lines
5.1 KiB
TypeScript
123 lines
5.1 KiB
TypeScript
"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";
|
||
|
||
/** designspec.md §2.5 — product-card, 157×254, with the two-state add/stepper control. */
|
||
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);
|
||
// Only clear locally once the backend actually confirms removal — otherwise
|
||
// the card would show "add" again while the item still exists server-side.
|
||
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-[254px] w-[157px] 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-[130px] 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-gradient-to-t from-black/40 to-transparent" />
|
||
</div>
|
||
</Link>
|
||
<div className="flex flex-1 flex-col justify-between p-3">
|
||
<div className="flex flex-col gap-1.5">
|
||
<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>
|
||
</div>
|
||
|
||
{disabled ? (
|
||
<span className="text-center text-label text-gray-500">ناموجود</span>
|
||
) : quantity === 0 ? (
|
||
<button
|
||
type="button"
|
||
onClick={handleAdd}
|
||
disabled={isPending}
|
||
aria-label="افزودن به سبد"
|
||
className="flex h-8 w-8 items-center justify-center self-start rounded-full bg-primary-action text-white disabled:opacity-40"
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
</button>
|
||
) : (
|
||
<div className="flex h-10 w-[110px] items-center justify-between gap-4 rounded-3xl border border-gray-300 bg-white p-1">
|
||
<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 bg-primary-action text-white disabled:opacity-40"
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
</button>
|
||
<span className="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 bg-blue-bg text-primary-action disabled:opacity-40"
|
||
>
|
||
<Minus className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|