2 changed files with 197 additions and 45 deletions
@ -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 = "[email protected]"; |
|||
export const MALE_2_EMAIL = "[email protected]"; |
|||
|
|||
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,26 +193,74 @@ export function TokenSwitcher({ |
|||
} |
|||
}; |
|||
|
|||
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 |
|||
// 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 |
|||
? "آقا ۲ 👨" |
|||
@ -173,14 +268,75 @@ export function TokenSwitcher({ |
|||
? "خانم 👩" |
|||
: isNoToken |
|||
? "بدون توکن 👤" |
|||
: "سفارشی 🔑"} |
|||
</span> |
|||
</button> |
|||
: "سفارشی 🔑"; |
|||
|
|||
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 ( |
|||
<> |
|||
{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="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="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-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> |
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue