2 Commits

  1. 3
      src/app/new-match/profile/page.tsx
  2. 17
      src/components/Componentes/question-answer-storage.tsx
  3. 311
      src/components/Componentes/token-switcher.tsx
  4. 6
      src/lib/get-submit-path.ts

3
src/app/new-match/profile/page.tsx

@ -22,6 +22,7 @@ import type {
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { getSubmitPath } from "@/lib/get-submit-path";
import { markMatchStarted } from "@/lib/match-start-grace";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
@ -454,6 +455,7 @@ export default function NewMatchProfilePage() {
return;
}
markMatchStarted();
await respondMutation.mutateAsync({ action: "accept" });
}}
>
@ -497,6 +499,7 @@ export default function NewMatchProfilePage() {
return;
}
markMatchStarted();
await respondMutation.mutateAsync({ action: "accept" });
}}
>

17
src/components/Componentes/question-answer-storage.tsx

@ -478,17 +478,12 @@ export function QuestionAnswersProvider({
return;
}
const fullPayload = createPayload(
answersRef.current,
questionsRef.current,
backendFieldsRef.current,
);
const pendingFields = fullPayload.fields.filter((field) =>
dirtyKeysRef.current.has(field.key),
);
const fullPayload = createPayload(answersRef.current, questionsRef.current, backendFieldsRef.current);
const nextDirtyKey = fullPayload.fields.find((field) => dirtyKeysRef.current.has(field.key))?.key;
const payload = {
...fullPayload,
fields: pendingFields,
fields: nextDirtyKey ? fullPayload.fields.filter((field) => field.key === nextDirtyKey) : [],
version: schemaVersionRef.current,
};
const revision = answersRevisionRef.current;
@ -541,9 +536,7 @@ export function QuestionAnswersProvider({
return;
}
pendingFields.forEach((field) => {
dirtyKeysRef.current.delete(field.key);
});
if (nextDirtyKey) dirtyKeysRef.current.delete(nextDirtyKey);
if (dirtyKeysRef.current.size > 0) {
await flushAnswersRef.current();

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

@ -1,54 +1,36 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { MdOutlineSwitchAccount, MdDragIndicator } from "react-icons/md";
import { IoClose, IoTrashOutline, IoKeyOutline } from "react-icons/io5";
import { useEffect, useState } from "react";
import { MdOutlineSwitchAccount } from "react-icons/md";
import { getClientCookie, setClientCookie } from "@/lib/cookies";
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache";
export const FEMALE_TOKEN = "545f61bb3e061ccb9b19f84715eb1b1ed4740331";
export const MALE_TOKEN = "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308";
export const MALE_2_TOKEN = "56cd0fceb93f7cecf7ffa84c590135026cf54a1d";
export const GUEST_MALE_1_TOKEN = "7490550734175c3935c54c808eba4933ee9c7633";
export const GUEST_MALE_2_TOKEN = "88813b0434f2207582f515f2c38de0cafd517fb7";
export const GUEST_FEMALE_1_TOKEN = "d9f0d61b2a7bbea89266eded6ad8d81a112f7ffe";
export const GUEST_FEMALE_2_TOKEN = "da44906159958b3f071096160b509e1479ccbc1a";
export const FEMALE_EMAIL = "m.a.ghorbani01@gmail.com";
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 [mounted, setMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [currentToken, setCurrentToken] = useState<string>("");
const [customTokenInput, setCustomTokenInput] = useState<string>("");
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const dragStart = useRef<{
x: number;
y: number;
startX: number;
startY: number;
hasMoved: boolean;
}>({ x: 0, y: 0, startX: 12, startY: 12, hasMoved: false });
const [isCustomInputOpen, setIsCustomInputOpen] = useState(false);
useEffect(() => {
setMounted(true);
if (typeof window !== "undefined") {
const token =
(window as any).HABIB_TOKEN ??
@ -57,59 +39,15 @@ 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;
const isFemale = currentToken === FEMALE_TOKEN;
const isNoToken = currentToken === "NO_TOKEN" || !currentToken;
const guestTokens = [
GUEST_MALE_1_TOKEN,
GUEST_MALE_2_TOKEN,
GUEST_FEMALE_1_TOKEN,
GUEST_FEMALE_2_TOKEN,
];
const isGuestToken = guestTokens.includes(currentToken);
const isCustomToken =
!isMale && !isMale2 && !isFemale && !isNoToken && !isGuestToken;
!isMale && !isMale2 && !isFemale && !isNoToken;
const handleSelectToken = (newToken: string) => {
const trimmed = newToken.trim();
@ -232,175 +170,36 @@ export function TokenSwitcher({
},
];
const guestAccounts = [
{
token: GUEST_MALE_1_TOKEN,
title: "حساب مهمان آقا ۱",
subtitle: "مهمان / بدون اطلاعات",
emoji: "👨",
isActive: currentToken === GUEST_MALE_1_TOKEN,
},
{
token: GUEST_MALE_2_TOKEN,
title: "حساب مهمان آقا ۲",
subtitle: "مهمان / بدون اطلاعات",
emoji: "👨",
isActive: currentToken === GUEST_MALE_2_TOKEN,
},
{
token: GUEST_FEMALE_1_TOKEN,
title: "حساب مهمان خانم ۱",
subtitle: "مهمان / بدون اطلاعات",
emoji: "👩",
isActive: currentToken === GUEST_FEMALE_1_TOKEN,
},
{
token: GUEST_FEMALE_2_TOKEN,
title: "حساب مهمان خانم ۲",
subtitle: "مهمان / بدون اطلاعات",
emoji: "👩",
isActive: currentToken === GUEST_FEMALE_2_TOKEN,
},
];
// 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
? "خانم 👩"
: isGuestToken
? (guestAccounts.find((g) => g.token === currentToken)?.title ?? "مهمان 👤")
: 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 */}
<MdOutlineSwitchAccount className="size-5 text-rose-500 shrink-0" />
<span className="text-xs font-bold whitespace-nowrap">{currentRoleLabel}</span>
</div>
);
return (
<>
{badgeContent}
<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>
{isOpen && (
<div
@ -492,50 +291,6 @@ export function TokenSwitcher({
</div>
))}
{/* Guest Accounts */}
{guestAccounts.map((acc) => (
<div
key={acc.token}
className={[
"flex items-center justify-between gap-3 p-3 rounded-2xl border transition-all select-none",
acc.isActive
? "border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30"
: "border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 bg-white dark:bg-slate-800/80 hover:bg-slate-50/60 dark:hover:bg-slate-800",
].join(" ")}
>
<div
onClick={() => handleSelectToken(acc.token)}
className="flex-1 min-w-0 flex items-center gap-3 text-right cursor-pointer"
>
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
{acc.emoji}
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
{acc.title}
</span>
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium truncate block leading-tight">
{acc.subtitle}
</span>
<span
className="text-[10px] font-mono text-slate-400 dark:text-slate-500 truncate block text-left leading-tight"
dir="ltr"
>
{acc.token}
</span>
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
{acc.isActive && (
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
)}
</div>
</div>
))}
{/* Custom Active Token Card (if any custom token is set) */}
{isCustomToken && (
<div className="flex items-center justify-between gap-3 p-3 rounded-2xl border border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30 select-none">

6
src/lib/get-submit-path.ts

@ -43,6 +43,9 @@ export function getSubmitPath(
if (caseStatus === "male_accepted") {
if (myAction === "pending") {
if (isWithinMatchStartGrace()) {
return "/request-accepted";
}
return "/new-match";
}
return "/request-sent";
@ -50,6 +53,9 @@ export function getSubmitPath(
if (caseStatus === "introduced") {
if (myAction === "pending") {
if (isWithinMatchStartGrace()) {
return "/request-sent";
}
return "/new-match";
}
return "/finding-match";

Loading…
Cancel
Save