Add guest cart drawer, Continue handoff, and shared customer login.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-25 17:33:39 +03:30
co-authored by Cursor
parent ad05241a77
commit 16340c3b48
18 changed files with 1082 additions and 97 deletions
+181
View File
@@ -0,0 +1,181 @@
"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="" 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,
);
}