Files
salar-store/components/CartPopup.tsx
T
Alireza HassaniandCursor c84147e765 Add Torob tags, Meshkee SEO routes, and updated static-image slots.
Product pages emit crawler-readable meta, public URLs use id plus Farsi slugs, and the homepage catalog matches the dashboard slot keys.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 16:12:26 +03:30

182 lines
7.1 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 Link from "next/link";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { customerContinueCheckoutUrl } from "@/lib/customer";
import { useCart } from "@/lib/cart";
import { formatPrice } from "@/lib/format";
import { PLACEHOLDER } from "@/lib/data";
import { useSession } from "@/lib/session";
import { IconClose } from "./Icons";
import { Media } from "./Media";
type Props = {
open: boolean;
onClose: () => void;
};
export function CartPopup({ open, onClose }: Props) {
const { isAuthenticated } = useSession();
const { items, itemCount, total, removeItem, setQuantity } = useCart();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (!open) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
document.addEventListener("keydown", onKeyDown);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKeyDown);
document.body.style.overflow = "";
};
}, [open, onClose]);
if (!mounted || !open) return null;
const continueHref = customerContinueCheckoutUrl(isAuthenticated, items);
return createPortal(
<div>
<button type="button" className="fixed inset-0 z-[120] bg-black/40" aria-label="بستن" onClick={onClose} />
<div
role="dialog"
aria-modal="true"
aria-label="سبد خرید"
className="fixed inset-y-0 left-0 z-[130] flex h-dvh w-full max-w-md flex-col bg-white shadow-2xl"
>
<div className="flex items-center justify-between border-b border-black/5 px-5 py-4">
<div>
<h2 className="text-base font-extrabold">سبد خرید</h2>
{itemCount > 0 && (
<p className="mt-0.5 text-xs text-zinc-400">
{itemCount.toLocaleString("fa-IR")} کالا
</p>
)}
</div>
<button
type="button"
onClick={onClose}
aria-label="بستن"
className="grid size-9 place-items-center rounded-full hover:bg-zinc-100"
>
<IconClose className="w-4 h-4" />
</button>
</div>
<div className="flex-1 overflow-y-auto px-5 py-4">
{items.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center text-center">
<p className="text-sm text-zinc-500">سبد خرید شما خالی است</p>
</div>
) : (
<ul className="space-y-4">
{items.map((item) => (
<li key={item.id} className="flex gap-3 border-b border-black/5 pb-4 last:border-0">
<Link
href={item.slug ? `/products/${item.slug}` : "/products"}
onClick={onClose}
className="size-20 shrink-0 overflow-hidden rounded-xl border border-black/5"
>
<Media src={item.image || PLACEHOLDER.product} alt={item.name} className="size-full" sizes="80px" />
</Link>
<div className="min-w-0 flex-1">
<Link
href={item.slug ? `/products/${item.slug}` : "/products"}
onClick={onClose}
className="line-clamp-2 text-sm font-medium leading-6 hover:text-brand"
>
{item.name}
</Link>
<div className="mt-2 flex items-center justify-between gap-2">
<div className="flex items-center overflow-hidden rounded-lg border border-black/10">
<button
type="button"
aria-label="افزایش"
onClick={() => setQuantity(item.id, item.quantity + 1)}
className="grid size-8 place-items-center text-sm hover:bg-zinc-50"
>
+
</button>
<span className="min-w-7 text-center text-xs font-bold">
{item.quantity.toLocaleString("fa-IR")}
</span>
<button
type="button"
aria-label="کاهش"
onClick={() => setQuantity(item.id, item.quantity - 1)}
className="grid size-8 place-items-center text-sm hover:bg-zinc-50"
>
−
</button>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-bold">
{formatPrice(item.price * item.quantity)}
<span className="mr-0.5 text-[11px] font-normal text-zinc-400">تومان</span>
</span>
<button
type="button"
onClick={() => removeItem(item.id)}
aria-label="حذف"
className="grid size-8 place-items-center rounded-lg text-zinc-400 hover:bg-brand-soft hover:text-brand"
>
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden>
<path
d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m-8 0v12a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V7M10 11v5M14 11v5"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
</div>
</div>
</li>
))}
</ul>
)}
</div>
<div className="border-t border-black/5 px-5 py-4">
{items.length > 0 && (
<div className="mb-3 flex items-center justify-between text-sm">
<span className="text-zinc-500">جمع کل</span>
<span className="font-extrabold">
{formatPrice(total)}
<span className="mr-0.5 text-xs font-normal text-zinc-400">تومان</span>
</span>
</div>
)}
{items.length > 0 ? (
<a
href={continueHref}
onClick={onClose}
className="flex w-full items-center justify-center rounded-xl bg-brand py-3 text-sm text-white hover:bg-[#e51f1f]"
>
ادامه
</a>
) : (
<button
type="button"
onClick={onClose}
className="flex w-full items-center justify-center rounded-xl bg-brand-soft py-3 text-sm font-medium text-brand"
>
ادامه خرید
</button>
)}
</div>
</div>
</div>,
document.body,
);
}