Browse Source

feat: add marriage profile page with dynamic fields, consent sheets, and submission path handling

Dev
parent
commit
4c7fd501ce
  1. 3
      src/app/new-match/profile/page.tsx
  2. 12
      src/components/Componentes/question-answer-storage.tsx
  3. 141
      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 { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { getSubmitPath } from "@/lib/get-submit-path"; import { getSubmitPath } from "@/lib/get-submit-path";
import { markMatchStarted } from "@/lib/match-start-grace";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
@ -454,6 +455,7 @@ export default function NewMatchProfilePage() {
return; return;
} }
markMatchStarted();
await respondMutation.mutateAsync({ action: "accept" }); await respondMutation.mutateAsync({ action: "accept" });
}} }}
> >
@ -497,6 +499,7 @@ export default function NewMatchProfilePage() {
return; return;
} }
markMatchStarted();
await respondMutation.mutateAsync({ action: "accept" }); await respondMutation.mutateAsync({ action: "accept" });
}} }}
> >

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

@ -482,10 +482,11 @@ export function QuestionAnswersProvider({
} }
const fullPayload = createPayload(answersRef.current, questionsRef.current, backendFieldsRef.current); const fullPayload = createPayload(answersRef.current, questionsRef.current, backendFieldsRef.current);
const nextDirtyKey = fullPayload.fields.find((field) => dirtyKeysRef.current.has(field.key))?.key;
const dirtyFields = fullPayload.fields.filter((field) => dirtyKeysRef.current.has(field.key));
const fieldsToSend = dirtyFields.length > 0 ? dirtyFields : fullPayload.fields;
const payload = { const payload = {
...fullPayload, ...fullPayload,
fields: nextDirtyKey ? fullPayload.fields.filter((field) => field.key === nextDirtyKey) : [],
fields: fieldsToSend,
version: schemaVersionRef.current, version: schemaVersionRef.current,
}; };
const revision = answersRevisionRef.current; const revision = answersRevisionRef.current;
@ -518,12 +519,7 @@ export function QuestionAnswersProvider({
return; return;
} }
if (nextDirtyKey) dirtyKeysRef.current.delete(nextDirtyKey);
if (dirtyKeysRef.current.size > 0) {
await flushAnswersRef.current();
return;
}
dirtyKeysRef.current.clear();
hasPendingSyncRef.current = false; hasPendingSyncRef.current = false;
setHasPendingSync(false); setHasPendingSync(false);

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

@ -2,42 +2,38 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { MdOutlineSwitchAccount } from "react-icons/md"; import { MdOutlineSwitchAccount } from "react-icons/md";
import {
IoClose,
IoTrashOutline,
IoKeyOutline,
} from "react-icons/io5";
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";
export const FEMALE_TOKEN = "545f61bb3e061ccb9b19f84715eb1b1ed4740331"; export const FEMALE_TOKEN = "545f61bb3e061ccb9b19f84715eb1b1ed4740331";
export const MALE_TOKEN = "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308"; export const MALE_TOKEN = "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308";
export const MALE_2_TOKEN = "56cd0fceb93f7cecf7ffa84c590135026cf54a1d"; 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 FEMALE_EMAIL = "m.a.ghorbani01@gmail.com";
export const MALE_EMAIL = "muhammadamin.ghorbani@gmail.com"; 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);
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 ??
@ -46,59 +42,15 @@ 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;
const isFemale = currentToken === FEMALE_TOKEN; const isFemale = currentToken === FEMALE_TOKEN;
const isNoToken = currentToken === "NO_TOKEN" || !currentToken; 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 = const isCustomToken =
!isMale && !isMale2 && !isFemale && !isNoToken && !isGuestToken;
!isMale && !isMale2 && !isFemale && !isNoToken;
const handleSelectToken = (newToken: string) => { const handleSelectToken = (newToken: string) => {
const trimmed = newToken.trim(); const trimmed = newToken.trim();
@ -221,46 +173,16 @@ 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,
},
];
return ( return (
<> <>
<div className="fixed top-4 left-4 z-[9999]">
<button <button
type="button" type="button"
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
aria-label="تغییر توکن کاربر (تست)" aria-label="تغییر توکن کاربر (تست)"
title="تغییر توکن (آقا / خانم)" title="تغییر توکن (آقا / خانم)"
className={[ 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",
"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 cursor-pointer",
variant === "transparent" variant === "transparent"
? "bg-black/40 text-white hover:bg-black/50" ? "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", : "bg-white/95 text-slate-800 hover:bg-white dark:bg-slate-900/95 dark:text-slate-100",
@ -282,6 +204,7 @@ export function TokenSwitcher({
: "سفارشی 🔑"} : "سفارشی 🔑"}
</span> </span>
</button> </button>
</div>
{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-[10000] flex items-center justify-center bg-black/50 p-4 backdrop-blur-xs animate-in fade-in duration-200">
@ -370,50 +293,6 @@ export function TokenSwitcher({
</div> </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) */} {/* Custom Active Token Card (if any custom token is set) */}
{isCustomToken && ( {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"> <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 (caseStatus === "male_accepted") {
if (myAction === "pending") { if (myAction === "pending") {
if (isWithinMatchStartGrace()) {
return "/request-accepted";
}
return "/new-match"; return "/new-match";
} }
return "/request-sent"; return "/request-sent";
@ -50,6 +53,9 @@ export function getSubmitPath(
if (caseStatus === "introduced") { if (caseStatus === "introduced") {
if (myAction === "pending") { if (myAction === "pending") {
if (isWithinMatchStartGrace()) {
return "/request-sent";
}
return "/new-match"; return "/new-match";
} }
return "/finding-match"; return "/finding-match";

Loading…
Cancel
Save