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
+138
View File
@@ -0,0 +1,138 @@
"use client";
import { useEffect, useRef, useState, type MouseEvent } from "react";
import { businessDashboardUrl, customerDashboardUrl } from "@/lib/customer";
import { useSession } from "@/lib/session";
import { IconChevron, IconUser } from "./Icons";
const itemClass =
"block w-full px-3 py-2.5 text-right text-sm text-ink transition-colors hover:bg-brand-soft hover:text-brand";
export function AccountMenu({
displayName,
isWebsiteAdmin,
onLogout,
compact = false,
}: {
displayName: string;
isWebsiteAdmin: boolean;
onLogout: () => void;
compact?: boolean;
}) {
const [open, setOpen] = useState(false);
const [handoffBusy, setHandoffBusy] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const { getAccessToken } = useSession();
useEffect(() => {
if (!open) return;
const onClickOutside = (event: Event) => {
if (menuRef.current && event.target instanceof Node && !menuRef.current.contains(event.target)) {
setOpen(false);
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setOpen(false);
};
document.addEventListener("mousedown", onClickOutside);
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("mousedown", onClickOutside);
document.removeEventListener("keydown", onKeyDown);
};
}, [open]);
async function openBusinessDashboard(event: MouseEvent<HTMLAnchorElement>) {
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
return;
}
event.preventDefault();
if (handoffBusy) return;
const baseUrl = businessDashboardUrl();
const popup = window.open("about:blank", "_blank");
setHandoffBusy(true);
try {
const token = getAccessToken();
const res = await fetch("/api/auth/handoff", {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
});
if (!res.ok) throw new Error("handoff failed");
const data = (await res.json()) as { ticket?: string };
const url = new URL(baseUrl);
if (data.ticket) url.searchParams.set("handoff", data.ticket);
if (popup) {
popup.opener = null;
popup.location.replace(url.toString());
} else {
window.open(url.toString(), "_blank", "noopener,noreferrer");
}
} catch {
if (popup) popup.location.replace(baseUrl);
else window.open(baseUrl, "_blank", "noopener,noreferrer");
} finally {
setHandoffBusy(false);
setOpen(false);
}
}
return (
<div ref={menuRef} className="relative">
<button
type="button"
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
aria-haspopup="menu"
className={`flex max-w-[12rem] items-center gap-2 rounded-full px-3 py-2 text-sm hover:bg-zinc-100 ${
compact ? "w-full justify-between px-0 hover:bg-transparent" : ""
}`}
>
<span className="flex min-w-0 items-center gap-2">
<IconUser />
<span className="truncate font-medium">
{displayName} عزیز
</span>
</span>
<IconChevron className={`w-3.5 h-3.5 shrink-0 transition-transform ${open ? "rotate-90" : "-rotate-90"}`} />
</button>
{open && (
<div
role="menu"
className={`absolute z-50 mt-2 min-w-[11rem] overflow-hidden rounded-2xl border border-black/5 bg-white py-1 shadow-xl ${
compact ? "start-0 end-0" : "start-0"
}`}
>
<a role="menuitem" href={customerDashboardUrl("/profile")} className={itemClass} onClick={() => setOpen(false)}>
پروفایل من
</a>
<a role="menuitem" href={customerDashboardUrl("/orders")} className={itemClass} onClick={() => setOpen(false)}>
سفارشات من
</a>
{isWebsiteAdmin && (
<a
role="menuitem"
href={businessDashboardUrl()}
className={itemClass}
onClick={openBusinessDashboard}
>
{handoffBusy ? "در حال انتقال…" : "مدیریت وبسایت"}
</a>
)}
<button
type="button"
role="menuitem"
className={`${itemClass} text-brand hover:text-[#e51f1f]`}
onClick={() => {
setOpen(false);
onLogout();
}}
>
خروج
</button>
</div>
)}
</div>
);
}