Muhammad A. Ghorbani 8 hours ago
parent
commit
82936af95d
  1. 12
      src/app/globals.css
  2. 1
      src/app/layout.tsx
  3. 33
      src/app/new-match/new-match-client.tsx
  4. 6
      src/app/questions-list/[slug]/question-detail-client.tsx
  5. 54
      src/app/questions-list/questions-list-client.tsx
  6. 20
      src/app/request-accepted/request-accepted-client.tsx
  7. 4
      src/components/Componentes/currency-sheet.tsx
  8. 2
      src/components/Componentes/error-toast.tsx
  9. 2
      src/components/Componentes/fix-to-the-end.tsx
  10. 11
      src/components/Componentes/question-birthplace.tsx
  11. 82
      src/components/Componentes/question-checkbox.tsx
  12. 4
      src/components/Componentes/question-date-sheet.tsx
  13. 22
      src/components/Componentes/question-date.test.tsx
  14. 3
      src/components/Componentes/question-number.test.tsx
  15. 4
      src/components/Componentes/question-phone.tsx
  16. 6
      src/components/Componentes/question-radio.tsx
  17. 17
      src/components/Componentes/question-renderer.tsx
  18. 48
      src/components/Componentes/question-sheet.test.tsx
  19. 4
      src/components/Componentes/question-sheet.tsx
  20. 13
      src/components/Componentes/question-slider.tsx
  21. 2
      src/components/Componentes/question-textarea.tsx
  22. 49
      src/components/Componentes/question-viewport-coordinator.ts
  23. 4
      src/components/Componentes/slider-page.tsx
  24. 2
      src/components/Componentes/slider-slide-one.tsx
  25. 19
      src/components/Componentes/test-questions-flow.tsx
  26. 5
      src/hooks/marriage/use-cattell.ts
  27. 10
      src/hooks/marriage/use-glasser.ts
  28. 38
      src/hooks/marriage/use-section-data.ts
  29. 8
      src/lib/marriage-field-formatter.ts
  30. 52
      src/lib/ssr-fetch.ts

12
src/app/globals.css

