149 lines
5.2 KiB
TypeScript
149 lines
5.2 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";
|
||
|
||
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>
|
||
);
|
||
}
|