|
|
|
@ -1,7 +1,7 @@ |
|
|
|
"use client"; |
|
|
|
|
|
|
|
import { useEffect, useState } from "react"; |
|
|
|
import { MdOutlineSwitchAccount } from "react-icons/md"; |
|
|
|
import { useEffect, useRef, useState } from "react"; |
|
|
|
import { MdOutlineSwitchAccount, MdDragIndicator } from "react-icons/md"; |
|
|
|
import { getClientCookie, setClientCookie } from "@/lib/cookies"; |
|
|
|
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; |
|
|
|
|
|
|
|
@ -14,22 +14,37 @@ export const MALE_EMAIL = "muhammadamin.ghorbani@gmail.com"; |
|
|
|
export const MALE_2_EMAIL = "habibwabackup@gmail.com"; |
|
|
|
|
|
|
|
const TOKEN_COOKIE_NAME = "HABIB_TOKEN"; |
|
|
|
const STORAGE_POS_KEY = "DEV_TOKEN_SWITCHER_POS"; |
|
|
|
|
|
|
|
type TokenSwitcherProps = { |
|
|
|
variant?: "default" | "transparent"; |
|
|
|
className?: string; |
|
|
|
isFloating?: boolean; |
|
|
|
}; |
|
|
|
|
|
|
|
export function TokenSwitcher({ |
|
|
|
variant = "default", |
|
|
|
className, |
|
|
|
isFloating = true, |
|
|
|
}: TokenSwitcherProps) { |
|
|
|
const [isOpen, setIsOpen] = useState(false); |
|
|
|
const [currentToken, setCurrentToken] = useState<string>(""); |
|
|
|
const [customTokenInput, setCustomTokenInput] = useState<string>(""); |
|
|
|
const [isCustomInputOpen, setIsCustomInputOpen] = useState(false); |
|
|
|
const [mounted, setMounted] = useState(false); |
|
|
|
const [position, setPosition] = useState<{ x: number; y: number } | null>(null); |
|
|
|
const [isDragging, setIsDragging] = useState(false); |
|
|
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null); |
|
|
|
const dragStart = useRef({ |
|
|
|
x: 0, |
|
|
|
y: 0, |
|
|
|
startX: 0, |
|
|
|
startY: 0, |
|
|
|
hasMoved: false, |
|
|
|
}); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
setMounted(true); |
|
|
|
if (typeof window !== "undefined") { |
|
|
|
const token = |
|
|
|
(window as any).HABIB_TOKEN ?? |
|
|
|
@ -38,8 +53,45 @@ export function TokenSwitcher({ |
|
|
|
sessionStorage.getItem(TOKEN_COOKIE_NAME) ?? |
|
|
|
""; |
|
|
|
setCurrentToken(token); |
|
|
|
|
|
|
|
if (isFloating) { |
|
|
|
try { |
|
|
|
const savedPos = localStorage.getItem(STORAGE_POS_KEY); |
|
|
|
if (savedPos) { |
|
|
|
const parsed = JSON.parse(savedPos); |
|
|
|
if (typeof parsed.x === "number" && typeof parsed.y === "number") { |
|
|
|
const clampedX = Math.max(10, Math.min(window.innerWidth - 180, parsed.x)); |
|
|
|
const clampedY = Math.max(10, Math.min(window.innerHeight - 50, parsed.y)); |
|
|
|
setPosition({ x: clampedX, y: clampedY }); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn("Failed to load dev token switcher position:", e); |
|
|
|
} |
|
|
|
// Default initial position (top-left)
|
|
|
|
setPosition({ x: 12, y: 12 }); |
|
|
|
} |
|
|
|
} |
|
|
|
}, []); |
|
|
|
}, [isFloating]); |
|
|
|
|
|
|
|
// Keep floating position bounded on window resize
|
|
|
|
useEffect(() => { |
|
|
|
if (!isFloating) return; |
|
|
|
const handleResize = () => { |
|
|
|
setPosition((prev) => { |
|
|
|
if (!prev) return null; |
|
|
|
const width = containerRef.current?.offsetWidth || 180; |
|
|
|
const height = containerRef.current?.offsetHeight || 50; |
|
|
|
return { |
|
|
|
x: Math.max(10, Math.min(window.innerWidth - width - 10, prev.x)), |
|
|
|
y: Math.max(10, Math.min(window.innerHeight - height - 10, prev.y)), |
|
|
|
}; |
|
|
|
}); |
|
|
|
}; |
|
|
|
window.addEventListener("resize", handleResize); |
|
|
|
return () => window.removeEventListener("resize", handleResize); |
|
|
|
}, [isFloating]); |
|
|
|
|
|
|
|
const isMale = currentToken === MALE_TOKEN; |
|
|
|
const isMale2 = currentToken === MALE_2_TOKEN; |
|
|
|
@ -52,7 +104,6 @@ export function TokenSwitcher({ |
|
|
|
if (!trimmed) return; |
|
|
|
|
|
|
|
if (typeof window !== "undefined") { |
|
|
|
// Clear local storage and session storage so answers and cached state from previous user are removed
|
|
|
|
try { |
|
|
|
window.localStorage.clear(); |
|
|
|
window.sessionStorage.clear(); |
|
|
|
@ -72,7 +123,6 @@ export function TokenSwitcher({ |
|
|
|
setCurrentToken(trimmed); |
|
|
|
setIsOpen(false); |
|
|
|
|
|
|
|
// Navigate to the beginning of the flow (intro page)
|
|
|
|
window.location.replace("/"); |
|
|
|
} |
|
|
|
}; |
|
|
|
@ -88,7 +138,6 @@ export function TokenSwitcher({ |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 1. Clear local storage and session storage completely
|
|
|
|
if (typeof window !== "undefined") { |
|
|
|
try { |
|
|
|
window.localStorage.clear(); |
|
|
|
@ -98,7 +147,6 @@ export function TokenSwitcher({ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 2. Call backend reset script via API route if it is a real DB user
|
|
|
|
if (userId !== null) { |
|
|
|
const response = await fetch("/api/dev-reset-profile", { |
|
|
|
method: "POST", |
|
|
|
@ -115,7 +163,6 @@ export function TokenSwitcher({ |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 3. Set the target token in the cookies and redirect to /
|
|
|
|
const targetToken = |
|
|
|
userId === 17119 |
|
|
|
? MALE_TOKEN |
|
|
|
@ -146,41 +193,150 @@ export function TokenSwitcher({ |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// Pointer drag handlers
|
|
|
|
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => { |
|
|
|
if (!isFloating || isOpen) return; |
|
|
|
|
|
|
|
e.currentTarget.setPointerCapture(e.pointerId); |
|
|
|
setIsDragging(true); |
|
|
|
const startX = position?.x ?? 12; |
|
|
|
const startY = position?.y ?? 12; |
|
|
|
dragStart.current = { |
|
|
|
x: e.clientX, |
|
|
|
y: e.clientY, |
|
|
|
startX, |
|
|
|
startY, |
|
|
|
hasMoved: false, |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => { |
|
|
|
if (!isDragging || !isFloating) return; |
|
|
|
|
|
|
|
const dx = e.clientX - dragStart.current.x; |
|
|
|
const dy = e.clientY - dragStart.current.y; |
|
|
|
|
|
|
|
if (Math.hypot(dx, dy) > 4) { |
|
|
|
dragStart.current.hasMoved = true; |
|
|
|
} |
|
|
|
|
|
|
|
const width = containerRef.current?.offsetWidth || 140; |
|
|
|
const height = containerRef.current?.offsetHeight || 40; |
|
|
|
|
|
|
|
const newX = Math.max( |
|
|
|
10, |
|
|
|
Math.min(window.innerWidth - width - 10, dragStart.current.startX + dx) |
|
|
|
); |
|
|
|
const newY = Math.max( |
|
|
|
10, |
|
|
|
Math.min(window.innerHeight - height - 10, dragStart.current.startY + dy) |
|
|
|
); |
|
|
|
|
|
|
|
setPosition({ x: newX, y: newY }); |
|
|
|
}; |
|
|
|
|
|
|
|
const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => { |
|
|
|
if (!isDragging || !isFloating) return; |
|
|
|
try { |
|
|
|
e.currentTarget.releasePointerCapture(e.pointerId); |
|
|
|
} catch (err) {} |
|
|
|
setIsDragging(false); |
|
|
|
|
|
|
|
if (dragStart.current.hasMoved) { |
|
|
|
if (position) { |
|
|
|
try { |
|
|
|
localStorage.setItem(STORAGE_POS_KEY, JSON.stringify(position)); |
|
|
|
} catch (e) {} |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const handleBadgeClick = (e: React.MouseEvent) => { |
|
|
|
if (dragStart.current.hasMoved) { |
|
|
|
e.preventDefault(); |
|
|
|
e.stopPropagation(); |
|
|
|
return; |
|
|
|
} |
|
|
|
setIsOpen(true); |
|
|
|
}; |
|
|
|
|
|
|
|
const currentRoleLabel = isMale |
|
|
|
? "آقا 👨" |
|
|
|
: isMale2 |
|
|
|
? "آقا ۲ 👨" |
|
|
|
: isFemale |
|
|
|
? "خانم 👩" |
|
|
|
: isNoToken |
|
|
|
? "بدون توکن 👤" |
|
|
|
: "سفارشی 🔑"; |
|
|
|
|
|
|
|
if (!mounted && isFloating) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
const badgeContent = ( |
|
|
|
<div |
|
|
|
ref={containerRef} |
|
|
|
onPointerDown={handlePointerDown} |
|
|
|
onPointerMove={handlePointerMove} |
|
|
|
onPointerUp={handlePointerUp} |
|
|
|
onPointerCancel={() => setIsDragging(false)} |
|
|
|
onClick={handleBadgeClick} |
|
|
|
style={ |
|
|
|
isFloating |
|
|
|
? { |
|
|
|
position: "fixed", |
|
|
|
left: position ? `${position.x}px` : "12px", |
|
|
|
top: position ? `${position.y}px` : "12px", |
|
|
|
zIndex: 99999, |
|
|
|
touchAction: "none", |
|
|
|
userSelect: "none", |
|
|
|
} |
|
|
|
: undefined |
|
|
|
} |
|
|
|
className={[ |
|
|
|
isFloating |
|
|
|
? "fixed flex items-center gap-1.5 px-3 py-2 rounded-[15px] backdrop-blur-md shadow-md border select-none transition-shadow cursor-pointer" |
|
|
|
: "inline-flex items-center gap-1.5 rounded-[15px] px-3 py-2 shadow-md border backdrop-blur-md cursor-pointer", |
|
|
|
variant === "transparent" |
|
|
|
? "bg-black/60 text-white border-white/20 hover:bg-black/70" |
|
|
|
: "bg-white/95 text-slate-800 border-slate-200/80 hover:bg-white dark:bg-slate-900/95 dark:text-slate-100 dark:border-slate-700/80", |
|
|
|
isDragging |
|
|
|
? "cursor-grabbing shadow-2xl ring-2 ring-rose-500/50 scale-[1.02]" |
|
|
|
: "cursor-grab active:scale-95", |
|
|
|
className, |
|
|
|
] |
|
|
|
.filter(Boolean) |
|
|
|
.join(" ")} |
|
|
|
title="تغییر توکن کاربر (تست) - برای جابجایی بکشید / برای انتخاب کلیک کنید" |
|
|
|
> |
|
|
|
{/* Drag handle */} |
|
|
|
{isFloating && ( |
|
|
|
<div |
|
|
|
className="flex items-center justify-center text-slate-400 dark:text-slate-500 shrink-0" |
|
|
|
title="برای جابجایی درگ کنید" |
|
|
|
> |
|
|
|
<MdDragIndicator className="size-4" /> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* Role icon & selected account label only */} |
|
|
|
<MdOutlineSwitchAccount className="size-5 text-rose-500 shrink-0" /> |
|
|
|
<span className="text-xs font-bold whitespace-nowrap">{currentRoleLabel}</span> |
|
|
|
</div> |
|
|
|
); |
|
|
|
|
|
|
|
return ( |
|
|
|
<> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
onClick={() => setIsOpen(true)} |
|
|
|
aria-label="تغییر توکن کاربر (تست)" |
|
|
|
title="تغییر توکن (آقا / خانم)" |
|
|
|
className={[ |
|
|
|
"inline-flex items-center gap-1.5 rounded-[15px] px-3 py-2 text-xs font-bold transition-all active:scale-95 shadow-md border border-slate-200/80 dark:border-slate-700/80 backdrop-blur-md", |
|
|
|
variant === "transparent" |
|
|
|
? "bg-black/40 text-white hover:bg-black/50" |
|
|
|
: "bg-white/95 text-slate-800 hover:bg-white dark:bg-slate-900/95 dark:text-slate-100", |
|
|
|
className, |
|
|
|
] |
|
|
|
.filter(Boolean) |
|
|
|
.join(" ")} |
|
|
|
> |
|
|
|
<MdOutlineSwitchAccount className="size-5 text-rose-500" /> |
|
|
|
<span> |
|
|
|
{isMale |
|
|
|
? "آقا 👨" |
|
|
|
: isMale2 |
|
|
|
? "آقا ۲ 👨" |
|
|
|
: isFemale |
|
|
|
? "خانم 👩" |
|
|
|
: isNoToken |
|
|
|
? "بدون توکن 👤" |
|
|
|
: "سفارشی 🔑"} |
|
|
|
</span> |
|
|
|
</button> |
|
|
|
{badgeContent} |
|
|
|
|
|
|
|
{isOpen && ( |
|
|
|
<div className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/50 p-4 backdrop-blur-xs animate-in fade-in duration-200"> |
|
|
|
<div |
|
|
|
className="fixed inset-0 z-[100000] flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs animate-in fade-in duration-200" |
|
|
|
onClick={() => setIsOpen(false)} |
|
|
|
> |
|
|
|
<div |
|
|
|
className="w-full max-w-sm rounded-2xl bg-white p-5 shadow-xl dark:bg-slate-900 border border-slate-100 dark:border-slate-800 max-h-[90vh] flex flex-col" |
|
|
|
className="w-full max-w-sm rounded-2xl bg-white p-5 shadow-2xl dark:bg-slate-900 border border-slate-100 dark:border-slate-800 max-h-[90vh] flex flex-col" |
|
|
|
onClick={(e) => e.stopPropagation()} |
|
|
|
> |
|
|
|
<div className="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 shrink-0"> |
|
|
|
@ -191,7 +347,7 @@ export function TokenSwitcher({ |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
onClick={() => setIsOpen(false)} |
|
|
|
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-sm p-1" |
|
|
|
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-sm p-1 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" |
|
|
|
> |
|
|
|
✕ |
|
|
|
</button> |
|
|
|
|