Add guest mini-cart with header icon, popup, and customer dashboard handoff
Wires a localStorage/cookie-backed guest cart (per AI_PROMPT.md contract) into the header cart icon and popup, with add-to-cart in the product card and buy panel resolved via the dedicated by-product store-items endpoint. Continue always hands off to the Meshkee customer dashboard for auth/checkout — no login, register, or payment UI is built on the storefront. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d7b490e442
commit
12d39bc020
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCart } from "./CartProvider";
|
||||
import { getContinueUrl } from "@/lib/cart";
|
||||
import { formatToman } from "@/lib/format";
|
||||
import { BagIcon, CloseIcon, MinusIcon, PlusIcon, TrashIcon } from "./icons";
|
||||
|
||||
export default function MiniCart() {
|
||||
const { items, count, total, updateQuantity, removeItem } = useCart();
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function onClickAway(e: MouseEvent) {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", onClickAway);
|
||||
return () => document.removeEventListener("mousedown", onClickAway);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={rootRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="relative flex items-center justify-center size-10 md:size-11 rounded-lg border border-line text-ink hover:border-brand hover:text-brand transition-colors"
|
||||
aria-label="سبد خرید"
|
||||
>
|
||||
<BagIcon className="size-5" />
|
||||
{count > 0 && (
|
||||
<span className="absolute -top-1.5 -end-1.5 flex items-center justify-center size-4.5 rounded-full bg-gold text-[10px] font-bold text-ink">
|
||||
{count > 99 ? "99+" : count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute end-0 top-full mt-2 w-80 sm:w-96 bg-white border border-line rounded-xl shadow-xl z-50 flex flex-col max-h-[70vh]">
|
||||
<div className="flex items-center justify-between px-4 h-12 border-b border-line shrink-0">
|
||||
<h3 className="text-sm font-bold text-ink">سبد خرید</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="بستن"
|
||||
className="p-1 text-body hover:text-ink"
|
||||
>
|
||||
<CloseIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="px-4 py-8 text-center text-sm text-body">سبد خرید شما خالی است</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto divide-y divide-line">
|
||||
{items.map((line) => (
|
||||
<div key={line.id} className="flex gap-3 px-4 py-3">
|
||||
<img
|
||||
src={line.image}
|
||||
alt={line.name}
|
||||
className="size-16 shrink-0 rounded-lg object-cover border border-line"
|
||||
/>
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
<p className="text-[13px] text-ink text-right line-clamp-2">{line.name}</p>
|
||||
<div className="mt-auto flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 rounded-full border border-line px-1 h-7">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateQuantity(line.id, line.quantity + 1)}
|
||||
aria-label="افزایش تعداد"
|
||||
className="flex items-center justify-center size-5 text-body hover:text-brand"
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
</button>
|
||||
<span className="w-5 text-center text-xs font-bold text-ink">
|
||||
{line.quantity}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateQuantity(line.id, line.quantity - 1)}
|
||||
aria-label="کاهش تعداد"
|
||||
className="flex items-center justify-center size-5 text-body hover:text-brand"
|
||||
>
|
||||
<MinusIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-brand whitespace-nowrap">
|
||||
{formatToman(line.price * line.quantity)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeItem(line.id)}
|
||||
aria-label="حذف از سبد خرید"
|
||||
className="self-start p-1 text-body hover:text-red-600"
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{items.length > 0 && (
|
||||
<div className="px-4 py-3 border-t border-line shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm text-body">جمع کل</span>
|
||||
<span className="text-sm font-bold text-ink">{formatToman(total)}</span>
|
||||
</div>
|
||||
<a
|
||||
href={getContinueUrl(items)}
|
||||
className="flex items-center justify-center w-full rounded-full bg-brand text-white text-sm font-bold h-11 hover:bg-brand-dark transition-colors"
|
||||
>
|
||||
ادامه فرآیند خرید
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user