@ -237,12 +237,12 @@ html:lang(ar) body,
} }
} }
body[data-page-background="none"] .app-shell {
background-image: none;
}
.app-shell:has(.page-background-none) {
background-image: none;
body[data-page-background="none"] .app-shell,
.app-shell:has(.page-background-none),
body:has(.page-background-none),
html:has(.page-background-none) {
background-image: none !important;
background-color: #F7F1F0 !important;
} }
body[data-page-background="custom"] .app-shell { body[data-page-background="custom"] .app-shell {

1
src/app/layout.tsx

@ -62,6 +62,7 @@ export const viewport: Viewport = {
userScalable: false, userScalable: false,
viewportFit: "cover", viewportFit: "cover",
themeColor: "#ffffff", themeColor: "#ffffff",
interactiveWidget: "overlays-content",
}; };
export default async function RootLayout({ export default async function RootLayout({

33
src/app/new-match/new-match-client.tsx

@ -20,6 +20,7 @@ import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, { import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay, useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay"; } from "@/components/Componentes/marriage-advisors-overlay";
import ErrorToast from "@/components/Componentes/error-toast";
import MatchProfileOverlay, { import MatchProfileOverlay, {
useMatchProfileOverlay, useMatchProfileOverlay,
} from "@/components/Componentes/match-profile-overlay"; } from "@/components/Componentes/match-profile-overlay";
@ -118,6 +119,7 @@ const fieldCandidateMatchers = {
"career", "career",
], ],
hobbies: [ hobbies: [
"what_are_your_main_hobbies_and_interests",
"your_hobbies_and_main_interests", "your_hobbies_and_main_interests",
"hobbies_and_interests", "hobbies_and_interests",
"hobbies", "hobbies",
@ -502,6 +504,8 @@ export default function NewMatchClient() {
const [paymentError, setPaymentError] = useState<string | null>(null); const [paymentError, setPaymentError] = useState<string | null>(null);
const [isInsufficientCoins, setIsInsufficientCoins] = useState(false); const [isInsufficientCoins, setIsInsufficientCoins] = useState(false);
const [showPaymentSuccessToast, setShowPaymentSuccessToast] = useState(false);
const [appliedDiscount, setAppliedDiscount] = const [appliedDiscount, setAppliedDiscount] =
useState<CheckDiscountResult | null>(null); useState<CheckDiscountResult | null>(null);
@ -539,6 +543,7 @@ export default function NewMatchClient() {
discountCode: appliedDiscount?.valid ? appliedDiscount.code : undefined, discountCode: appliedDiscount?.valid ? appliedDiscount.code : undefined,
}); });
setIsPaymentSheetOpen(false); setIsPaymentSheetOpen(false);
setShowPaymentSuccessToast(true);
openProfile(); openProfile();
} catch (err: any) { } catch (err: any) {
console.error("Payment failed", err); console.error("Payment failed", err);
@ -629,12 +634,14 @@ export default function NewMatchClient() {
<> <>
<PageBackground /> <PageBackground />
<main <main
style={{
paddingTop: `${Math.max(12, top + 4)}px`,
}}
className="-mx-[17px] flex min-h-dvh flex-col px-[18px] text-center" className="-mx-[17px] flex min-h-dvh flex-col px-[18px] text-center"
> >
<div className="shrink-0 mb-1">
<div
style={{
paddingTop: `${Math.max(12, top + 4)}px`,
}}
className="sticky top-0 z-30 -mx-[18px] px-[18px] pb-2 bg-[#F7F1F0]/90 backdrop-blur-md"
>
<PageHeader profile={profile} /> <PageHeader profile={profile} />
</div> </div>
@ -716,12 +723,14 @@ export default function NewMatchClient() {
<PageBackground /> <PageBackground />
<main <main
style={{
paddingTop: `${Math.max(12, top + 4)}px`,
}}
className="-mx-[17px] flex min-h-dvh flex-col px-[18px] text-center" className="-mx-[17px] flex min-h-dvh flex-col px-[18px] text-center"
> >
<div className="shrink-0 mb-1">
<div
style={{
paddingTop: `${Math.max(12, top + 4)}px`,
}}
className="sticky top-0 z-30 -mx-[18px] px-[18px] pb-2 bg-[#F7F1F0]/90 backdrop-blur-md"
>
<PageHeader profile={profile} /> <PageHeader profile={profile} />
</div> </div>
@ -1166,6 +1175,14 @@ export default function NewMatchClient() {
<MarriageAdvisorsOverlay open={isAdvisorOpen} onClose={closeAdvisors} /> <MarriageAdvisorsOverlay open={isAdvisorOpen} onClose={closeAdvisors} />
<MatchProfileOverlay open={isProfileOpen} onClose={closeProfile} /> <MatchProfileOverlay open={isProfileOpen} onClose={closeProfile} />
{showPaymentSuccessToast && (
<ErrorToast
variant="success"
message={t["Payment successful"] || "Payment successful"}
onClose={() => setShowPaymentSuccessToast(false)}
/>
)}
</> </>
); );
} }

6
src/app/questions-list/[slug]/question-detail-client.tsx

@ -270,12 +270,16 @@ export default function QuestionDetailClient({
const profileId = useCurrentProfileId(); const profileId = useCurrentProfileId();
const handleExit = useCallback(() => { const handleExit = useCallback(() => {
void queryClient.invalidateQueries({
queryKey: [...marriageQueryKeys.all, "form-overview"],
exact: false,
});
if (onClose) { if (onClose) {
onClose(); onClose();
return; return;
} }
router.replace(questionsListHref); router.replace(questionsListHref);
}, [onClose, questionsListHref, router]);
}, [onClose, questionsListHref, router, queryClient]);
// Hardware back in the detail page = navigate back to questions list. // Hardware back in the detail page = navigate back to questions list.
// QuestionAnswersProvider's pagehide/unmount safety net will flush // QuestionAnswersProvider's pagehide/unmount safety net will flush

54
src/app/questions-list/questions-list-client.tsx

@ -18,8 +18,8 @@ import QuestionCard from "@/components/Componentes/question-card";
import RequiredStepsCard from "@/components/Componentes/required-steps-card"; import RequiredStepsCard from "@/components/Componentes/required-steps-card";
import type { MarriageField } from "@/hooks/marriage/types"; import type { MarriageField } from "@/hooks/marriage/types";
import { marriageQueryKeys } from "@/hooks/marriage/query-keys"; import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import { getCattellQuestions } from "@/hooks/marriage/use-cattell";
import { getGlasserQuestions } from "@/hooks/marriage/use-glasser";
import { getCattellQuestions, useCattellResultQuery } from "@/hooks/marriage/use-cattell";
import { getGlasserQuestions, useGlasserResultQuery } from "@/hooks/marriage/use-glasser";
import { import {
getFormSection, getFormSection,
useFormOverviewQuery, useFormOverviewQuery,
@ -97,6 +97,14 @@ export default function QuestionsListClient() {
"profile", "profile",
locale, locale,
); );
const { data: glasserResult } = useGlasserResultQuery(locale, {
enabled: Boolean(profile?.id),
retry: false,
});
const { data: cattellResult } = useCattellResultQuery(locale, {
enabled: Boolean(profile?.id),
retry: false,
});
const isSectionsLoading = false; const isSectionsLoading = false;
@ -205,11 +213,46 @@ export default function QuestionsListClient() {
const profileId = profile?.id; const profileId = profile?.id;
for (const slug of ["personality_test", "glasser_5_needs_test"]) { for (const slug of ["personality_test", "glasser_5_needs_test"]) {
try { try {
// 1. Check backend test results
if (
(slug === "glasser_5_needs_test" && Boolean(glasserResult)) ||
(slug === "personality_test" && Boolean(cattellResult))
) {
next.set(slug, 100);
continue;
}
// 2. Check local completion flag
if (typeof window !== "undefined") {
const completionKeys = [
profileId ? `marriage:user:${profileId}:sections:${slug}:completed` : null,
`marriage:sections:${slug}:completed`,
`marriage:sections:${slug}:answers`,
].filter(Boolean) as string[];
let isLocalCompleted = false;
for (const key of completionKeys) {
const raw = window.localStorage.getItem(key);
if (raw) {
try {
const parsed = JSON.parse(raw);
if (parsed?.completed === true) {
next.set(slug, 100);
isLocalCompleted = true;
break;
}
} catch {}
}
}
if (isLocalCompleted) continue;
}
// 3. If not completed, read draft progress
let draft: any = null; let draft: any = null;
if (profileId) { if (profileId) {
draft = readScopedAssessmentDraft(profileId, slug); draft = readScopedAssessmentDraft(profileId, slug);
} }
if (!draft) {
if (!draft && typeof window !== "undefined") {
const raw = window.localStorage.getItem(`marriage:tests:${slug}:draft`); const raw = window.localStorage.getItem(`marriage:tests:${slug}:draft`);
draft = raw ? JSON.parse(raw) : null; draft = raw ? JSON.parse(raw) : null;
} }
@ -220,7 +263,7 @@ export default function QuestionsListClient() {
} }
} }
setLocalAssessmentProgress(next); setLocalAssessmentProgress(next);
}, [overview, profile?.id]);
}, [overview, profile?.id, glasserResult, cattellResult]);
const sectionProgressBySlug = useMemo(() => { const sectionProgressBySlug = useMemo(() => {
const progressBySlug = new Map<string, number>(); const progressBySlug = new Map<string, number>();
@ -236,8 +279,7 @@ export default function QuestionsListClient() {
); );
} }
localAssessmentProgress.forEach((progress, slug) => { localAssessmentProgress.forEach((progress, slug) => {
if ((progressBySlug.get(slug) ?? 0) < 100)
progressBySlug.set(slug, progress);
progressBySlug.set(slug, progress);
}); });
// Add fallback for combined section from the schema adapter which attaches it to questionListItems directly // Add fallback for combined section from the schema adapter which attaches it to questionListItems directly

20
src/app/request-accepted/request-accepted-client.tsx

@ -9,6 +9,7 @@ import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, { import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay, useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay"; } from "@/components/Componentes/marriage-advisors-overlay";
import ErrorToast from "@/components/Componentes/error-toast";
import MatchProfileOverlay, { import MatchProfileOverlay, {
useMatchProfileOverlay, useMatchProfileOverlay,
} from "@/components/Componentes/match-profile-overlay"; } from "@/components/Componentes/match-profile-overlay";
@ -199,6 +200,7 @@ export default function RequestAcceptedClient() {
useState(false); useState(false);
const [hasConfirmedFemaleContact, setHasConfirmedFemaleContact] = const [hasConfirmedFemaleContact, setHasConfirmedFemaleContact] =
useState(false); useState(false);
const [showPaymentSuccessToast, setShowPaymentSuccessToast] = useState(false);
const profileHref = localizePath("/new-match/profile", locale); const profileHref = localizePath("/new-match/profile", locale);
const { data: profile, isLoading } = useMarriageProfileQuery({ const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000, refetchInterval: 3000,
@ -260,6 +262,7 @@ export default function RequestAcceptedClient() {
: caseStatus === "payment_done" || caseStatus === "contacted" : caseStatus === "payment_done" || caseStatus === "contacted"
? t["Contact info released"] ? t["Contact info released"]
: t["Request approved!"]; : t["Request approved!"];
const primaryActionText = isFemaleProfile const primaryActionText = isFemaleProfile
? t["No Contact"] || t["No Contact Received"] || "No Contact" ? t["No Contact"] || t["No Contact Received"] || "No Contact"
: t["View profile"]; : t["View profile"];
@ -272,6 +275,14 @@ export default function RequestAcceptedClient() {
contactInfoQuery.data?.contact_info, contactInfoQuery.data?.contact_info,
); );
const handlePrimaryAction = () => {
if (isFemaleProfile) {
setIsDismissReasonSheetOpen(true);
} else {
openProfile();
}
};
const handleSecondaryAction = async () => { const handleSecondaryAction = async () => {
if (isFemaleProfile) { if (isFemaleProfile) {
setIsContactReceivedConfirmOpen(true); setIsContactReceivedConfirmOpen(true);
@ -314,6 +325,7 @@ export default function RequestAcceptedClient() {
} }
setIsSubscriptionSheetOpen(false); setIsSubscriptionSheetOpen(false);
setShowPaymentSuccessToast(true);
if (caseId) { if (caseId) {
await contactInfoQuery.refetch(); await contactInfoQuery.refetch();
@ -773,6 +785,14 @@ export default function RequestAcceptedClient() {
open={isProfileOpen} open={isProfileOpen}
onClose={closeProfile} onClose={closeProfile}
/> />
{showPaymentSuccessToast && (
<ErrorToast
variant="success"
message={t["Payment successful"] || "Payment successful"}
onClose={() => setShowPaymentSuccessToast(false)}
/>
)}
</> </>
); );
} }

4
src/components/Componentes/currency-sheet.tsx

@ -148,8 +148,8 @@ export function CurrencySheet({
].join(" ")} ].join(" ")}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 pt-3 pb-3 border-b border-[#F2F4F7]">
<h3 className="text-[17px] font-bold text-[#181818] truncate pr-2">
<div className="flex items-center justify-between gap-3 px-5 pt-3 pb-3 border-b border-[#F2F4F7]">
<h3 className="flex-1 min-w-0 text-[17px] font-bold leading-snug text-[#181818] line-clamp-2 break-words text-start">
{sheetTitle} {sheetTitle}
</h3> </h3>
<button <button

2
src/components/Componentes/error-toast.tsx

@ -51,7 +51,7 @@ export default function ErrorToast({
return ( return (
<div <div
className={[ className={[
"fixed top-3.5 left-1/2 z-50 flex w-[calc(100%-32px)] max-w-[360px] -translate-x-1/2 items-start justify-between gap-3 rounded-[16px] bg-[#141414]/95 p-3.5 px-4 text-white shadow-[0_10px_30px_rgba(0,0,0,0.35)] backdrop-blur-md border border-white/5 border-t-[2.5px] transition-all duration-300 ease-out",
"fixed top-3.5 left-1/2 z-[70] flex w-[calc(100%-32px)] max-w-[360px] -translate-x-1/2 items-start justify-between gap-3 rounded-[16px] bg-[#141414]/95 p-3.5 px-4 text-white shadow-[0_10px_30px_rgba(0,0,0,0.35)] backdrop-blur-md border border-white/5 border-t-[2.5px] transition-all duration-300 ease-out",
getBorderColor(), getBorderColor(),
isVisible isVisible
? "translate-y-0 opacity-100 scale-100" ? "translate-y-0 opacity-100 scale-100"

2
src/components/Componentes/fix-to-the-end.tsx

@ -12,7 +12,7 @@ export function FixToTheEnd({ children, className }: FixToTheEndProps) {
<div <div
style={{ paddingBottom: "calc(16px + var(--safe-bottom))" }} style={{ paddingBottom: "calc(16px + var(--safe-bottom))" }}
className={[ className={[
"fixed inset-x-0 bottom-0 z-20 mx-auto w-full sm:max-w-[375px] bg-[#F5F5F5]/95 px-[17px] pt-3 backdrop-blur-md",
"question-fix-to-end fixed inset-x-0 bottom-0 z-20 mx-auto w-full sm:max-w-[375px] bg-[#F5F5F5]/95 px-[17px] pt-3 backdrop-blur-md",
className, className,
] ]
.filter(Boolean) .filter(Boolean)

11
src/components/Componentes/question-birthplace.tsx

@ -155,7 +155,12 @@ export function QuestionBirthplace({
const { getAnswerValue, setAnswerValue, isLoading } = useQuestionAnswers(); const { getAnswerValue, setAnswerValue, isLoading } = useQuestionAnswers();
const rawValue = getAnswerValue(question); const rawValue = getAnswerValue(question);
const isResidence = question.ui_config?.enable_geoip === true;
const isResidence =
question.ui_config?.enable_geoip === true ||
question.ui_config?.location_kind === "residence" ||
question.id?.includes("residence") ||
question.id?.includes("living_area") ||
question.id?.includes("location");
const storedRegion = isResidence ? getStoredUserGeoRegion() : null; const storedRegion = isResidence ? getStoredUserGeoRegion() : null;
const initial = parseValue(rawValue); const initial = parseValue(rawValue);
@ -707,8 +712,8 @@ export function QuestionBirthplace({
</div> </div>
{/* Header with Title and Close Button */} {/* Header with Title and Close Button */}
<div className="flex items-center justify-between px-5 pt-2 pb-3 border-b border-[#F2F4F7]">
<h3 className="text-[17px] font-bold text-[#181818] truncate pr-2">
<div className="flex items-center justify-between gap-3 px-5 pt-2.5 pb-3 border-b border-[#F2F4F7]">
<h3 className="flex-1 min-w-0 text-[17px] font-bold leading-snug text-[#181818] line-clamp-2 break-words text-start">
{selectCountryPlaceholder} {selectCountryPlaceholder}
</h3> </h3>
<button <button

82
src/components/Componentes/question-checkbox.tsx

@ -23,8 +23,81 @@ export function QuestionCheckbox({
? [String(rawValue)] ? [String(rawValue)]
: []; : [];
if (options.length === 0) {
return null;
const isSingleCheckbox = options.length === 0;
if (isSingleCheckbox) {
const isChecked = Boolean(
rawValue === true ||
rawValue === "true" ||
rawValue === "confirmed" ||
(Array.isArray(rawValue) && rawValue.length > 0),
);
const toggleSingle = () => {
setAnswerValue(question, isChecked ? null : ["confirmed"]);
};
const labelText =
question.description ||
question.title ||
"تأیید می‌کنم که نام، سن، تصویر و اطلاعات هویتی من با مدارک بارگذاری‌شده مطابقت دارد.";
return (
<div
className={[
"flex w-full flex-col gap-3.5 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
<div className="flex flex-col gap-3.5 pt-1">
<label
htmlFor={`checkbox-${question.id}`}
className={[
"cursor-pointer rounded-[16px] font-semibold text-[15px] transition-all duration-200 active:scale-[0.99] flex items-center gap-3 w-full px-5 py-4 text-start leading-snug",
isChecked
? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]"
: "bg-[#FAFAFA] text-[#181818] hover:bg-white shadow-[0_2px_6px_rgba(0,0,0,0.03)] border border-[#F0EDED]",
].join(" ")}
>
<input
type="checkbox"
id={`checkbox-${question.id}`}
checked={isChecked}
disabled={disabled}
onChange={toggleSingle}
className="sr-only"
/>
<div
className={[
"size-[18px] shrink-0 rounded-[5px] border-[2px] transition-all flex items-center justify-center",
isChecked
? "border-white bg-white text-[#F0445B]"
: "border-[#D0D5DD] bg-white text-transparent",
].join(" ")}
>
{isChecked && (
<svg
width="10"
height="8"
viewBox="0 0 10 8"
fill="none"
className="stroke-current stroke-[2]"
>
<path
d="M1 4L3.5 6.5L9 1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<span className="flex-1">{labelText}</span>
</label>
</div>
</div>
);
} }
const toggleOption = (optionId: string) => { const toggleOption = (optionId: string) => {
@ -40,9 +113,10 @@ export function QuestionCheckbox({
); );
}; };
// Render horizontally only for 2-option binary choices
const isShortOptions = const isShortOptions =
options.length <= 4 &&
options.every((opt) => String(opt.label || opt.value || "").length <= 15);
options.length <= 2 &&
options.every((opt) => String(opt.label || opt.value || "").length <= 10);
return ( return (
<div <div

4
src/components/Componentes/question-date-sheet.tsx

@ -294,8 +294,8 @@ export function QuestionDateSheet({
isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface", isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface",
].join(" ")} ].join(" ")}
> >
<div className="flex items-center justify-between border-b border-[#F2F4F7] px-5 pb-3 pt-2">
<h3 className="truncate pr-2 text-[17px] font-bold text-[#181818]">
<div className="flex items-center justify-between gap-3 border-b border-[#F2F4F7] px-5 pb-3 pt-2.5">
<h3 className="flex-1 min-w-0 text-[17px] font-bold leading-snug text-[#181818] line-clamp-2 break-words text-start">
{question.title} {question.title}
</h3> </h3>
<button <button

22
src/components/Componentes/question-date.test.tsx

@ -70,4 +70,26 @@ describe("QuestionDate", () => {
expect(screen.queryByText("Age")).toBeNull(); expect(screen.queryByText("Age")).toBeNull();
expect(screen.getByText("Select date")).toBeDefined(); expect(screen.getByText("Select date")).toBeDefined();
}); });
it("renders long date question title in sheet header with line-clamp-2 and without truncate", () => {
const longTitle =
"تاریخ دقیق تولد شما بر اساس مدارک شناسایی رسمی و شناسنامه معتبر";
const longDateQ = {
...question,
title: longTitle,
};
render(<QuestionDate question={longDateQ} />);
fireEvent.click(screen.getByRole("button", { name: /select date/i }));
const dialog = screen.getByRole("dialog");
expect(dialog).toBeDefined();
const heading = dialog.querySelector("h3");
expect(heading).toBeDefined();
expect(heading?.textContent?.trim()).toBe(longTitle);
expect(heading).toHaveClass("line-clamp-2");
expect(heading).toHaveClass("flex-1");
expect(heading).not.toHaveClass("truncate");
});
}); });

3
src/components/Componentes/question-number.test.tsx

@ -40,10 +40,11 @@ describe("QuestionNumber Component", () => {
type: "number", type: "number",
order: 1, order: 1,
required: true, required: true,
baseRequired: true,
isVisible: true, isVisible: true,
description: "", description: "",
tooltip: "", tooltip: "",
extras: { placeHolder: "e.g. 3", range: [0, 20] },
extras: { placeHolder: "e.g. 3", range: [0, 20], options: [] },
options: [], options: [],
ui_config: {}, ui_config: {},
}; };

4
src/components/Componentes/question-phone.tsx

@ -816,8 +816,8 @@ export function QuestionPhone({
].join(" ")} ].join(" ")}
> >
{/* Header with Title and Close Button */} {/* Header with Title and Close Button */}
<div className="flex items-center justify-between px-5 pt-2 pb-3 border-b border-[#F2F4F7]">
<h3 className="text-[17px] font-bold text-[#181818] truncate pr-2">
<div className="flex items-center justify-between gap-3 px-5 pt-2.5 pb-3 border-b border-[#F2F4F7]">
<h3 className="flex-1 min-w-0 text-[17px] font-bold leading-snug text-[#181818] line-clamp-2 break-words text-start">
{selectCountryTitle} {selectCountryTitle}
</h3> </h3>
<button <button

6
src/components/Componentes/question-radio.tsx

@ -22,10 +22,10 @@ export function QuestionRadio({
return null; return null;
} }
// Render horizontally if all options are short (e.g. Single, Divorced, Widowed)
// Render horizontally only for 2-option binary choices (e.g. Yes/No, Male/Female)
const isShortOptions = const isShortOptions =
options.length <= 4 &&
options.every((opt) => String(opt.label || opt.value || "").length <= 15);
options.length <= 2 &&
options.every((opt) => String(opt.label || opt.value || "").length <= 10);
return ( return (
<div <div

17
src/components/Componentes/question-renderer.tsx

@ -33,6 +33,8 @@ export function QuestionRenderer({
switch (question.type) { switch (question.type) {
case "birthplace": case "birthplace":
case "location":
case "residence":
return ( return (
<QuestionBirthplace <QuestionBirthplace
question={question} question={question}
@ -117,6 +119,7 @@ export function QuestionRenderer({
); );
case "slider": case "slider":
case "scale": case "scale":
case "range":
return ( return (
<QuestionSlider <QuestionSlider
question={question} question={question}
@ -124,6 +127,20 @@ export function QuestionRenderer({
/> />
); );
case "text": case "text":
if (
question.ui_config?.enable_geoip === true ||
question.ui_config?.location_kind === "residence" ||
question.id?.includes("approximate_location_of_current_living_area") ||
question.id?.includes("current_residence") ||
question.id?.includes("living_area")
) {
return (
<QuestionBirthplace
question={question}
disabled={disabled}
/>
);
}
return ( return (
<QuestionText <QuestionText
question={question} question={question}

48
src/components/Componentes/question-sheet.test.tsx

@ -433,4 +433,52 @@ describe("QuestionSheet component", () => {
expect(screen.queryByText("Antigua and Barbuda")).toBeNull(); expect(screen.queryByText("Antigua and Barbuda")).toBeNull();
expect(screen.queryByText("افغانستان")).toBeNull(); expect(screen.queryByText("افغانستان")).toBeNull();
}); });
it("renders long question title with line-clamp-2 and flex-1 classes in sheet header", () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const longTitle =
"میزان پایبندی شما به مناسک و تکالیف شرعی و اعتقادات مذهبی در زندگی روزمره خانوادگی و اجتماعی";
const qLong = {
id: "religious_commitment_details",
title: longTitle,
type: "dropdown",
order: 1,
required: true,
isVisible: true,
description: "",
tooltip: "",
extras: { placeHolder: "انتخاب کنید" },
options: [
{ id: "high", value: "high", label: "بسیار زیاد", order: 1 },
{ id: "medium", value: "medium", label: "متوسط", order: 2 },
],
ui_config: {},
} as QuestionField;
render(
<QueryClientProvider client={queryClient}>
<QuestionAnswersProvider slug="test" questions={[qLong]}>
<QuestionSheet question={qLong} />
</QuestionAnswersProvider>
</QueryClientProvider>,
);
const trigger = screen.getByRole("button", { name: "انتخاب کنید" });
fireEvent.click(trigger);
const dialog = screen.getByRole("dialog");
expect(dialog).toBeDefined();
const heading = dialog.querySelector("h3");
expect(heading).toBeDefined();
expect(heading?.textContent?.trim()).toBe(longTitle);
expect(heading).toHaveClass("line-clamp-2");
expect(heading).toHaveClass("flex-1");
expect(heading).not.toHaveClass("truncate");
});
}); });

4
src/components/Componentes/question-sheet.tsx

@ -436,8 +436,8 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface", isClosing ? "flutter-sheet-surface-exit" : "flutter-sheet-surface",
].join(" ")} ].join(" ")}
> >
<div className="flex items-center justify-between px-5 pt-2 pb-3 border-b border-[#F2F4F7]">
<h3 className="text-[17px] font-bold text-[#181818] truncate pr-2">
<div className="flex items-center justify-between gap-3 px-5 pt-2.5 pb-3 border-b border-[#F2F4F7]">
<h3 className="flex-1 min-w-0 text-[17px] font-bold leading-snug text-[#181818] line-clamp-2 break-words text-start">
{question.title} {question.title}
</h3> </h3>
<button <button

13
src/components/Componentes/question-slider.tsx

@ -86,7 +86,10 @@ export function QuestionSlider({
? Number(question.extras.placeHolder) ? Number(question.extras.placeHolder)
: Math.round((min + max) / 2); : Math.round((min + max) / 2);
const isDesiredAgeRange = false;
const isDesiredAgeRange =
question.type === "range" ||
(question.id || "").toLowerCase().includes("desired_age") ||
(question.id || "").toLowerCase().includes("age_range");
const thumbWidth = 18; const thumbWidth = 18;
@ -124,7 +127,7 @@ export function QuestionSlider({
})(); })();
// Multi-state logic for Age Range Slider // Multi-state logic for Age Range Slider
let fromVal = 25;
let fromVal = 20;
let toVal = 30; let toVal = 30;
if (isDesiredAgeRange) { if (isDesiredAgeRange) {
if (typeof storedValue === "string" && storedValue.includes("-")) { if (typeof storedValue === "string" && storedValue.includes("-")) {
@ -140,6 +143,12 @@ export function QuestionSlider({
} else if (typeof storedValue === "number") { } else if (typeof storedValue === "number") {
fromVal = storedValue; fromVal = storedValue;
toVal = Math.max(storedValue, max); toVal = Math.max(storedValue, max);
} else if (typeof question.extras?.placeHolder === "string") {
const match = question.extras.placeHolder.match(/(\d+)[^\d]+(\d+)/);
if (match) {
fromVal = Number(match[1]);
toVal = Number(match[2]);
}
} }
fromVal = Math.max(min, Math.min(max, fromVal)); fromVal = Math.max(min, Math.min(max, fromVal));
toVal = Math.max(min, Math.min(max, toVal)); toVal = Math.max(min, Math.min(max, toVal));

2
src/components/Componentes/question-textarea.tsx

@ -88,7 +88,7 @@ export function QuestionTextarea({
placeholder={question.extras.placeHolder} placeholder={question.extras.placeHolder}
disabled={disabled} disabled={disabled}
rows={5} rows={5}
className="h-[270px]"
className="h-[180px] sm:h-[220px] resize-none"
/> />
{description ? ( {description ? (
<span className="block group-10 font-semibold text-[#747474]"> <span className="block group-10 font-semibold text-[#747474]">

49
src/components/Componentes/question-viewport-coordinator.ts

@ -29,11 +29,16 @@ const NON_KEYBOARD_INPUT_TYPES = new Set([
"submit", "submit",
]); ]);
const DEFAULT_TOP_GAP = 48;
const DEFAULT_BOTTOM_GAP = 16;
export type QuestionLiftGeometry = { export type QuestionLiftGeometry = {
baseTop: number; baseTop: number;
baseBottom: number; baseBottom: number;
visibleTop: number; visibleTop: number;
visibleBottom: number; visibleBottom: number;
topGap?: number;
bottomGap?: number;
gap?: number; gap?: number;
}; };
@ -42,18 +47,28 @@ export function computeQuestionLift({
baseBottom, baseBottom,
visibleTop, visibleTop,
visibleBottom, visibleBottom,
topGap,
bottomGap,
gap = QUESTION_GAP, gap = QUESTION_GAP,
}: QuestionLiftGeometry) { }: QuestionLiftGeometry) {
const actualTopGap = topGap ?? gap ?? DEFAULT_TOP_GAP;
const actualBottomGap = bottomGap ?? gap ?? DEFAULT_BOTTOM_GAP;
const contentHeight = baseBottom - baseTop; const contentHeight = baseBottom - baseTop;
const availableTop = visibleTop + gap;
const availableBottom = visibleBottom - gap;
const availableTop = visibleTop + actualTopGap;
const availableBottom = visibleBottom - actualBottomGap;
const availableCenter = (availableTop + availableBottom) / 2; const availableCenter = (availableTop + availableBottom) / 2;
const contentCenter = baseTop + contentHeight / 2; const contentCenter = baseTop + contentHeight / 2;
const centerShift = Math.max(0, contentCenter - availableCenter); const centerShift = Math.max(0, contentCenter - availableCenter);
const overlapShift = Math.max(0, baseBottom - availableBottom); const overlapShift = Math.max(0, baseBottom - availableBottom);
const maxShift = Math.max(0, baseTop - availableTop); const maxShift = Math.max(0, baseTop - availableTop);
return Math.min(Math.max(centerShift, overlapShift), maxShift);
// When content is taller than available space, don't shove top to the extreme limit;
// prioritize keeping badge and title readable with comfortable top clearance.
const idealShift = contentHeight > (availableBottom - availableTop)
? Math.min(overlapShift, maxShift)
: Math.max(centerShift, overlapShift);
return Math.min(idealShift, maxShift);
} }
function isKeyboardInputTarget( function isKeyboardInputTarget(
@ -94,7 +109,8 @@ function setLift(nextLift: number) {
const prevLift = currentLift; const prevLift = currentLift;
currentLift = Math.max(0, Math.round(nextLift)); currentLift = Math.max(0, Math.round(nextLift));
document.documentElement.style.setProperty(LIFT_VARIABLE, `${currentLift}px`); document.documentElement.style.setProperty(LIFT_VARIABLE, `${currentLift}px`);
const isOpen = currentLift > 0 && keyboardVisible && activeQuestionInput !== null;
const isOpen =
(currentLift > 0 || keyboardVisible) && activeQuestionInput !== null;
document.body.classList.toggle("question-keyboard-open", isOpen); document.body.classList.toggle("question-keyboard-open", isOpen);
console.log(`[Coord] setLift: ${prevLift}px -> ${currentLift}px | visible: ${keyboardVisible} | class: ${isOpen}`); console.log(`[Coord] setLift: ${prevLift}px -> ${currentLift}px | visible: ${keyboardVisible} | class: ${isOpen}`);
} }
@ -157,17 +173,30 @@ function updateLift() {
const safeTop = Number.parseFloat( const safeTop = Number.parseFloat(
getComputedStyle(document.documentElement).getPropertyValue("--safe-top"), getComputedStyle(document.documentElement).getPropertyValue("--safe-top"),
); );
const visibleTop = Math.max(
Number.isFinite(safeTop) ? safeTop : 0,
snapList?.getBoundingClientRect().top ?? 0,
);
const visibleBottom = Math.min(...visibleBottomCandidates);
const badgeEl = content.querySelector<HTMLElement>(
'.rounded-full.bg-\\[\\#F8D7DA\\]',
);
const badgeTop = badgeEl ? badgeEl.getBoundingClientRect().top : null;
const inputRect = activeQuestionInput?.getBoundingClientRect();
const computed = computeQuestionLift({ const computed = computeQuestionLift({
baseTop, baseTop,
baseBottom, baseBottom,
visibleTop: Math.max(
Number.isFinite(safeTop) ? safeTop : 0,
snapList?.getBoundingClientRect().top ?? 0,
),
visibleBottom: Math.min(...visibleBottomCandidates),
visibleTop,
visibleBottom,
topGap: DEFAULT_TOP_GAP,
bottomGap: DEFAULT_BOTTOM_GAP,
}); });
console.log(`[Coord] computed lift: ${computed}px (baseTop: ${baseTop.toFixed(0)}, baseBottom: ${baseBottom.toFixed(0)}, kTop: ${keyboardTop?.toFixed(0)})`);
console.log(
`[Coord Geometry] lift: ${computed}px | content: [${baseTop.toFixed(0)}..${baseBottom.toFixed(0)}] h=${(baseBottom - baseTop).toFixed(0)} | vis: [${visibleTop.toFixed(0)}..${visibleBottom.toFixed(0)}] (kTop: ${keyboardTop?.toFixed(0)}) | badgeTop: ${badgeTop?.toFixed(0) ?? "none"} | input: [${inputRect?.top.toFixed(0) ?? "?"}..${inputRect?.bottom.toFixed(0) ?? "?"}]`,
);
setLift(computed); setLift(computed);
} }

4
src/components/Componentes/slider-page.tsx

@ -133,7 +133,7 @@ export default function SliderPage({ onClose }: SliderPageProps = {}) {
onClose={() => setSubmitError(null)} onClose={() => setSubmitError(null)}
/> />
)} )}
<header className="relative -mx-[17px] rounded-b-[32px] px-[17px] pt-[max(12px,calc(var(--safe-top)+4px))] pb-3">
<header className="relative pt-[max(12px,calc(var(--safe-top)+4px))] pb-3">
<div className="relative flex items-center justify-between"> <div className="relative flex items-center justify-between">
<NavigationButton <NavigationButton
icon="back" icon="back"
@ -185,7 +185,7 @@ export default function SliderPage({ onClose }: SliderPageProps = {}) {
</div> </div>
</header> </header>
<main className="flex min-h-0 flex-1 overflow-hidden pt-6">
<main className="flex min-h-0 flex-1 overflow-hidden pt-3">
<div <div
className="flex h-full w-full transition-transform duration-300 ease-out" className="flex h-full w-full transition-transform duration-300 ease-out"
style={{ transform: `translateX(-${activeSlide * 100}%)` }} style={{ transform: `translateX(-${activeSlide * 100}%)` }}

2
src/components/Componentes/slider-slide-one.tsx

@ -60,7 +60,7 @@ export function SliderSlideOne({ index, onScrollToEnd }: SliderSlideOneProps) {
}} }}
> >
<SliderHeader index={index} title="Terms & Conditions" /> <SliderHeader index={index} title="Terms & Conditions" />
<div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-8">
<div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-4">
<p className="text-[#F14B46] group-12 font-semibold text-center"> <p className="text-[#F14B46] group-12 font-semibold text-center">
Welcome to Habib Marriage. Please review our terms of service before Welcome to Habib Marriage. Please review our terms of service before
proceeding. By registering and using this platform, you agree to proceeding. By registering and using this platform, you agree to

19
src/components/Componentes/test-questions-flow.tsx

@ -12,7 +12,6 @@ import NavigationButton from "./navigation-button";
import { PageBackground } from "./page-background"; import { PageBackground } from "./page-background";
import StickyHeader from "./sticky-header"; import StickyHeader from "./sticky-header";
import TestExitSheet from "./test-exit-sheet"; import TestExitSheet from "./test-exit-sheet";
import TestLoadingScreen from "./test-loading-screen";
export type QuestionOption = { export type QuestionOption = {
id: string; id: string;
@ -225,15 +224,6 @@ export default function TestQuestionsFlow({
} }
}; };
if (isSubmitting) {
return (
<TestLoadingScreen
title="Analyzing and submitting responses"
subtitle="Please wait a moment while your answers are reviewed and registered."
/>
);
}
if (!currentQuestion) { if (!currentQuestion) {
return null; return null;
} }
@ -306,7 +296,7 @@ export default function TestQuestionsFlow({
key={q.id} key={q.id}
aria-hidden={offset !== 0} aria-hidden={offset !== 0}
className={[ className={[
"absolute inset-0 flex flex-col overflow-y-auto pt-2 pb-4 will-change-transform transition-transform duration-200 ease-in-out",
"absolute inset-0 flex flex-col overflow-y-auto pt-3 pb-6 will-change-transform transition-transform duration-200 ease-in-out",
offset === 0 && !isSliding offset === 0 && !isSliding
? "pointer-events-auto" ? "pointer-events-auto"
: "pointer-events-none", : "pointer-events-none",
@ -316,14 +306,14 @@ export default function TestQuestionsFlow({
}} }}
> >
{/* Question Title */} {/* Question Title */}
<div className="w-full min-h-[100px] sm:min-h-[120px] flex items-center justify-center my-3 sm:my-4 shrink-0 px-2">
<h2 className="text-[17px] sm:text-[18px] font-bold text-[#1F2024] leading-[1.6] text-center w-full">
<div className="w-full min-h-[110px] sm:min-h-[130px] flex items-center justify-center mt-3 mb-6 sm:mt-5 sm:mb-8 shrink-0 px-3">
<h2 className="text-[18px] sm:text-[19px] font-bold text-[#1F2024] leading-[1.7] text-center w-full">
{q.text} {q.text}
</h2> </h2>
</div> </div>
{/* Answer Options Stack */} {/* Answer Options Stack */}
<div className="w-full flex flex-col gap-3 pb-3">
<div className="w-full flex flex-col gap-3.5 pb-6">
{options.map((option) => { {options.map((option) => {
const isSelected = qSelectedValue === option.value; const isSelected = qSelectedValue === option.value;
@ -426,6 +416,7 @@ export default function TestQuestionsFlow({
!areAllQuestionsAnswered || !areAllQuestionsAnswered ||
isSubmitting isSubmitting
} }
isLoading={isSubmitting}
onClick={handleFinishSubmit} onClick={handleFinishSubmit}
> >
{finishLabel} {finishLabel}

5
src/hooks/marriage/use-cattell.ts

@ -86,9 +86,12 @@ export function useSubmitCattellAssessmentMutation(
queryKey: marriageQueryKeys.cattellResult(), queryKey: marriageQueryKeys.cattellResult(),
}); });
queryClient.setQueriesData<FormSchemaResponse>( queryClient.setQueriesData<FormSchemaResponse>(
{ queryKey: ["marriage", "form-schema", "profile"] },
{ queryKey: [...marriageQueryKeys.all, "form-overview"] },
(schema) => markAssessmentCompleted(schema, "personality_test"), (schema) => markAssessmentCompleted(schema, "personality_test"),
); );
await queryClient.invalidateQueries({
queryKey: [...marriageQueryKeys.all, "form-overview"],
});
await options?.onSuccess?.(data, variables, onMutateResult, context); await options?.onSuccess?.(data, variables, onMutateResult, context);
}, },
}); });

10
src/hooks/marriage/use-glasser.ts

@ -61,7 +61,7 @@ export function useSubmitGlasserAssessmentMutation(
queryKey: marriageQueryKeys.glasserResult(), queryKey: marriageQueryKeys.glasserResult(),
}); });
queryClient.setQueriesData<FormSchemaResponse>( queryClient.setQueriesData<FormSchemaResponse>(
{ queryKey: ["marriage", "form-schema", "profile"] },
{ queryKey: [...marriageQueryKeys.all, "form-overview"] },
(schema) => { (schema) => {
if (!schema) return schema; if (!schema) return schema;
const slug = "glasser_5_needs_test"; const slug = "glasser_5_needs_test";
@ -84,11 +84,19 @@ export function useSubmitGlasserAssessmentMutation(
total_steps: 1, total_steps: 1,
completion_percent: 100, completion_percent: 100,
}, },
glasser: {
current_step: 1,
total_steps: 1,
completion_percent: 100,
},
}, },
}, },
}; };
}, },
); );
await queryClient.invalidateQueries({
queryKey: [...marriageQueryKeys.all, "form-overview"],
});
await options?.onSuccess?.(data, variables, onMutateResult, context); await options?.onSuccess?.(data, variables, onMutateResult, context);
}, },
}); });

38
src/hooks/marriage/use-section-data.ts

@ -149,22 +149,28 @@ export function applyProfilePatchResultToCache(
const slug = String(queryKey[3] ?? ""); const slug = String(queryKey[3] ?? "");
queryClient.setQueryData(queryKey, patchSection(current, slug)); queryClient.setQueryData(queryKey, patchSection(current, slug));
}); });
queryClient.setQueryData(
marriageQueryKeys.formOverview("profile", locale),
(current: any) =>
current
? {
...current,
progress: data.progress,
sections: current.sections.map((section: any) => {
const progress =
data.affected_sections[section.id] ??
data.progress.sections_progress[section.id];
return progress ? { ...section, progress } : section;
}),
}
: current,
);
queryClient
.getQueriesData({
queryKey: [...marriageQueryKeys.all, "form-overview"],
})
.forEach(([queryKey, current]: [any, any]) => {
if (!current) return;
queryClient.setQueryData(queryKey, {
...current,
progress: data.progress,
sections: current.sections?.map((section: any) => {
const progress =
data.affected_sections[section.id] ??
data.progress?.sections_progress?.[section.id];
return progress ? { ...section, progress } : section;
}),
});
});
void queryClient.invalidateQueries({
queryKey: [...marriageQueryKeys.all, "form-overview"],
exact: false,
});
} }
export function useMarriageSectionDataQuery( export function useMarriageSectionDataQuery(

8
src/lib/marriage-field-formatter.ts

@ -122,7 +122,13 @@ export function formatFieldLabel(
return matchedCanonicalKey; return matchedCanonicalKey;
} }
// 5. Fallback
// 5. If rawLabel contains Persian/Arabic characters and active dictionary is English, use englishTitle
const hasPersian = /[؀-ۿ]/.test(rawLabel);
if (hasPersian && dictionary) {
return englishTitle || rawLabel;
}
// 6. Fallback
return rawLabel || englishTitle; return rawLabel || englishTitle;
} }

52
src/lib/ssr-fetch.ts

@ -1,4 +1,6 @@
// Server-only module: imported only by Server Components (page.tsx wrappers). // Server-only module: imported only by Server Components (page.tsx wrappers).
import { cookies, headers } from "next/headers";
import { defaultLocale, isLocale } from "@/translations/config";
/** /**
* Helper to get the API base URL for server-side fetches. * Helper to get the API base URL for server-side fetches.
@ -18,6 +20,41 @@ const DEFAULT_SSR_TIMEOUT_MS =
Number(process.env.SSR_FETCH_TIMEOUT_MS) || Number(process.env.SSR_FETCH_TIMEOUT_MS) ||
(process.env.NODE_ENV === "development" ? 15000 : 1500); (process.env.NODE_ENV === "development" ? 15000 : 1500);
async function resolveServerLocale(
localeOverride?: string | null,
): Promise<string> {
if (localeOverride && isLocale(localeOverride)) {
return localeOverride;
}
try {
const cookieStore = await cookies();
const cookieLang =
cookieStore.get("HABIB_LANGUAGE")?.value ??
cookieStore.get("habib_language")?.value;
if (isLocale(cookieLang)) {
return cookieLang;
}
} catch {
// Ignore if called outside request context
}
try {
const headersList = await headers();
const headerLang = headersList
.get("accept-language")
?.split(",")[0]
?.split("-")[0];
if (isLocale(headerLang)) {
return headerLang;
}
} catch {
// Ignore if called outside request context
}
return defaultLocale;
}
/** /**
* Fetches the marriage profile server-side. * Fetches the marriage profile server-side.
* Runs only on the server, typically inside a Next.js Server Component. * Runs only on the server, typically inside a Next.js Server Component.
@ -25,11 +62,13 @@ const DEFAULT_SSR_TIMEOUT_MS =
* *
* @param token - The user authentication token * @param token - The user authentication token
* @param timeoutMs - Timeout in milliseconds (defaults to 15s in dev, 1.5s in prod) * @param timeoutMs - Timeout in milliseconds (defaults to 15s in dev, 1.5s in prod)
* @param localeOverride - Optional locale override
* @returns Profile data or null on failure * @returns Profile data or null on failure
*/ */
export async function fetchProfileSSR( export async function fetchProfileSSR(
token?: string | null, token?: string | null,
timeoutMs = DEFAULT_SSR_TIMEOUT_MS, timeoutMs = DEFAULT_SSR_TIMEOUT_MS,
localeOverride?: string | null,
): Promise<any | null> { ): Promise<any | null> {
if (!token || token === "NO_TOKEN" || token.trim() === "") { if (!token || token === "NO_TOKEN" || token.trim() === "") {
console.log("🖥️ [SSR fetchProfileSSR] No valid token found in cookies."); console.log("🖥️ [SSR fetchProfileSSR] No valid token found in cookies.");
@ -40,10 +79,11 @@ export async function fetchProfileSSR(
const timeoutId = setTimeout(() => controller.abort(), timeoutMs); const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try { try {
const lang = await resolveServerLocale(localeOverride);
const baseUrl = getApiBaseUrl(); const baseUrl = getApiBaseUrl();
const url = `${baseUrl}/api/marriage/profile/main/`;
const url = `${baseUrl}/api/marriage/profile/main/?lang=${lang}`;
console.log( console.log(
`🖥️ [SSR fetchProfileSSR] Fetching profile from ${url} with token: ${token.slice(0, 8)}...`,
`🖥️ [SSR fetchProfileSSR] Fetching profile from ${url} with token: ${token.slice(0, 8)}... (lang: ${lang})`,
); );
const response = await fetch(url, { const response = await fetch(url, {
@ -53,6 +93,8 @@ export async function fetchProfileSSR(
headers: { headers: {
Accept: "application/json", Accept: "application/json",
Authorization: `Token ${token.trim()}`, Authorization: `Token ${token.trim()}`,
"Accept-Language": lang,
"X-User-Language": lang,
}, },
}); });
@ -88,13 +130,15 @@ export async function fetchProfileSSR(
*/ */
export async function fetchConfigSSR( export async function fetchConfigSSR(
timeoutMs = DEFAULT_SSR_TIMEOUT_MS, timeoutMs = DEFAULT_SSR_TIMEOUT_MS,
localeOverride?: string | null,
): Promise<any | null> { ): Promise<any | null> {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs); const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try { try {
const lang = await resolveServerLocale(localeOverride);
const baseUrl = getApiBaseUrl(); const baseUrl = getApiBaseUrl();
const url = `${baseUrl}/api/marriage/config/`;
const url = `${baseUrl}/api/marriage/config/?lang=${lang}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
@ -102,6 +146,8 @@ export async function fetchConfigSSR(
signal: controller.signal, signal: controller.signal,
headers: { headers: {
Accept: "application/json", Accept: "application/json",
"Accept-Language": lang,
"X-User-Language": lang,
}, },
}); });

Loading…
Cancel
Save