mortezaei 1 week ago
parent
commit
50b5a5e145
  1. 6
      src/app/layout.tsx
  2. 236
      src/components/Componentes/token-switcher.tsx

6
src/app/layout.tsx

@ -211,11 +211,7 @@ export default function RootLayout({
suppressHydrationWarning suppressHydrationWarning
> >
<Providers> <Providers>
{isDevelopment ? (
<div className="fixed top-3 left-3 z-[9999] pointer-events-auto">
<TokenSwitcher />
</div>
) : null}
{isDevelopment ? <TokenSwitcher /> : null}
<div className="app-shell">{children}</div> <div className="app-shell">{children}</div>
</Providers> </Providers>
{isDevelopment ? <DevClickToComponent /> : null} {isDevelopment ? <DevClickToComponent /> : null}

236
src/components/Componentes/token-switcher.tsx

@ -1,7 +1,7 @@
"use client"; "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 { getClientCookie, setClientCookie } from "@/lib/cookies";
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; 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"; export const MALE_2_EMAIL = "habibwabackup@gmail.com";
const TOKEN_COOKIE_NAME = "HABIB_TOKEN"; const TOKEN_COOKIE_NAME = "HABIB_TOKEN";
const STORAGE_POS_KEY = "DEV_TOKEN_SWITCHER_POS";
type TokenSwitcherProps = { type TokenSwitcherProps = {
variant?: "default" | "transparent"; variant?: "default" | "transparent";
className?: string; className?: string;
isFloating?: boolean;
}; };
export function TokenSwitcher({ export function TokenSwitcher({
variant = "default", variant = "default",
className, className,
isFloating = true,
}: TokenSwitcherProps) { }: TokenSwitcherProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [currentToken, setCurrentToken] = useState<string>(""); const [currentToken, setCurrentToken] = useState<string>("");
const [customTokenInput, setCustomTokenInput] = 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(() => { useEffect(() => {
setMounted(true);
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const token = const token =
(window as any).HABIB_TOKEN ?? (window as any).HABIB_TOKEN ??
@ -38,8 +53,45 @@ export function TokenSwitcher({
sessionStorage.getItem(TOKEN_COOKIE_NAME) ?? sessionStorage.getItem(TOKEN_COOKIE_NAME) ??
""; "";
setCurrentToken(token); 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 isMale = currentToken === MALE_TOKEN;
const isMale2 = currentToken === MALE_2_TOKEN; const isMale2 = currentToken === MALE_2_TOKEN;
@ -52,7 +104,6 @@ export function TokenSwitcher({
if (!trimmed) return; if (!trimmed) return;
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
// Clear local storage and session storage so answers and cached state from previous user are removed
try { try {
window.localStorage.clear(); window.localStorage.clear();
window.sessionStorage.clear(); window.sessionStorage.clear();
@ -72,7 +123,6 @@ export function TokenSwitcher({
setCurrentToken(trimmed); setCurrentToken(trimmed);
setIsOpen(false); setIsOpen(false);
// Navigate to the beginning of the flow (intro page)
window.location.replace("/"); window.location.replace("/");
} }
}; };
@ -88,7 +138,6 @@ export function TokenSwitcher({
} }
try { try {
// 1. Clear local storage and session storage completely
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
try { try {
window.localStorage.clear(); 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) { if (userId !== null) {
const response = await fetch("/api/dev-reset-profile", { const response = await fetch("/api/dev-reset-profile", {
method: "POST", method: "POST",
@ -115,7 +163,6 @@ export function TokenSwitcher({
} }
} }
// 3. Set the target token in the cookies and redirect to /
const targetToken = const targetToken =
userId === 17119 userId === 17119
? MALE_TOKEN ? 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 ( 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 && ( {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 <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()} onClick={(e) => e.stopPropagation()}
> >
<div className="flex items-center justify-between pb-3 border-b border-slate-100 dark:border-slate-800 shrink-0"> <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 <button
type="button" type="button"
onClick={() => setIsOpen(false)} 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> </button>

Loading…
Cancel
Save