Browse Source

feat: implement marriage matching application flow and internationalization support

front-test-2
ghorbani 3 weeks ago
parent
commit
3103665420
  1. 54
      scratch/update_translations.js
  2. 36
      src/app/candidate-contact/page.tsx
  3. 66
      src/app/finding-match/page.tsx
  4. 29
      src/app/globals.css
  5. 43
      src/app/intro/page.tsx
  6. 20
      src/app/layout.tsx
  7. 152
      src/app/new-match/page.tsx
  8. 105
      src/app/new-match/profile/page.tsx
  9. 9
      src/app/questions-list/[slug]/answer-pace-sheet.tsx
  10. 10
      src/app/questions-list/[slug]/page.tsx
  11. 16
      src/app/questions-list/[slug]/question-detail-client.tsx
  12. 115
      src/app/questions-list/page.tsx
  13. 11
      src/app/questions-list/sections-request.tsx
  14. 48
      src/app/request-accepted/page.tsx
  15. 17
      src/app/request-sent/page.tsx
  16. 15
      src/components/Componentes/button.tsx
  17. 29
      src/components/Componentes/call-result-sheet.tsx
  18. 136
      src/components/Componentes/dismiss-reason-sheet.tsx
  19. 23
      src/components/Componentes/error-toast.tsx
  20. 2
      src/components/Componentes/female-consent-sheet.tsx
  21. 18
      src/components/Componentes/fix-to-the-end.tsx
  22. 8
      src/components/Componentes/help-modal.tsx
  23. 38
      src/components/Componentes/information-sheet.tsx
  24. 27
      src/components/Componentes/loading-skeleton.tsx
  25. 280
      src/components/Componentes/navigation-button.tsx
  26. 4
      src/components/Componentes/notice-box.tsx
  27. 24
      src/components/Componentes/outcome-selection-sheet.tsx
  28. 35
      src/components/Componentes/page-header.tsx
  29. 59
      src/components/Componentes/progress-helper.ts
  30. 6
      src/components/Componentes/question-card.tsx
  31. 4
      src/components/Componentes/question-dropdown.tsx
  32. 4
      src/components/Componentes/question-number.tsx
  33. 29
      src/components/Componentes/question-phone.tsx
  34. 8
      src/components/Componentes/question-section-flow.tsx
  35. 4
      src/components/Componentes/question-slider.tsx
  36. 2
      src/components/Componentes/question-text.tsx
  37. 16
      src/components/Componentes/question-title.tsx
  38. 8
      src/components/Componentes/required-steps-card.tsx
  39. 7
      src/components/Componentes/slider-slide-four.tsx
  40. 18
      src/components/Componentes/subscription-required-sheet.tsx
  41. 13
      src/components/Componentes/support-sheet.tsx
  42. 2
      src/components/Componentes/swipe-button.tsx
  43. 2
      src/components/Componentes/test-intro-page.tsx
  44. 8
      src/components/Componentes/token-switcher.tsx
  45. 4
      src/data/question-data.ts
  46. 7
      src/hooks/marriage/types.ts
  47. 29
      src/hooks/marriage/use-rejection-seen.ts
  48. 17
      src/lib/auth-bridge.ts
  49. 3
      src/lib/http.ts
  50. 1009
      src/translations/locales/ar.json
  51. 1009
      src/translations/locales/az.json
  52. 1009
      src/translations/locales/bn.json
  53. 1007
      src/translations/locales/da.json
  54. 1007
      src/translations/locales/de.json
  55. 1011
      src/translations/locales/en.json
  56. 1007
      src/translations/locales/es.json
  57. 1011
      src/translations/locales/fa.json
  58. 1007
      src/translations/locales/fr.json
  59. 1007
      src/translations/locales/gu.json
  60. 1007
      src/translations/locales/ha.json
  61. 4
      src/translations/locales/he.json
  62. 4
      src/translations/locales/hi.json
  63. 4
      src/translations/locales/id.json
  64. 4
      src/translations/locales/ks.json
  65. 4
      src/translations/locales/pt.json
  66. 4
      src/translations/locales/ru.json
  67. 4
      src/translations/locales/sw.json
  68. 4
      src/translations/locales/tg.json
  69. 4
      src/translations/locales/tr.json
  70. 4
      src/translations/locales/ul.json
  71. 4
      src/translations/locales/ur.json
  72. 4
      src/translations/locales/uz.json
  73. 4
      src/translations/locales/zh.json
  74. 248
      src/translations/path_to_english.json

54
scratch/update_translations.js

@ -0,0 +1,54 @@
const fs = require("fs");
const path = require("path");
const localesDir = path.join(__dirname, "..", "src", "translations", "locales");
const files = fs.readdirSync(localesDir).filter((f) => f.endsWith(".json"));
const newTranslations = {
fa: {
subscriptionNoActiveTitle: "فاقد اشتراک فعال",
subscriptionNoActiveDesc:
"شما در حال حاضر اشتراک فعالی ندارید. فعالسازی اشتراک تنها زمانی امکانپذیر است که اولین کیس به شما معرفی شود.",
subscriptionStatusTitle: "وضعیت اشتراک",
subscriptionStatusDesc: "{days} روز از اعتبار اشتراک شما باقی مانده است.",
subscriptionRenewButton: "تمدید اشتراک",
subscriptionRenewPending: "در حال تمدید...",
},
ar: {
subscriptionNoActiveTitle: "لا يوجد اشتراك نشط",
subscriptionNoActiveDesc:
"ليس لديك اشتراك نشط حاليًا. تفعيل الاشتراك ممكن فقط عند تقديم أول مرشح لك.",
subscriptionStatusTitle: "حالة الاشتراك",
subscriptionStatusDesc: "متبقي {days} يوم من صلاحية اشتراكك.",
subscriptionRenewButton: "تجديد الاشتراك",
subscriptionRenewPending: "جاري التجديد...",
},
default: {
subscriptionNoActiveTitle: "No Active Subscription",
subscriptionNoActiveDesc:
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.",
subscriptionStatusTitle: "Subscription Status",
subscriptionStatusDesc: "{days} days remaining of your subscription.",
subscriptionRenewButton: "Renew Subscription",
subscriptionRenewPending: "Renewing...",
},
};
files.forEach((file) => {
const filePath = path.join(localesDir, file);
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
if (!data.common) {
data.common = {};
}
const lang = file.replace(".json", "");
const translations = newTranslations[lang] || newTranslations.default;
Object.keys(translations).forEach((key) => {
data.common[key] = translations[key];
});
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
console.log(`Updated ${file}`);
});

36
src/app/candidate-contact/page.tsx

@ -133,10 +133,10 @@ export default function CandidateContactPage() {
🎉
</div>
<h1 className="mt-8 text-center text-[24px] leading-[1.25] font-bold text-[#E03950]">
{t.candidateContact.congratsTitle}
{t["Congratulations! 🎉"]}
</h1>
<p className="mt-6 text-[#4A4A4A] group-14 font-medium leading-relaxed">
{t.candidateContact.congratsMessage}
{t["Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed."]}
</p>
</div>
) : (
@ -145,7 +145,7 @@ export default function CandidateContactPage() {
<div className="relative mt-4 flex items-center justify-center">
<Image
src="/assets/images/Group 159788fd0467.svg"
alt={t.candidateContact.imageAlt}
alt={t["Selected candidate contact status"]}
width={160}
height={152}
priority
@ -154,7 +154,7 @@ export default function CandidateContactPage() {
{/* Title / Status message */}
<h1 className="mt-8 text-center text-[20px] leading-[1.25] font-bold text-[#1A1A1A] max-w-[280px]">
{t.candidateContact.title}
{t["The selected candidate will contact your family shortly."]}
</h1>
{caseStatus === "contacted" ? (
@ -162,7 +162,7 @@ export default function CandidateContactPage() {
{/* Outcome instructions and button */}
<div className="w-full border border-[#ECECEC] bg-white rounded-[15px] mt-8 px-4 py-4 text-center shadow-sm">
<p className="text-[#555555] group-14 font-medium leading-relaxed">
{t.candidateContact.thankYouFeedback}
{t["Thank you for giving us feedback, we would be very happy if you also let us know the final result."]}
</p>
</div>
@ -176,7 +176,7 @@ export default function CandidateContactPage() {
}
className="flex-1 h-[52px] rounded-[15px] border border-[#BFBFBF] bg-white text-[#36363C] font-semibold group-14 flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F5F5F5]"
>
{t.requestAccepted.actionViewProfile}
{t["View Profile"]}
</button>
<button
@ -188,7 +188,7 @@ export default function CandidateContactPage() {
{outcomeMutation.isPending ? (
<DotsLoader className="text-white" />
) : (
t.candidateContact.submitOutcome
t["Submit Final Outcome"]
)}
</button>
</div>
@ -206,7 +206,7 @@ export default function CandidateContactPage() {
{contactStatusMutation.isPending ? (
<DotsLoader className="text-[#8E8E93]" />
) : (
t.candidateContact.noContactYet
t["Report No Contact"]
)}
</button>
@ -225,7 +225,7 @@ export default function CandidateContactPage() {
{contactStatusMutation.isPending ? (
<DotsLoader className="text-white" />
) : (
t.candidateContact.contacted
t["Confirm Contacted"]
)}
</button>
</div>
@ -233,7 +233,7 @@ export default function CandidateContactPage() {
{/* Red warning box */}
<div className="w-full border border-[#F0445B] bg-[#FFF5F6] rounded-[15px] mt-4 px-4 py-3 text-center">
<p className="text-[#F0445B] group-12 font-semibold leading-normal">
{t.candidateContact.contactWarning}
{t["To keep the process moving smoothly, the other party has a 48-hour (2-day) window to make initial contact with you or your family. If no contact is established after 2 days, you have the option to decline his request or notify us that he hasn't reached out."]}
</p>
</div>
</>
@ -245,11 +245,11 @@ export default function CandidateContactPage() {
<div className="space-y-4 pb-20">
{/* Advisor section */}
<AdvisorActionsCard
title={t.findingMatch.advisorTitle}
description={t.findingMatch.advisorDescription}
title={t["Get an advisor"]}
description={t["Not sure what to do next? Our psychology section is here to guide you at every step."]}
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorLabel={t["Get Advisor"]}
getAdvisorHref="/questions-list"
/>
@ -267,7 +267,7 @@ export default function CandidateContactPage() {
alt="lock"
/>
<h2 className="group-14 leading-none font-semibold text-[#747474]">
{t.requestSent.profileLocked}
{t["Profile is locked"]}
</h2>
</div>
</div>
@ -320,13 +320,13 @@ export default function CandidateContactPage() {
<div className="flex max-w-[300px] flex-col items-center">
<Image
src="/assets/images/Group 1597880467.svg"
alt={t.candidateContact.imageAlt}
alt={t["Selected candidate contact status"]}
width={131}
height={125}
/>
<h1 className="mt-10 text-center text-[18px] leading-[1.2] font-bold text-[#1A1A1A]">
{t.candidateContact.title}
{t["The selected candidate will contact your family shortly."]}
</h1>
</div>
</section>
@ -340,7 +340,7 @@ export default function CandidateContactPage() {
onClick={() => setIsCallResultSheetOpen(true)}
className="flex-1 h-[52px] rounded-[11px] bg-gradient-to-tl from-[#FE6F82] to-[#E03950] text-white font-semibold flex items-center justify-center cursor-pointer transition-opacity active:opacity-90"
>
{t.candidateContact.contacted}
{t["Confirm Contacted"]}
</button>
<button
type="button"
@ -351,7 +351,7 @@ export default function CandidateContactPage() {
{contactStatusMutation.isPending ? (
<DotsLoader className="text-[#8B8B8B]" />
) : (
t.candidateContact.noContactYet
t["Report No Contact"]
)}
</button>
</section>

66
src/app/finding-match/page.tsx

@ -4,7 +4,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo } from "react";
import { DotsLoader } from "@/components/Componentes/button";
import Button, { DotsLoader } from "@/components/Componentes/button";
import { FaLock, FaPen } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import PageHeader from "@/components/Componentes/page-header";
@ -13,6 +13,9 @@ import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { getSubmitPath } from "@/lib/get-submit-path";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
import { useRejectionSeenMutation } from "@/hooks/marriage/use-rejection-seen";
import { IoAlertCircle } from "react-icons/io5";
const advisorAvatars = [
{ id: "advisor-primary", src: "/assets/images/Avatar Image.png" },
@ -27,6 +30,9 @@ export default function FindingMatchPage() {
refetchInterval: 3000,
});
const { mutate: markRejectionSeen, isPending: isMarkingSeen } =
useRejectionSeenMutation();
useEffect(() => {
if (!profile) {
return;
@ -53,7 +59,14 @@ export default function FindingMatchPage() {
);
}
const copy = t.findingMatch;
const copy = {
title: t["SEARCH IN PROGRESS"],
description: t["Our system is actively looking for compatible partners based on your criteria. This process requires time and patience. We will notify you immediately once a profile is ready for your review."],
advisorTitle: t["Get an advisor"],
advisorDescription: t["Not sure what to do next? Our psychology section is here to guide you at every step."],
getAdvisor: t["Get Advisor"],
editProfile: t["Edit Profile"]
};
const matchImageSrc = "/assets/images/Group 1597880466.svg";
return (
@ -61,7 +74,7 @@ export default function FindingMatchPage() {
<PageBackground />
<main
style={{ paddingBottom: "calc(20px + var(--safe-bottom))" }}
style={{ paddingBottom: "calc(100px + var(--safe-bottom))" }}
className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(16px,calc(var(--safe-top)+8px))] text-center"
>
<PageHeader
@ -99,28 +112,57 @@ export default function FindingMatchPage() {
getAdvisorHref="/questions-list"
/>
<FixToTheEnd>
{profile?.can_edit_profile === false ? (
<div
className="inline-flex mt-3.5 w-full items-center justify-center gap-2 rounded-[9px] border-none bg-[#D1D1D6] dark:bg-[#3D3E42] px-4 py-[17px] text-center text-[#747474] dark:text-[#A1A1A1] shadow-none"
className="inline-flex w-full items-center justify-center gap-2 rounded-[9px] border-none bg-[#D1D1D6] dark:bg-[#3D3E42] px-4 py-[17px] text-center text-[#747474] dark:text-[#A1A1A1] shadow-none"
role="status"
>
<FaLock aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span className="group-16 leading-none font-semibold">
{t.requestSent.profileLocked}
{t["Profile is locked"]}
</span>
</div>
) : (
<Link
className="inline-flex mt-3.5 w-full cursor-pointer items-center justify-center rounded-[9px] border-none bg-[#2B2C31] bg-none px-4 py-[17px] text-center text-white shadow-none transition-opacity hover:opacity-90"
href={localizePath("/questions-list", locale)}
>
<span className="flex min-w-0 items-center justify-center gap-2 text-center group-16 leading-none font-semibold">
<Button variant="dark" href="/questions-list">
<FaPen aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span>{copy.editProfile}</span>
</span>
</Link>
</Button>
)}
</FixToTheEnd>
</main>
{profile?.unseen_rejection && (
<div className="fixed inset-0 z-[100] 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-[343px] rounded-[24px] bg-[#F9F8F8] dark:bg-[#1E1E1E] p-6 text-center shadow-[0_20px_60px_rgba(0,0,0,0.15)] animate-in zoom-in-95 duration-200">
<div className="mx-auto flex h-[56px] w-[56px] items-center justify-center rounded-full bg-rose-50 dark:bg-rose-950/30 text-[#F2465F] dark:text-[#FE6F82]">
<IoAlertCircle className="h-8 w-8" />
</div>
<h2 className="mt-4 text-[18px] font-bold text-[#171717] dark:text-white leading-[1.3]">
{t["Your request was rejected"]}
</h2>
<p className="mt-3 text-[14px] leading-[1.5] text-[#747474] dark:text-[#A0A0A0] font-semibold">
{t["Your request was rejected by the lady. You will be introduced to other candidates in the future."]}
</p>
<div className="mt-6 w-full">
<Button
variant="dark"
isLoading={isMarkingSeen}
onClick={() => {
markRejectionSeen({
rejection_id: profile.unseen_rejection!.id,
});
}}
>
{t["Got it"]}
</Button>
</div>
</div>
</div>
)}
</>
);
}

29
src/app/globals.css

@ -284,3 +284,32 @@ body[data-page-background="custom"] .app-shell {
animation: button-dot-bounce 0.9s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation-delay: 0.3s;
}
/* ─── Premium Shimmer Animations ─── */
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.shimmer-bg {
background: linear-gradient(
90deg,
rgba(0, 0, 0, 0.05) 25%,
rgba(0, 0, 0, 0.1) 37%,
rgba(0, 0, 0, 0.05) 63%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
}
.dark .shimmer-bg {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.09) 37%,
rgba(255, 255, 255, 0.04) 63%
);
background-size: 200% 100%;
}

43
src/app/intro/page.tsx

@ -6,14 +6,14 @@ import { useEffect, useRef, useState } from "react";
import Button from "@/components/Componentes/button";
import PageHeader from "@/components/Componentes/page-header";
import ReportActionsSheet from "@/components/Componentes/report-actions-sheet";
import VideoPlayer from "@/components/Componentes/video-player";
import type { MarriageProfileResponse } from "@/hooks/marriage/types";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { authBridge } from "@/lib/auth-bridge";
import { getSubmitPath } from "@/lib/get-submit-path";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
import VideoPlayer from "@/components/Componentes/video-player";
const REDIRECT_SESSION_KEY = "redirect";
@ -40,6 +40,7 @@ export default function Intro() {
return;
}
try {
const isAuthenticated = authBridge.isAuthenticated();
if (!isAuthenticated) {
@ -55,18 +56,28 @@ export default function Intro() {
setIsCheckingRedirect(true);
}
try {
const profileResponse = profile ?? (await refetch()).data;
const nextPath = localizePath(getSubmitPath(profileResponse), locale);
if (typeof window !== "undefined") {
try {
window.sessionStorage.removeItem(REDIRECT_SESSION_KEY);
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
}
if (!isCancelled) {
const currentPath =
typeof window !== "undefined" ? window.location.pathname : "";
if (currentPath === nextPath) {
setIsCheckingRedirect(false);
} else {
router.replace(nextPath);
}
return;
}
} catch {
} catch (error) {
console.error("Error in redirectIfNeeded:", error);
if (!isCancelled) {
setIsCheckingRedirect(false);
}
@ -148,57 +159,57 @@ export default function Intro() {
<div className="flex flex-col items-center mt-16">
<Image
src={"/assets/images/Group 1597880466.svg"}
alt={t.intro.imageAlt}
alt={t["heavenly marriage"]}
width={168}
height={155}
/>
<h2 className="group-16 text-[#475569] font-bold mt-5 text-center">
{t.intro.title}
{t["A Path to Heavenly Marriage"]}
</h2>
<p className="text-center mt-2 group-12 text-[#4D4D4D]">
{t.intro.description}
{t["We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims"]}
</p>
</div>
<div className="flex items-center justify-between mt-4">
<div className="flex items-center gap-0.5 p-2 border border-[#FD6ABB]/10 rounded-2xl">
<Image
src={"/assets/images/tabler_user-filled.svg"}
alt={t.intro.userProfile}
alt={t["user profiles"]}
width={37}
height={37}
/>
<div className="flex flex-col items-start justify-start -mb-2">
<p className="text-[#36363C] group-16 font-bold leading-3">120</p>
<p className="text-[#36363C] group-10 font-semibold">
{t.intro.userProfile}
{t["user profiles"]}
</p>
</div>
</div>
<div className="flex items-center gap-0.5 p-2 border border-[#FD6ABB]/10 rounded-2xl">
<Image
src={"/assets/images/icon-park-solid_success.svg"}
alt={t.intro.matches}
alt={t["matches"]}
width={37}
height={37}
/>
<div className="flex flex-col items-start justify-start -mb-2">
<p className="text-[#36363C] group-16 font-bold leading-3">14</p>
<p className="text-[#36363C] group-10 font-semibold">
{t.intro.matches}
{t["matches"]}
</p>
</div>
</div>
<div className="flex items-center gap-0.5 p-2 border border-[#FD6ABB]/10 rounded-2xl">
<Image
src={"/assets/images/typcn_heart-full-outline.svg"}
alt={t.intro.marriage}
alt={t["marriages"]}
width={37}
height={37}
/>
<div className="flex flex-col items-start justify-start -mb-2">
<p className="text-[#36363C] group-16 font-bold leading-3">14</p>
<p className="text-[#36363C] group-10 font-semibold">
{t.intro.marriage}
{t["marriages"]}
</p>
</div>
</div>
@ -212,7 +223,7 @@ export default function Intro() {
config?.intro_video_thumbnail_url ||
"/assets/images/Frame 2095586523.png"
}
alt={t.intro.videoAlt}
alt={t["video"]}
fill
sizes="344px"
className="object-cover transition-transform duration-300 group-hover:scale-105"
@ -221,7 +232,7 @@ export default function Intro() {
<div className="absolute inset-0 bg-black/10 transition-colors duration-300 group-hover:bg-black/20" />
<Image
src={"/assets/images/Frame 1116607280.svg"}
alt={t.intro.playAlt}
alt={t["play"]}
width={68}
height={68}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transition-transform duration-300 group-hover:scale-110 active:scale-95"
@ -243,7 +254,7 @@ export default function Intro() {
disabled={isSubmitting}
isLoading={isSubmitting}
>
{t.common.submit}
{t["Submit"]}
</Button>
</div>
</div>

20
src/app/layout.tsx

@ -121,11 +121,17 @@ export default function RootLayout({
this._habib_token = value;
if (value) {
writeCookie(HABIB_TOKEN_COOKIE, value);
try {
sessionStorage.setItem(HABIB_TOKEN_COOKIE, value);
} catch (e) {}
}
},
get: function() {
return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || sessionStorage.getItem(HABIB_TOKEN_COOKIE) || undefined;
var ssVal;
try {
ssVal = sessionStorage.getItem(HABIB_TOKEN_COOKIE);
} catch (e) {}
return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || ssVal || undefined;
}
});
}
@ -137,20 +143,30 @@ export default function RootLayout({
this._habib_coins = value;
if (value !== undefined && value !== null && value !== '') {
writeCookie(HABIB_COINS_COOKIE, value);
try {
sessionStorage.setItem(HABIB_COINS_COOKIE, String(value));
} catch (e) {}
}
},
get: function() {
var cookieValue = readCookie(HABIB_COINS_COOKIE);
return this._habib_coins || parseInt(cookieValue || sessionStorage.getItem(HABIB_COINS_COOKIE) || '0');
var ssVal;
try {
ssVal = sessionStorage.getItem(HABIB_COINS_COOKIE);
} catch (e) {}
return this._habib_coins || parseInt(cookieValue || ssVal || '0');
}
});
}
// Performance monitoring
window.addEventListener('load', function() {
try {
var perfData = performance.getEntriesByType('navigation')[0];
if (perfData) {
console.log('⏱️ Load time:', Math.round(perfData.loadEventEnd - perfData.fetchStart), 'ms');
}
} catch (e) {}
});
}
`,

152
src/app/new-match/page.tsx

@ -10,6 +10,7 @@ import { DotsLoader } from "@/components/Componentes/button";
import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background";
import { IoClose } from "react-icons/io5";
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
import { useHabcoinPaymentMutation } from "@/hooks/marriage/use-habcoin-payment";
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import type {
@ -276,30 +277,86 @@ export default function NewMatchPage() {
return getSubmitPath(profile) !== "/new-match";
}, [profile]);
const matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary);
if (isLoading || isRedirecting) {
return (
<>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(16px,calc(var(--safe-top)+8px))] items-center justify-center">
<DotsLoader />
<main
style={{
paddingBottom: `${bottom + 20}px`,
paddingTop: `${Math.max(12, top + 4)}px`,
}}
className="-mx-[17px] min-h-screen h-full text-center px-4"
>
<PageHeader />
<div>
{/* Header Section Skeleton */}
<section className="flex flex-col items-center mt-9">
<LoadingSkeleton className="h-[60px] w-[60px] rounded-full" />
<LoadingSkeleton className="h-6 w-[220px] mt-4" />
<LoadingSkeleton className="h-4 w-[280px] mt-3" />
<LoadingSkeleton className="h-4 w-[240px] mt-1.5" />
</section>
<div className="flex h-full flex-col justify-between">
{/* Match Card Skeleton */}
<section className="mt-[36px] rounded-[15px] border border-white/80 bg-white px-[17px] pt-[18px] pb-[17px] shadow-[0_18px_45px_rgba(15,23,42,0.06)] flex flex-col items-center">
{/* Name line */}
<LoadingSkeleton className="h-5 w-[140px]" />
{/* Subtitle / Details lines */}
<div className="mt-3 w-full flex flex-col items-center gap-2 min-h-[48px]">
<LoadingSkeleton className="h-3 w-[180px]" />
<LoadingSkeleton className="h-3 w-[120px]" />
<LoadingSkeleton className="h-3 w-[150px]" />
</div>
{/* Button skeleton */}
<LoadingSkeleton className="mt-[15px] h-[40px] w-full rounded-[10px]" />
</section>
{/* Advisor Card Skeleton */}
<section className="mt-[36px]">
<div className="rounded-[13px] border border-white/80 bg-white px-3 py-3.5 text-left shadow-[0_18px_45px_rgba(15,23,42,0.06)] backdrop-blur-sm">
<LoadingSkeleton className="h-[16px] w-[140px]" />
<div className="mt-2 space-y-1">
<LoadingSkeleton className="h-[10px] w-[260px]" />
<LoadingSkeleton className="h-[10px] w-[180px]" />
</div>
<div className="mt-4 flex items-center justify-between gap-3">
<div className="flex items-center pl-1">
<LoadingSkeleton className="h-[30px] w-[30px] rounded-full border-2 border-white" />
<LoadingSkeleton className="-ml-1.5 h-[30px] w-[30px] rounded-full border-2 border-white" />
<LoadingSkeleton className="-ml-1.5 h-[30px] w-[30px] rounded-full border-2 border-white" />
<LoadingSkeleton className="-ml-1.5 h-[30px] w-[30px] rounded-full border-2 border-white" />
</div>
<LoadingSkeleton className="h-[38px] w-[120px] rounded-[9px]" />
</div>
</div>
</section>
</div>
</div>
</main>
</>
);
}
const matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary);
const pairedFields = [matchDisplay.age, matchDisplay.city].filter(
(field): field is DisplayField => Boolean(field),
);
const isFemaleProfile = profile?.gender === "female";
const matchHeadingTitle = isFemaleProfile
? t.match.newMatchTitleFemale
: t.match.newMatchTitleMale;
? t["New Marriage Proposal"]
: t["YOU HAVE A NEW MATCH!"];
const matchHeadingDescription = isFemaleProfile
? t.match.newMatchDescriptionFemale
: t.match.newMatchDescriptionMale;
? t["A suitable match has been found for you. If approved, your profile will be evaluated to proceed with the introduction process."]
: t["If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information."];
const isMale = profile?.gender === "male";
const hasActiveSub = !!profile?.active_subscription;
@ -339,12 +396,26 @@ export default function NewMatchPage() {
</p>
</section>
<div className="flex h-full flex-col justify-between">
<section className="mt-[36px] rounded-[15px] bg-[linear-gradient(180deg,#F0445B_0%,#F4556E_100%)] px-[17px] pt-[18px] pb-[17px] text-white shadow-[0_18px_38px_rgba(240,68,91,0.25)]">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<DotsLoader />
<section className="mt-[36px] rounded-[15px] border border-white/80 bg-white px-[17px] pt-[18px] pb-[17px] shadow-[0_18px_45px_rgba(15,23,42,0.06)]">
<div className="flex flex-col items-center py-2">
{/* Name line */}
<LoadingSkeleton className="h-5 w-[140px]" />
{/* Subtitle / Details lines */}
<div className="mt-3 w-full flex flex-col items-center gap-2 min-h-[48px]">
<LoadingSkeleton className="h-3 w-[180px]" />
<LoadingSkeleton className="h-3 w-[120px]" />
<LoadingSkeleton className="h-3 w-[150px]" />
</div>
) : isError ? (
{/* Button skeleton */}
<LoadingSkeleton className="mt-[15px] h-[40px] w-full rounded-[10px]" />
</div>
</section>
) : (
<section className="mt-[36px] rounded-[15px] bg-[linear-gradient(180deg,#F0445B_0%,#F4556E_100%)] px-[17px] pt-[18px] pb-[17px] text-white shadow-[0_18px_38px_rgba(240,68,91,0.25)]">
{isError ? (
<p className="py-8 text-[13px] font-semibold">
Unable to load match summary.
</p>
@ -390,9 +461,9 @@ export default function NewMatchPage() {
router.push(localizePath("/new-match/profile", locale));
}
}}
className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white py-[12px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none hover:bg-white/95 transition-colors cursor-pointer"
className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white h-[40px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none hover:bg-white/95 transition-colors cursor-pointer"
>
{t.match.viewMoreDetails}
{t["View more details"]}
</button>
</>
) : (
@ -401,17 +472,17 @@ export default function NewMatchPage() {
</p>
)}
</section>
)}
<div className="w-full space-y-3">
<AdvisorActionsCard
className="mt-[36px]"
title={t.findingMatch.advisorTitle}
description={t.findingMatch.advisorDescription}
title={t["Get an advisor"]}
description={t["Not sure what to do next? Our psychology section is here to guide you at every step."]}
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorLabel={t["Get Advisor"]}
getAdvisorHref="/questions-list"
/>
</div>
</div>
</div>
@ -423,12 +494,12 @@ export default function NewMatchPage() {
className="fixed inset-x-0 bottom-0 z-20 mx-auto w-full max-w-[375px] bg-background/95 px-[17px] pt-3 pb-[16px] backdrop-blur-md"
>
<div
className="inline-flex w-full items-center justify-center gap-2 rounded-[9px] border-none bg-[#D1D1D6] px-4 py-[17px] text-center text-[#747474] shadow-none"
className="inline-flex w-full items-center justify-center gap-2 rounded-[9px] border-none bg-[#D1D1D6] px-4 h-[52px] text-center text-[#747474] shadow-none"
role="status"
>
<FaLock aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span className="group-16 leading-none font-semibold">
{t.requestSent.profileLocked}
{t["Profile is locked"]}
</span>
</div>
</div>
@ -463,24 +534,27 @@ export default function NewMatchPage() {
</div>
<h3 className="group-16 font-bold text-gray-900 mb-2">
{t.paymentModal?.title || "Verification & Subscription Activation"}
{t["Verification & Subscription Activation"] ||
"Verification & Subscription Activation"}
</h3>
<p className="text-xs text-gray-500 leading-relaxed mb-4 text-center">
{t.paymentModal?.verificationText || "This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."}
{t["This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."] ||
"This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."}
</p>
<div className="w-full bg-gray-50 rounded-xl p-3 mb-4 text-center">
<span className="text-[10px] text-gray-400 block mb-0.5">
{t.paymentModal?.activeFor3Months || "Valid for 3 months"}
{t["Valid for 3 months"] || "Valid for 3 months"}
</span>
<span className="text-base font-bold text-[#FF4E67]">
{t.paymentModal?.cost || "50 Habib Coins"}
{t["50 Coins"] || "50 Habib Coins"}
</span>
</div>
<p className="text-[10px] text-gray-400 leading-normal mb-6 text-center">
{t.paymentModal?.disclaimerText || "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users."}
{t["Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users."] ||
"Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users."}
</p>
{paymentError && (
@ -489,28 +563,36 @@ export default function NewMatchPage() {
</div>
)}
<div className="grid w-full grid-cols-[1fr_2fr] gap-3">
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
<button
type="button"
disabled={paymentMutation.isPending || respondMutation.isPending}
disabled={
paymentMutation.isPending || respondMutation.isPending
}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={handleDecline}
>
<div className="inline-flex w-full items-center justify-center rounded-[18px] border border-[#9A9A9A] bg-[#F7F7F7] px-4 py-[18px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t.common?.decline || "Decline"}</span>
<div className="inline-flex w-full items-center justify-center rounded-[18px] border border-[#9A9A9A] bg-[#F7F7F7] px-2 h-[52px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">
{t["Decline"] || "Decline"}
</span>
</div>
</button>
<button
type="button"
disabled={paymentMutation.isPending || respondMutation.isPending}
disabled={
paymentMutation.isPending || respondMutation.isPending
}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={handlePayment}
>
<div className="inline-flex w-full items-center justify-center gap-3 rounded-[18px] bg-[#F0445B] px-4 py-[16px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate min-w-0">{t.paymentModal?.pay || "Pay"}</span>
<div className="inline-flex w-full items-center justify-center gap-3 rounded-[18px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate min-w-0">
{t["Pay"] || "Pay"}
</span>
<span className="inline-flex items-center gap-1 rounded-full bg-[#E43B51] p-1.5 text-xs font-semibold leading-none text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.12)] shrink-0 min-w-0 max-w-[120px]">
<span className="inline-flex items-center gap-1 rounded-full bg-[#E43B51] p-1.5 text-xs font-semibold leading-none text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.12)] shrink-0 min-w-0 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
@ -519,7 +601,7 @@ export default function NewMatchPage() {
height={18}
className="shrink-0"
/>
<span className="truncate">{t.paymentModal?.cost || "50 Coins"}</span>
<span className="truncate">50</span>
</span>
</div>
</button>

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

@ -3,10 +3,11 @@
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import Button, { DotsLoader } from "@/components/Componentes/button";
import Button from "@/components/Componentes/button";
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet";
import InformationSheet from "@/components/Componentes/information-sheet";
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background";
import StickyHeader from "@/components/Componentes/sticky-header";
@ -187,49 +188,49 @@ function NewMatchProfileSkeleton() {
return (
<>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col bg-[linear-gradient(180deg,rgba(255,197,196,0.2)_0%,rgba(251,237,237,0.7)_100%)] pb-10">
<main className="-mx-[17px] flex min-h-screen flex-col bg-[#F5F5F5] pb-10">
<StickyHeader>
<div className="flex items-center justify-between gap-3">
<NavigationButton
variant="transparent"
icon="close"
iconLabel={t.match.goBack}
iconLabel={t["Go back"]}
/>
<h1 className="min-w-0 flex-1 text-center group-16 font-bold text-white">
{t.match.title}
{t["New Match"]}
</h1>
<div className="size-10 shrink-0" />
</div>
</StickyHeader>
<section className="px-[17px] pb-32 pt-5">
<div className="flex flex-col items-start">
<div className="size-[90px] rounded-full bg-white/60 animate-pulse shadow-sm" />
<div className="mt-3.5 h-7 w-44 rounded-xl bg-white/70 animate-pulse shadow-xs" />
<div className="flex flex-col items-center justify-center w-full">
<LoadingSkeleton className="size-[90px] rounded-full shadow-sm" />
<LoadingSkeleton className="mt-3.5 h-7 w-44 rounded-xl shadow-xs" />
</div>
<div className="mt-6 space-y-4 rounded-[18px] bg-white/80 p-5 shadow-xs animate-pulse">
<div className="h-4 w-44 rounded-md bg-[#F0445B]/20 border-b border-[#F0445B]/15 pb-2" />
<div className="mt-6 space-y-4 rounded-[18px] bg-white/80 p-5 shadow-xs">
<LoadingSkeleton className="h-4 w-44 rounded-md bg-[#F0445B]/10 border-b border-[#F0445B]/15 pb-2" />
<div className="space-y-4 pt-1">
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3">
<div className="h-3 w-24 rounded bg-gray-200/90" />
<div className="h-4 w-48 rounded bg-gray-300/80" />
<LoadingSkeleton className="h-3 w-24" />
<LoadingSkeleton className="h-4 w-48" />
</div>
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3">
<div className="h-3 w-28 rounded bg-gray-200/90" />
<div className="h-4 w-36 rounded bg-gray-300/80" />
<LoadingSkeleton className="h-3 w-28" />
<LoadingSkeleton className="h-4 w-36" />
</div>
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3">
<div className="h-3 w-20 rounded bg-gray-200/90" />
<div className="h-4 w-56 rounded bg-gray-300/80" />
<LoadingSkeleton className="h-3 w-20" />
<LoadingSkeleton className="h-4 w-56" />
</div>
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3">
<div className="h-3 w-32 rounded bg-gray-200/90" />
<div className="h-4 w-40 rounded bg-gray-300/80" />
<LoadingSkeleton className="h-3 w-32" />
<LoadingSkeleton className="h-4 w-40" />
</div>
</div>
</div>
@ -241,8 +242,8 @@ function NewMatchProfileSkeleton() {
>
<div className="mx-auto w-full sm:max-w-[375px] rounded-t-[24px] bg-white px-4 py-4 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
<div className="flex gap-3">
<div className="w-1/3 h-[46px] rounded-[12px] bg-gray-200/80 animate-pulse" />
<div className="w-2/3 h-[46px] rounded-[12px] bg-[#F0445B]/30 animate-pulse" />
<LoadingSkeleton className="w-1/3 h-[46px] rounded-[12px]" />
<LoadingSkeleton className="w-2/3 h-[46px] rounded-[12px] bg-[#F0445B]/10" />
</div>
</div>
</div>
@ -306,7 +307,6 @@ export default function NewMatchProfilePage() {
const caseId = profile?.active_case?.case_id;
const caseStatus = profile?.active_case?.status;
const isFemaleProfile = profile?.gender === "female";
const isMaleAccepted = caseStatus === "male_accepted";
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", {
onSuccess: async (_, variables) => {
if (variables.action === "accept") {
@ -364,11 +364,11 @@ export default function NewMatchProfilePage() {
}
const isSubmitting = respondMutation.isPending;
const isAcceptProfileEnabled = isFemaleProfile
? !isSubmitting
: Boolean(caseId) &&
const isAcceptProfileEnabled =
Boolean(caseId) &&
!isSubmitting &&
canAcceptProfile(profile?.gender, caseStatus);
const isRejectProfileEnabled = isAcceptProfileEnabled;
const nameParts = candidateName.trim().split(/\s+/);
const firstName = nameParts[0] || "";
@ -377,10 +377,10 @@ export default function NewMatchProfilePage() {
const mainClass = "-mx-[17px] flex min-h-screen flex-col pb-10";
const mainStyle = {
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg"), linear-gradient(178.25deg, rgba(255, 197, 196, 0.2) 1.48%, rgba(251, 237, 237, 0.2) 20.64%)`,
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`,
backgroundColor: "#F5F5F5",
backgroundRepeat: "repeat-y",
backgroundSize: "100% auto, 100% 100%",
backgroundSize: "100% auto",
};
return (
@ -389,8 +389,8 @@ export default function NewMatchProfilePage() {
{isRequestSheetOpen ? (
isFemaleProfile ? (
<FemaleConsentSheet
title={t.match.femaleConsentTitle}
description={formatBoldText(t.match.femaleConsentDescription)}
title={t["Final Consent"]}
description={formatBoldText(t["Approving this profile will **display your contact number** to the gentleman. Ensure **family consensus** before proceeding."])}
buttons={({ close }) => (
<div className="space-y-5">
<button
@ -412,7 +412,7 @@ export default function NewMatchProfilePage() {
) : null}
</span>
<span className="text-xs leading-[1.35] text-[#3F3F3F]">
{formatBoldText(t.match.femaleConsentCheckboxLabel)}
{formatBoldText(t["I confirm the family has **reviewed this profile** and **consents to communicate**."])}
</span>
</button>
@ -422,7 +422,7 @@ export default function NewMatchProfilePage() {
className="py-[18px] text-[18px]"
onClick={close}
>
{t.common.cancel}
{t["Cancel"]}
</Button>
<Button
className="py-[18px] text-[18px]"
@ -441,7 +441,7 @@ export default function NewMatchProfilePage() {
await respondMutation.mutateAsync({ action: "accept" });
}}
>
{t.common.confirm}
{t["Confirm"]}
</Button>
</div>
</div>
@ -454,8 +454,8 @@ export default function NewMatchProfilePage() {
) : (
<InformationSheet
icon="check"
title={t.match.requestProceedTitle}
description={t.match.requestProceedDescription}
title={t["Request to Proceed"]}
description={t["Upon your approval, your request will be sent to the lady, and we must await her response. This process may take between 2 to 4 days. Thank you for your patience."]}
buttons={({ close }) => (
<div className="grid w-full grid-cols-2 gap-3">
<Button
@ -463,7 +463,7 @@ export default function NewMatchProfilePage() {
className="py-[18px]"
onClick={close}
>
{t.common.cancel}
{t["Cancel"]}
</Button>
<Button
className="py-[18px]"
@ -480,7 +480,7 @@ export default function NewMatchProfilePage() {
await respondMutation.mutateAsync({ action: "accept" });
}}
>
{t.common.confirm}
{t["Confirm"]}
</Button>
</div>
)}
@ -491,21 +491,20 @@ export default function NewMatchProfilePage() {
{isRejectSheetOpen ? (
<InformationSheet
icon="warning"
title="Reject Profile"
description="Are you sure you've fully reviewed the profile and want to reject this profile?"
title={t["Reject Profile"]}
description={t["Are you sure you've fully reviewed the profile and want to reject this profile?"]}
buttons={({ close }) => (
<div className="grid w-full grid-cols-2 gap-3">
<Button variant="outlined" className="py-[18px]" onClick={close}>
{t.common.cancel}
{t["Cancel"]}
</Button>
<Button
className="py-[18px]"
onClick={() => {
close();
setIsDismissReasonSheetOpen(true);
}}
>
Reject
{t["Reject"]}
</Button>
</div>
)}
@ -515,7 +514,7 @@ export default function NewMatchProfilePage() {
{isMaleRejectWarningOpen ? (
<InformationSheet
icon="warning"
title={t.maleRejectionWarning.title}
title={t["Rejection Warning"]}
description={
<div
className="space-y-4 text-right"
@ -530,18 +529,18 @@ export default function NewMatchProfilePage() {
}
>
<p className="text-sm font-bold text-[#E11D48] leading-relaxed bg-[#FFF1F2] p-3 rounded-xl border border-[#FFE4E6]">
{t.maleRejectionWarning.carefulReview}
{t["Before making a final decision, please carefully review the other person's profile again completely to make an informed choice."]}
</p>
<div className="flex gap-2.5 items-start p-3 bg-gray-50 rounded-xl border border-gray-100">
<span className="text-lg shrink-0">💡</span>
<p className="text-xs text-[#6B7280] leading-relaxed">
{t.maleRejectionWarning.friendlyDelay}
{t["Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose."]}
</p>
</div>
<div className="flex gap-2.5 items-start p-3 bg-gray-50 rounded-xl border border-gray-100">
<span className="text-lg shrink-0"></span>
<p className="text-xs text-[#6B7280] leading-relaxed">
{t.maleRejectionWarning.noPenalty}
{t["Confirming this rejection will not result in any penalty. Instead, it simply enters your status into a 2-day decision window to finalize the case."]}
</p>
</div>
</div>
@ -549,7 +548,7 @@ export default function NewMatchProfilePage() {
buttons={({ close }) => (
<div className="w-full space-y-3">
<SwipeButton
text={t.maleRejectionWarning.swipeText}
text={t["Swipe to confirm rejection"]}
onSuccess={() => {
close();
setIsDismissReasonSheetOpen(true);
@ -557,10 +556,10 @@ export default function NewMatchProfilePage() {
/>
<Button
variant="outlined"
className="w-full py-[14px] text-[15px] font-semibold text-gray-500 border-gray-200"
className="w-full text-[15px] font-semibold text-gray-500 border-gray-200"
onClick={close}
>
{t.common.cancel}
{t["Cancel"]}
</Button>
</div>
)}
@ -589,13 +588,13 @@ export default function NewMatchProfilePage() {
<NavigationButton
variant="transparent"
icon="close"
iconLabel={t.match.goBack}
iconLabel={t["Go back"]}
/>
<h1
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white"
style={{ fontFamily: "'Segoe UI', sans-serif" }}
>
{t.match.title}
{t["New Match"]}
</h1>
<div className="size-10 shrink-0" />
</div>
@ -670,13 +669,13 @@ export default function NewMatchProfilePage() {
onClick={() => router.back()}
className="w-full h-[52px] flex items-center justify-center rounded-[12px] bg-[#F5F5F5] text-[#36363C] font-semibold text-[16px] transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#EBEBEB]"
>
{t.common.back}
{t["Back"]}
</button>
) : (
<div className="flex gap-3">
<button
type="button"
disabled={!caseId || isSubmitting || isMaleAccepted}
disabled={!isRejectProfileEnabled}
onClick={() => {
if (isFemaleProfile) {
setIsRejectSheetOpen(true);
@ -684,9 +683,9 @@ export default function NewMatchProfilePage() {
setIsMaleRejectWarningOpen(true);
}
}}
className="inline-flex w-1/3 h-[52px] items-center justify-center rounded-[12px] border border-[#BFBFBF] bg-white px-4 text-[16px] font-semibold text-[#9A9A9A]"
className="inline-flex w-1/3 h-[52px] items-center justify-center rounded-[12px] border border-[#BFBFBF] bg-white px-4 text-[16px] font-semibold text-[#9A9A9A] disabled:cursor-not-allowed disabled:opacity-50"
>
Reject
{t["Reject"]}
</button>
<button
type="button"
@ -710,7 +709,7 @@ export default function NewMatchProfilePage() {
width={28}
height={28}
/>
<span>{t.match.acceptProfile}</span>
<span>{t["Accept Profile"]}</span>
</button>
</div>
)}

9
src/app/questions-list/[slug]/answer-pace-sheet.tsx

@ -109,7 +109,12 @@ export default function AnswerPaceSheet({
isSuccess && !hasSeenSheet && !hasLocalProgress && !hasAnySectionProgress;
useEffect(() => {
try {
setHasSeenSheet(window.sessionStorage.getItem(storageKey) === "true");
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
setHasSeenSheet(false);
}
setHasLocalProgress(hasStoredQuestionProgress(sectionSlugs));
}, [sectionSlugs, storageKey]);
@ -118,7 +123,11 @@ export default function AnswerPaceSheet({
return;
}
try {
window.sessionStorage.setItem(storageKey, "true");
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
}, [isOpen, storageKey]);
if (!isOpen) {

10
src/app/questions-list/[slug]/page.tsx

@ -46,11 +46,11 @@ export default async function QuestionDetailPage({
itemSlug={item.slug}
locale={locale}
questionsListHref={questionsListHref}
title={t.questions.answerAtYourOwnPace}
description={t.questions.answerAtYourOwnPaceDescription}
closeLabel={t.questions.closeQuestionsList}
informationLabel={t.sheets.informationSheet}
continueLabel={t.common.submit}
title={t["Answer at Your Own Pace"]}
description={t["You can pause the survey anytime and resume later. Your progress is saved automatically."]}
closeLabel={t["Close questions list"]}
informationLabel={t["Information sheet"]}
continueLabel={t["Submit"]}
/>
);
}

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

@ -759,7 +759,15 @@ export default function QuestionDetailClient({
const bulletKey =
item.slug === "glasser_5_needs_test" ? "glasser" : "personality";
const bullets = t.questions.testIntroBullets[bulletKey];
const bullets = bulletKey === "glasser" ? [
t["Understanding your five basic needs helps you recognize what truly drives your behavior in relationships."],
t["Taking this test is not mandatory, but it will help you better understand your priorities and find a more compatible spouse."],
t["By identifying your strongest needs, you can better communicate your expectations and build healthier relationships."]
] : [
t["Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?"],
t["Taking this test is not mandatory, but it will help you better search for a spouse. The Personality test is a test for self-knowledge and better understanding of your spouse."],
t["Do you operate based on superficial behavioral adaptations, or are you aware of the deep \"source traits\" that fundamentally control your decision-making processes?"]
];
return (
<>
@ -791,10 +799,10 @@ export default function QuestionDetailClient({
<TestIntroPage
title={item.title}
estimateTime={item.estimate}
description={t.questions.testIntroEstimateLabel}
description={t["Estimate time"]}
bulletPoints={bullets}
disclaimerText={t.questions.testIntroDisclaimer}
startLabel={t.questions.testIntroStart}
disclaimerText={t["All provided information is held in strict confidence and will not be disclosed to third parties. This data is utilized exclusively to ensure optimal matchmaking accuracy. Consequently, it is imperative that this assessment be completed with the utmost diligence, precision, and integrity."]}
startLabel={t["Start"]}
onStart={() => {
setIsTestStarted(true);
}}

115
src/app/questions-list/page.tsx

@ -14,6 +14,8 @@ import RequiredStepsCard from "@/components/Componentes/required-steps-card";
import Button from "@/components/Componentes/button";
import InformationSheet from "@/components/Componentes/information-sheet";
import NavigationButton from "@/components/Componentes/navigation-button";
import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
import { PageBackground } from "@/components/Componentes/page-background";
import ErrorToast from "@/components/Componentes/error-toast";
import {
@ -40,8 +42,10 @@ export default function QuestionsListPage() {
const { dictionary: t, locale } = useI18n();
const router = useRouter();
const queryClient = useQueryClient();
const { data: profile } = useMarriageProfileQuery();
const { data: sections } = useMarriageSectionsQuery({
const { data: profile, isLoading: isProfileLoading } =
useMarriageProfileQuery();
const { data: sections, isLoading: isSectionsLoading } =
useMarriageSectionsQuery({
refetchOnMount: "always",
});
@ -142,9 +146,9 @@ export default function QuestionsListPage() {
useEffect(() => {
if (startMatchMutation.isError || isSyncError) {
setToastMessage(t.questions.startMatchFailed);
setToastMessage(t["Sending the match request failed. Please check your connection and try again."]);
}
}, [startMatchMutation.isError, isSyncError, t.questions.startMatchFailed]);
}, [startMatchMutation.isError, isSyncError, t["Sending the match request failed. Please check your connection and try again."]]);
const handleCloseToast = () => {
setToastMessage(null);
@ -217,6 +221,78 @@ export default function QuestionsListPage() {
}
};
if (isProfileLoading || isSectionsLoading) {
return (
<>
<PageBackground disabled />
<main
style={{ paddingBottom: "calc(100px + var(--safe-bottom))" }}
className="-mx-[17px] relative min-h-screen overflow-x-clip bg-[#F5F5F5] px-[17px] pt-0"
>
<div className="pointer-events-none absolute inset-x-0 top-0 h-[240px] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.98)_0%,rgba(255,255,255,0.7)_42%,rgba(255,255,255,0)_100%)]" />
<div className="pointer-events-none absolute -top-16 left-1/2 h-[220px] w-[220px] -translate-x-1/2 rounded-full bg-white/70 blur-3xl" />
<header
style={{ paddingTop: "calc(var(--safe-top) + 16px)" }}
className="sticky top-0 z-20 -mx-[17px] flex items-center justify-between bg-[#F5F5F5]/90 px-[17px] pb-3 backdrop-blur-md"
>
<NavigationButton
icon="back"
iconLabel={t["Close questions list"]}
onClick={(e) => {
e.preventDefault();
if (typeof window !== "undefined" && (window as any).HabibApp) {
(window as any).HabibApp.postMessage(
JSON.stringify({ action: "close_service" }),
);
} else {
router.push("/");
}
}}
/>
<h1 className="group-16 font-semibold text-[#151515]">
{t["Profile registration"]}
</h1>
<NavigationButton
icon="document"
iconLabel={t["Support"]}
className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]"
/>
</header>
<div className="relative mt-4 space-y-5">
{/* Required Steps Card Skeleton */}
<LoadingSkeleton className="h-[96px] w-full rounded-[15px] bg-[#40506A]/20 dark:bg-[#40506A]/10" />
{/* Section Cards Skeletons */}
<div className="space-y-3">
{Array.from({ length: 6 }).map((_, idx) => (
<div
key={idx}
className="flex items-center gap-3 rounded-[20px] border border-white/80 bg-white p-3 shadow-[0_12px_28px_rgba(15,23,42,0.05)]"
>
<LoadingSkeleton className="h-[44px] w-[44px] rounded-[12px]" />
<div className="flex-1 space-y-2">
<LoadingSkeleton className="h-4 w-[60%] rounded-md" />
<LoadingSkeleton className="h-3 w-[30%] rounded-md" />
</div>
<div className="flex flex-col items-end gap-3">
<LoadingSkeleton className="h-5 w-14 rounded-full" />
<LoadingSkeleton className="h-[22px] w-[22px] rounded-full" />
</div>
</div>
))}
</div>
</div>
<FixToTheEnd>
<LoadingSkeleton className="h-[52px] w-full rounded-[11px]" />
</FixToTheEnd>
</main>
</>
);
}
return (
<>
{toastMessage && (
@ -225,31 +301,31 @@ export default function QuestionsListPage() {
{isOptionalInfoSheetOpen ? (
<InformationSheet
icon="warning"
title={t.questions.optionalInfoPromptTitle}
title={t["Important Note"]}
description={
<p className="px-0.5 text-sm leading-[1.45] text-[#2D2D2D]">
{t.questions.optionalInfoPromptDescription}
{t["You've completed all required fields. However, filling in all sections will help us find better matches for you"]}
</p>
}
onClose={() => setIsOptionalInfoSheetOpen(false)}
buttons={({ close }) => (
<div className="grid w-full grid-cols-[1fr_2fr] gap-3">
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
<Button
className="rounded-[11px] border border-[#9B9B9B] bg-transparent py-[18px] text-[#8B8B8B]"
className="rounded-[11px] border border-[#9B9B9B] bg-transparent text-[#8B8B8B] min-w-0"
onClick={close}
variant="outlined"
>
{t.common.cancel}
<span className="truncate">{t["Cancel"]}</span>
</Button>
<Button
className="rounded-[11px] py-[18px]"
className="rounded-[11px] min-w-0"
disabled={isStartMatchDisabled}
onClick={() => {
close();
handleStartMatch();
}}
>
{t.questions.findingMatch}
<span className="truncate">{t["Submit"]}</span>
</Button>
</div>
)}
@ -298,7 +374,7 @@ export default function QuestionsListPage() {
>
<NavigationButton
icon="back"
iconLabel={t.questions.closeQuestionsList}
iconLabel={t["Close questions list"]}
onClick={(e) => {
e.preventDefault();
if (typeof window !== "undefined" && (window as any).HabibApp) {
@ -311,11 +387,11 @@ export default function QuestionsListPage() {
}}
/>
<h1 className="group-16 font-semibold text-[#151515]">
{t.questions.profileRegistration}
{t["Profile registration"]}
</h1>
<NavigationButton
icon="document"
iconLabel={t.common.support}
iconLabel={t["Support"]}
className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]"
/>
</header>
@ -340,12 +416,9 @@ export default function QuestionsListPage() {
</section>
</div>
<div
style={{ paddingBottom: "calc(16px + var(--safe-bottom))" }}
className="fixed inset-x-0 bottom-0 z-20 mx-auto w-full max-w-[375px] bg-[#F5F5F5]/95 px-[17px] pt-3 backdrop-blur-md"
>
<FixToTheEnd>
<Button
aria-label={t.questions.findMatches}
aria-label={t["Find Matches"]}
disabled={isStartMatchDisabled}
isLoading={startMatchMutation.isPending || isSyncing}
onClick={() => {
@ -367,11 +440,11 @@ export default function QuestionsListPage() {
className="shrink-0"
/>
<span className="leading-none font-semibold">
{t.questions.findingMatch}
{t["Submit"]}
</span>
</span>
</Button>
</div>
</FixToTheEnd>
</main>
</>
);

11
src/app/questions-list/sections-request.tsx

@ -36,9 +36,14 @@ export default function SectionsRequest() {
const isOpen = isSuccess && hasNoProgression && !hasSeenSheet;
useEffect(() => {
try {
setHasSeenSheet(
window.sessionStorage.getItem(FIRST_ENTRY_TERMS_SEEN_KEY) === "true",
);
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
setHasSeenSheet(false);
}
}, []);
useEffect(() => {
@ -46,7 +51,11 @@ export default function SectionsRequest() {
return;
}
try {
window.sessionStorage.setItem(FIRST_ENTRY_TERMS_SEEN_KEY, "true");
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
}, [isOpen]);
if (!isOpen) {
@ -79,7 +88,7 @@ export default function SectionsRequest() {
</ul>
}
buttons={({ close }) => (
<Button className="rounded-[8px] py-[14px]" onClick={close}>
<Button className="rounded-[8px]" onClick={close}>
Got it
</Button>
)}

48
src/app/request-accepted/page.tsx

@ -226,18 +226,18 @@ export default function RequestAcceptedPage() {
}
const titleText = isFemaleProfile
? t.requestAccepted.titleFemale
? t["Request Approved"]
: caseStatus === "payment_done" || caseStatus === "contacted"
? t.requestAccepted.titleMalePaymentDone
: t.requestAccepted.titleMalePaymentPending;
? t["Contact info released"]
: t["Request approved!"];
const primaryActionText = isFemaleProfile
? t.requestAccepted.primaryFemale
: t.requestAccepted.primaryMale;
? t["Report no contact"]
: t["View profile"];
const secondaryActionText = isFemaleProfile
? t.requestAccepted.secondaryFemale
? t["Record call result"]
: caseStatus === "payment_done" || caseStatus === "contacted"
? t.requestAccepted.secondaryMalePaymentDone
: t.requestAccepted.secondaryMalePaymentPending;
? t["View contact number"]
: t["Pay and get contact"];
const contactInfoPhoneItems = getContactInfoPhoneItems(
contactInfoQuery.data?.contact_info,
);
@ -356,8 +356,8 @@ export default function RequestAcceptedPage() {
{isContactInfoSheetOpen ? (
<FemaleConsentSheet
title={t.requestAccepted.contactDetailTitle}
description={t.requestAccepted.contactDetailDescription}
title={t["Contact Detail"]}
description={t["Please mention during the call that you were introduced by the Habib Marriage app."]}
buttons={
contactInfoPhoneItems.length ? (
<div className="space-y-4">
@ -367,7 +367,7 @@ export default function RequestAcceptedPage() {
</div>
) : (
<div className="rounded-[12px] bg-[#ECECEC] px-4 py-3 group-12 font-semibold text-[#555]">
{t.requestAccepted.contactNotAvailable}
{t["Contact information is not available yet."]}
</div>
)
}
@ -394,10 +394,10 @@ export default function RequestAcceptedPage() {
🎉
</div>
<h1 className="mt-8 text-center text-[24px] leading-[1.25] font-bold text-[#E03950]">
{t.candidateContact.congratsTitle}
{t["Congratulations! 🎉"]}
</h1>
<p className="mt-6 text-[#4A4A4A] group-14 font-medium leading-relaxed">
{t.candidateContact.congratsMessage}
{t["Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed."]}
</p>
</div>
) : (
@ -405,7 +405,7 @@ export default function RequestAcceptedPage() {
<div className="relative isolate flex items-center justify-center">
<Image
src="/assets/images/Group 15978804fdasf68.svg"
alt={t.requestAccepted.imageAlt}
alt={t["Request accepted"]}
width={131}
height={125}
priority
@ -420,14 +420,14 @@ export default function RequestAcceptedPage() {
{caseStatus === "contacted" ? (
<div className="w-full border border-[#ECECEC] bg-white rounded-[15px] mt-6 px-4 py-4 text-center shadow-sm max-w-[315px]">
<p className="text-[#555555] group-14 font-medium leading-relaxed">
{t.candidateContact.thankYouFeedback}
{t["Thank you for giving us feedback, we would be very happy if you also let us know the final result."]}
</p>
</div>
) : (
<p className="mt-4 max-w-[315px] group-12 leading-[1.45] font-semibold text-[#777777]">
{isFemaleProfile
? t.candidateContact.title
: t.requestAccepted.description}
? t["The selected candidate will contact your family shortly."]
: t["You can now view their family's contact details and arrange further steps."]}
</p>
)}
@ -440,7 +440,7 @@ export default function RequestAcceptedPage() {
}
className="flex-1 h-[52px] rounded-[15px] border border-[#BFBFBF] bg-white text-[#36363C] font-semibold group-14 flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F5F5F5]"
>
{t.requestAccepted.actionViewProfile}
{t["View Profile"]}
</button>
<button
@ -452,7 +452,7 @@ export default function RequestAcceptedPage() {
{outcomeMutation.isPending ? (
<DotsLoader className="text-white" />
) : (
t.candidateContact.submitOutcome
t["Submit Final Outcome"]
)}
</button>
</div>
@ -503,7 +503,7 @@ export default function RequestAcceptedPage() {
{caseStatus !== "contacted" ? (
<div className="border border-[#F0445B] bg-[#F0445B]/10 rounded-xl mt-4">
<p className="text-[#F0445B] group-12 font-semibold py-2.5 px-3.5 whitespace-pre-line">
{t.requestAccepted.contactWarning}
{t["Please be informed that from the time of this introduction, you have 48 hours (2 days) to contact the person or their respected family to declare your readiness and begin the acquaintance process. At this stage, merely an initial call to announce your presence is sufficient, and planning for further steps (such as an in-person meeting) depends entirely on your subsequent mutual agreements. Since failing to make contact within the specified time might be considered socially disrespectful, if no action is taken within these 2 days, the introduced match will be removed according to the platform's rules. We also remind you that this issue may lead to restrictions such as delays in future introductions and financial penalties."]}
</p>
</div>
) : null}
@ -514,11 +514,11 @@ export default function RequestAcceptedPage() {
<div className="space-y-8 pb-20">
{/* Advisor section */}
<AdvisorActionsCard
title={t.findingMatch.advisorTitle}
description={t.findingMatch.advisorDescription}
title={t["Get an advisor"]}
description={t["Not sure what to do next? Our psychology section is here to guide you at every step."]}
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorLabel={t["Get Advisor"]}
getAdvisorHref="/questions-list"
/>
@ -536,7 +536,7 @@ export default function RequestAcceptedPage() {
/>
<h2 className="group-14 leading-none font-semibold text-[#747474]">
{t.requestAccepted.profileLocked}
{t["Profile is locked"]}
</h2>
</div>
</div>

17
src/app/request-sent/page.tsx

@ -52,13 +52,16 @@ export default function RequestSentPage() {
);
}
const copy = t.findingMatch;
const requestSentCopy = (t as any).requestSent ?? {
title: "درخواست ارسال شد",
description:
"درخواست معرفی شما از طرف ما به خانواده طرف مقابل ارسال شد. در صورت تایید ایشان، اطلاعات تماس به اطلاع شما خواهد رسید.",
matchProfile: "مشاهده پروفایل گزینه",
profileLocked: "پروفایل قفل است",
const copy = {
advisorTitle: t["Get an advisor"],
advisorDescription: t["Not sure what to do next? Our psychology section is here to guide you at every step."],
getAdvisor: t["Get Advisor"],
};
const requestSentCopy = {
title: t["Request Sent"],
description: t["Your request has been sent. Once the lady reviews your request, you will be notified."],
matchProfile: t["View More Details"],
profileLocked: t["Profile is locked"],
};
return (

15
src/components/Componentes/button.tsx

@ -12,7 +12,12 @@ import { GoArrowRight } from "react-icons/go";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
type ButtonVariant = "default" | "secondary" | "countdown" | "outlined";
type ButtonVariant =
| "default"
| "secondary"
| "countdown"
| "outlined"
| "dark";
type ArrowDirection = "left" | "right";
export type ButtonProps = Omit<
@ -123,12 +128,14 @@ export function Button({
"items-center",
"justify-center",
"h-[52px]",
"rounded-[11px]",
variant === "dark" ? "rounded-[9px]" : "rounded-[11px]",
"px-4",
"text-center",
"transition-opacity",
isOutlined
variant === "outlined"
? "border border-[#8B8B8B] bg-transparent text-[#8B8B8B]"
: variant === "dark"
? "border-none bg-[#2B2C31] text-white shadow-none hover:opacity-90"
: "bg-linear-to-tl from-[#FE6F82] to-[#E03950] text-white",
isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
className,
@ -169,7 +176,7 @@ export function Button({
<span className="flex w-full items-center justify-center gap-2">
{renderArrow("left")}
<span className="flex min-w-0 flex-col items-center justify-center">
<span className="text-center group-16 font-semibold leading-none">
<span className="flex items-center justify-center gap-2 text-center group-16 font-semibold leading-none">
{children}
</span>
{description && !isOutlined ? (

29
src/components/Componentes/call-result-sheet.tsx

@ -25,7 +25,14 @@ export function CallResultSheet({
...props
}: CallResultSheetProps) {
const { dictionary: t } = useI18n();
const callResultOptions = t.sheets.callOptions;
const callResultOptions = [
t["Not a good personal fit"],
t["No mutual interest"],
t["Different expectations"],
t["No connection felt"],
t["Location not suitable"],
t["Other reasons"]
];
const groupId = useId();
const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true);
@ -100,7 +107,7 @@ export function CallResultSheet({
.join(" ")}
role="dialog"
aria-modal="true"
aria-label={t.sheets.callResult}
aria-label={t["Call result"]}
tabIndex={-1}
onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) {
@ -130,7 +137,7 @@ export function CallResultSheet({
>
<div className="mx-auto flex flex-col items-center">
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]">
{t.sheets.callResult}
{t["Call result"]}
</h2>
<div
@ -139,7 +146,7 @@ export function CallResultSheet({
aria-labelledby={groupId}
>
<span id={groupId} className="sr-only">
{t.sheets.selectCallResult}
{t["Select call result"]}
</span>
<div className="flex flex-col gap-[14px]">
@ -191,27 +198,27 @@ export function CallResultSheet({
</div>
</div>
<div className="mt-8 grid w-full grid-cols-[1fr_2fr] gap-3">
<div className="mt-8 grid w-full grid-cols-[33fr_67fr] gap-3">
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={closeSheet}
>
<div className="inline-flex w-full items-center justify-center rounded-[14px] border border-[#9A9A9A] bg-[#F7F7F7] px-4 py-[18px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90">
{t.common.cancel}
<div className="inline-flex w-full items-center justify-center rounded-[14px] border border-[#9A9A9A] bg-[#F7F7F7] px-2 h-[52px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t["Cancel"]}</span>
</div>
</button>
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={() => {
onSubmit?.(selectedReason);
closeSheet();
}}
>
<div className="inline-flex w-full items-center justify-center rounded-[14px] bg-[#F0445B] px-4 py-[18px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90">
{t.common.submit}
<div className="inline-flex w-full items-center justify-center rounded-[14px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t["Submit"]}</span>
</div>
</button>
</div>

136
src/components/Componentes/dismiss-reason-sheet.tsx

@ -1,9 +1,9 @@
"use client";
import type { HTMLAttributes } from "react";
import { useEffect, useId, useState } from "react";
import Button from "./button";
import { useEffect, useId, useRef, useState } from "react";
import { useI18n } from "@/translations/provider";
import Button from "./button";
const EXIT_ANIMATION_MS = 220;
@ -24,13 +24,34 @@ export function DismissReasonSheet({
...props
}: DismissReasonSheetProps) {
const { dictionary: t } = useI18n();
const options = t.sheets.callOptions;
const options = [
t["Not a good personal fit"],
t["No mutual interest"],
t["Different expectations"],
t["No connection felt"],
t["Location not suitable"],
t["Other reasons"]
];
const groupId = useId();
const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false);
const [selectedReason, setSelectedReason] = useState<string>(options[0]);
const [selectedReasons, setSelectedReasons] = useState<string[]>([]);
const [reasonText, setReasonText] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const otherReason = options[options.length - 1];
if (selectedReasons.includes(otherReason)) {
const timeoutId = setTimeout(() => {
textareaRef.current?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}, 100);
return () => clearTimeout(timeoutId);
}
}, [selectedReasons, options]);
const closeSheet = () => {
if (isClosing) {
@ -98,7 +119,7 @@ export function DismissReasonSheet({
.join(" ")}
role="dialog"
aria-modal="true"
aria-label={t.sheets.dismissReasons}
aria-label={t["Dismiss reasons"]}
tabIndex={-1}
onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) {
@ -119,63 +140,77 @@ export function DismissReasonSheet({
<section
{...props}
className={[
"w-full sm:max-w-[375px] rounded-t-[34px] bg-[#F9F8F8] p-3.5 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform",
"w-full sm:max-w-[375px] h-[80vh] rounded-t-[15px] bg-[#F9F8F8] p-3.5 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform flex flex-col justify-between",
isClosing || isEntering ? "translate-y-full" : "translate-y-0",
className,
]
.filter(Boolean)
.join(" ")}
>
<div className="mx-auto flex flex-col items-center">
<div className="mx-auto flex flex-col items-center w-full h-full justify-between">
<div className="w-full flex flex-col items-center">
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]">
{t.sheets.dismissReasons}
{t["Dismiss reasons"]}
</h2>
<div
className="mt-5 w-full"
role="radiogroup"
aria-labelledby={groupId}
>
<span id={groupId} className="sr-only">
{t.sheets.dismissReasons}
</span>
<p className="mt-3.5 w-full text-start text-[14px] leading-[1.45] text-[#2C2C2C] dir-auto">
{t["Please provide the full reason for rejecting the submitted item"]}
</p>
<div className="flex flex-col gap-[14px]">
<fieldset className="mt-4 w-full" aria-labelledby={groupId}>
<legend id={groupId} className="sr-only">
{t["Dismiss reasons"]}
</legend>
<div className="flex flex-col gap-[14px] max-h-[55vh] overflow-y-auto pr-1">
{options.map((option) => {
const checked = selectedReason === option;
const checked = selectedReasons.includes(option);
const showTextArea = option === options[options.length - 1];
return (
<div
key={option}
className={[
"w-full rounded-[14px] bg-[#ECECEC] px-[14px] py-[18px]",
checked ? "text-[#171717]" : "text-[#7B7B7B]",
]
.filter(Boolean)
.join(" ")}
>
<div key={option} className="w-full flex flex-col gap-2">
<button
type="button"
className={[
"flex w-full items-start gap-3 text-left",
"flex w-full items-center gap-3 bg-[#ECECEC] px-5 py-[14px] text-start transition-all rounded-[15px]",
checked ? "text-[#171717]" : "text-[#7B7B7B]",
]
.filter(Boolean)
.join(" ")}
onClick={() => setSelectedReason(option)}
onClick={() => {
setSelectedReasons((prev) =>
prev.includes(option)
? prev.filter((r) => r !== option)
: [...prev, option],
);
}}
>
<span
aria-hidden="true"
className={[
"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border",
checked ? "border-[#F0445B]" : "border-[#9E9E9E]",
"flex h-6 w-6 shrink-0 items-center justify-center rounded-[6px] border transition-colors",
checked
? "border-[#F0445B] bg-[#F0445B]"
: "border-[#9E9E9E] bg-transparent",
]
.filter(Boolean)
.join(" ")}
>
{checked ? (
<span className="h-3 w-3 rounded-full bg-[#F0445B]" />
<svg
aria-hidden="true"
className="h-4 w-4 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={3}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
) : null}
</span>
@ -188,35 +223,42 @@ export function DismissReasonSheet({
{checked && showTextArea ? (
<textarea
className="mt-3 h-[125px] w-full resize-none rounded-[16px] border border-[#454545] bg-transparent px-3 py-3 text-[16px] text-[#171717] outline-none placeholder:text-[#AAAAAA]"
placeholder={t.sheets.dismissPlaceholder}
ref={textareaRef}
className="mt-1 h-[125px] w-full resize-none rounded-[16px] border border-[#454545] bg-transparent px-3 py-3 text-[16px] text-[#171717] outline-none placeholder:text-[#AAAAAA]"
placeholder={t["Feel free to briefly explain your decision..."]}
value={reasonText}
onChange={(event) => setReasonText(event.target.value)}
onChange={(event) =>
setReasonText(event.target.value)
}
/>
) : null}
</div>
);
})}
</div>
</fieldset>
</div>
<p className="mt-4 w-full text-left text-[14px] leading-[1.45] text-[#2C2C2C]">
{t.sheets.dismissDescription}
</p>
<div className="mt-[14px] w-full">
<div className="mt-4 w-full">
<Button
className="rounded-[14px] py-[18px] shadow-none"
className="rounded-[15px] shadow-none"
disabled={
selectedReasons.length === 0 ||
(selectedReasons.includes(options[options.length - 1]) &&
reasonText.trim() === "")
}
onClick={() => {
onSubmit?.(
reasonText
? `${selectedReason}\n${reasonText}`
: selectedReason,
);
const activeReasons = selectedReasons.map((r) => {
if (r === options[options.length - 1]) {
return reasonText ? `${r}: ${reasonText}` : r;
}
return r;
});
onSubmit?.(activeReasons.join("\n"));
closeSheet();
}}
>
{t.common.decline}
{t["Decline"]}
</Button>
</div>
</div>

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

@ -1,18 +1,20 @@
"use client";
import { useEffect, useState } from "react";
import { IoAlertCircle, IoClose } from "react-icons/io5";
import { IoAlertCircle, IoClose, IoCheckmarkCircle } from "react-icons/io5";
type ErrorToastProps = {
message: string;
onClose: () => void;
duration?: number;
variant?: "error" | "success";
};
export default function ErrorToast({
message,
onClose,
duration = 4000,
variant = "error",
}: ErrorToastProps) {
const [isVisible, setIsVisible] = useState(true);
@ -30,6 +32,8 @@ export default function ErrorToast({
setTimeout(onClose, 300);
};
const isSuccess = variant === "success";
return (
<div
className={[
@ -37,19 +41,30 @@ export default function ErrorToast({
isVisible
? "translate-y-0 opacity-100 scale-100"
: "-translate-y-4 opacity-0 scale-95",
"bg-red-50/95 dark:bg-red-950/95 text-red-800 dark:text-red-200 border-red-100/80 dark:border-red-900/50",
isSuccess
? "bg-green-50/95 dark:bg-green-950/95 text-green-800 dark:text-green-200 border-green-100/80 dark:border-green-900/50"
: "bg-red-50/95 dark:bg-red-950/95 text-red-800 dark:text-red-200 border-red-100/80 dark:border-red-900/50",
].join(" ")}
role="alert"
>
{isSuccess ? (
<IoCheckmarkCircle className="size-5 shrink-0 text-green-500" />
) : (
<IoAlertCircle className="size-5 shrink-0 text-red-500" />
)}
<span className="flex-1 text-sm font-semibold leading-normal">
{message}
</span>
<button
type="button"
onClick={handleClose}
className="flex size-6 items-center justify-center rounded-lg text-red-500 hover:bg-red-100/50 dark:hover:bg-red-900/50 transition-colors"
aria-label="Close error toast"
className={[
"flex size-6 items-center justify-center rounded-lg transition-colors",
isSuccess
? "text-green-500 hover:bg-green-100/50 dark:hover:bg-green-900/50"
: "text-red-500 hover:bg-red-100/50 dark:hover:bg-red-900/50",
].join(" ")}
aria-label="Close toast"
>
<IoClose className="size-4" />
</button>

2
src/components/Componentes/female-consent-sheet.tsx

@ -102,7 +102,7 @@ export function FemaleConsentSheet({
.join(" ")}
role="dialog"
aria-modal="true"
aria-label={t.sheets.informationSheet}
aria-label={t["Information sheet"]}
tabIndex={-1}
onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) {

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

@ -0,0 +1,18 @@
"use client";
import type { ReactNode } from "react";
type FixToTheEndProps = {
children: ReactNode;
};
export function FixToTheEnd({ children }: FixToTheEndProps) {
return (
<div
style={{ paddingBottom: "calc(16px + var(--safe-bottom))" }}
className="fixed inset-x-0 bottom-0 z-20 mx-auto w-full max-w-[375px] bg-[#F5F5F5]/95 px-[17px] pt-3 backdrop-blur-md"
>
{children}
</div>
);
}

8
src/components/Componentes/help-modal.tsx

@ -36,11 +36,11 @@ export function HelpModal({
const resolvedTitle = "Tips";
const resolvedDescription =
description ??
(t.common as Record<string, string | undefined>)?.helpDescription ??
t["Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance."] ??
"Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance.";
const resolvedButtonText =
buttonText ??
(t.common as Record<string, string | undefined>)?.gotIt ??
t["Got it"] ??
"Got it";
const closeSheet = useCallback(() => {
@ -203,9 +203,7 @@ export function HelpModal({
})()}
<div className="mt-5 w-full">
<Button onClick={closeSheet} className="py-[14px]">
{resolvedButtonText}
</Button>
<Button onClick={closeSheet}>{resolvedButtonText}</Button>
</div>
</div>
</section>

38
src/components/Componentes/information-sheet.tsx

@ -4,6 +4,7 @@ import Image, { type StaticImageData } from "next/image";
import type { HTMLAttributes, ReactNode } from "react";
import { useEffect, useState } from "react";
import Button from "./button";
import { LoadingSkeleton } from "./loading-skeleton";
import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220;
@ -33,11 +34,12 @@ export type InformationSheetProps = Omit<
"children" | "title"
> & {
icon?: InformationSheetIcon | null;
title: ReactNode | ((controls: { close: () => void }) => ReactNode);
title?: ReactNode | ((controls: { close: () => void }) => ReactNode);
description?: ReactNode;
buttons?: ReactNode | ((controls: { close: () => void }) => ReactNode);
closeOnOutside?: boolean;
onClose?: () => void;
isLoading?: boolean;
};
const DEFAULT_ICON = {
@ -132,9 +134,16 @@ export function InformationSheet({
closeOnOutside = true,
onClose,
className,
isLoading = false,
...props
}: InformationSheetProps) {
const { dictionary: t } = useI18n();
const { locale, dictionary: t } = useI18n();
const isRtl =
locale === "fa" ||
locale === "ar" ||
locale === "ur" ||
locale === "he" ||
locale === "ks";
const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false);
@ -150,9 +159,7 @@ export function InformationSheet({
const resolvedTitle = typeof title === "function" ? title(controls) : title;
const resolvedButtons =
typeof buttons === "string" ? (
<Button onClick={closeSheet} className="py-[18px]">
{buttons}
</Button>
<Button onClick={closeSheet}>{buttons}</Button>
) : typeof buttons === "function" ? (
buttons(controls)
) : (
@ -217,8 +224,9 @@ export function InformationSheet({
.join(" ")}
role="dialog"
aria-modal="true"
aria-label={t.sheets.informationSheet}
aria-label={t["Information sheet"]}
tabIndex={-1}
dir={isRtl ? "rtl" : "ltr"}
onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) {
closeSheet();
@ -245,7 +253,21 @@ export function InformationSheet({
.filter(Boolean)
.join(" ")}
>
<div className="mx-auto flex flex-col items-center">
<div className="mx-auto flex flex-col items-center w-full">
{isLoading ? (
<>
<LoadingSkeleton className="h-[50px] w-[50px] rounded-full" />
<LoadingSkeleton className="mt-3.5 h-[20px] w-[140px]" />
<div className="mt-3 flex flex-col items-center gap-2 w-full">
<LoadingSkeleton className="h-[12px] w-[220px]" />
<LoadingSkeleton className="h-[12px] w-[160px]" />
</div>
<div className="mt-4 flex w-full gap-3">
<LoadingSkeleton className="h-[52px] flex-1 rounded-[11px]" />
</div>
</>
) : (
<>
{resolvedIcon ? (
<Image
src={resolvedIcon.src}
@ -274,6 +296,8 @@ export function InformationSheet({
{resolvedButtons}
</div>
) : null}
</>
)}
</div>
</section>
</div>

27
src/components/Componentes/loading-skeleton.tsx

@ -0,0 +1,27 @@
import type { ComponentProps } from "react";
interface LoadingSkeletonProps extends ComponentProps<"div"> {
variant?: "pulse" | "shimmer";
}
export function LoadingSkeleton({
className = "",
variant = "shimmer",
...props
}: LoadingSkeletonProps) {
const baseClasses =
variant === "shimmer"
? "shimmer-bg"
: "animate-pulse bg-neutral-200/80 dark:bg-neutral-800/80";
const hasRoundedClass = className.split(" ").some((c) => c.startsWith("rounded-"));
const roundedClass = hasRoundedClass ? "" : "rounded-md";
return (
<div
data-slot="loading-skeleton"
className={`${baseClasses} ${roundedClass} ${className}`.trim()}
{...props}
/>
);
}

280
src/components/Componentes/navigation-button.tsx

@ -3,12 +3,22 @@
import Image from "next/image";
import { useRouter } from "next/navigation";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { useState } from "react";
import { useState, useRef, useEffect } from "react";
import { GoArrowLeft } from "react-icons/go";
import { HiEllipsisVertical } from "react-icons/hi2";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import { useI18n } from "@/translations/provider";
import HelpModal from "./help-modal";
import SupportSheet from "./support-sheet";
import InformationSheet from "./information-sheet";
import { DotsLoader } from "./button";
import { useQueryClient } from "@tanstack/react-query";
import ErrorToast from "./error-toast";
import {
useHabcoinPaymentMutation,
extractHabcoinPaymentUrl,
} from "@/hooks/marriage/use-habcoin-payment";
type NavigationButtonIcon =
| "back"
@ -17,7 +27,8 @@ type NavigationButtonIcon =
| "info"
| "document"
| "subscription"
| "consultation";
| "consultation"
| "more";
type NavigationButtonVariant = "default" | "transparent";
export type NavigationButtonProps = Omit<
@ -49,11 +60,157 @@ export function NavigationButton({
const { dictionary: t } = useI18n();
const [isHelpOpen, setIsHelpOpen] = useState(false);
const [isSupportOpen, setIsSupportOpen] = useState(false);
const { data: profile } = useMarriageProfileQuery();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isSubscriptionInfoOpen, setIsSubscriptionInfoOpen] = useState(false);
const [isSubscriptionLoading, setIsSubscriptionLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { data: profile, refetch } = useMarriageProfileQuery();
const paymentMutation = useHabcoinPaymentMutation();
const queryClient = useQueryClient();
const [toastMessage, setToastMessage] = useState<string | null>(null);
const [toastVariant, setToastVariant] = useState<"error" | "success">(
"error",
);
const [isRenewing, setIsRenewing] = useState(false);
const renderSubscriptionModal = () => {
if (!isSubscriptionInfoOpen) return null;
if (isSubscriptionLoading) {
return (
<InformationSheet
isLoading={true}
onClose={() => setIsSubscriptionInfoOpen(false)}
/>
);
}
return hasActiveSubscription ? (
<>
<InformationSheet
icon="/assets/images/diamond-color.png"
title={t["Subscription Status"]}
description={t["{days} days remaining of your subscription."].replace(
"{days}",
String(profile?.active_subscription?.remaining_days ?? 0),
)}
onClose={() => setIsSubscriptionInfoOpen(false)}
buttons={({ close }) => (
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={close}
>
<div className="inline-flex w-full items-center justify-center rounded-[18px] border border-[#9A9A9A] bg-[#F7F7F7] px-2 h-[52px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t["Back"]}</span>
</div>
</button>
<button
type="button"
disabled={!profile?.recommended_plan?.id || isRenewing}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={async () => {
const planId = profile?.recommended_plan?.id;
if (!planId) return;
setIsRenewing(true);
setToastMessage(null);
setToastVariant("error");
try {
const paymentResponse =
await paymentMutation.mutateAsync(planId);
const paymentUrl =
extractHabcoinPaymentUrl(paymentResponse);
if (paymentUrl) {
window.location.assign(paymentUrl);
return;
}
// Success! Refetch profile query to update remaining_days in modal.
await queryClient.refetchQueries({
queryKey: marriageQueryKeys.profile(),
});
// Close the modal and show success toast
setIsSubscriptionInfoOpen(false);
setToastVariant("success");
setToastMessage(
t["Payment successful"] || "Payment successful",
);
} catch (err: any) {
console.error("Renewal payment failed", err);
const errMessage =
err?.response?.data?.error || err?.message || "";
setToastVariant("error");
if (
errMessage.includes("Not enough coins") ||
err?.response?.status === 400
) {
setToastMessage(
t["Insufficient coin balance. Please recharge your account."] ||
"Insufficient coin balance. Please recharge your account.",
);
} else {
setToastMessage(errMessage || "Payment failed");
}
} finally {
setIsRenewing(false);
}
}}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[18px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">
{isRenewing
? t["Renewing..."]
: t["Renew Subscription"]}
</span>
{!isRenewing && (
<span className="inline-flex items-center gap-1 rounded-full bg-[#E43B51] p-1.5 text-xs font-semibold leading-none text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.12)] shrink-0 min-w-0 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={18}
height={18}
className="shrink-0"
/>
<span className="truncate">
{profile?.recommended_plan?.price || "100"}
</span>
</span>
)}
</div>
</button>
</div>
)}
/>
</>
) : (
<InformationSheet
icon="/assets/images/icon-park-outline_diamond.svg"
title={t["No Active Subscription"]}
description={t["You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you."]}
buttons={t["Got it"]}
onClose={() => setIsSubscriptionInfoOpen(false)}
/>
);
};
const isFemale = profile?.gender === "female";
const hasActiveSubscription = !!profile?.active_subscription;
useEffect(() => {
if (!isDropdownOpen) return;
const handleOutsideClick = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
};
document.addEventListener("mousedown", handleOutsideClick);
return () => document.removeEventListener("mousedown", handleOutsideClick);
}, [isDropdownOpen]);
// پشتیبانی برای هم خانم‌ها و هم آقایان در هر وضعیتی فعال و در دسترس است
const hasSupportAccess = true;
@ -124,6 +281,13 @@ export function NavigationButton({
height={24}
/>
);
case "more":
return (
<HiEllipsisVertical
aria-hidden="true"
className={`size-6 ${variant === "transparent" ? "text-white" : "text-[#111111]"}`}
/>
);
default:
return null;
}
@ -138,9 +302,97 @@ export function NavigationButton({
const resolvedHelpDescription =
helpDescription ??
(icon === "document"
? (t.questions as any)?.testIntroDisclaimer
? (t as any)["Taking this test is not mandatory, but it will help you better search for a spouse."]
: undefined);
if (icon === "more") {
return (
<div className="relative inline-block text-left" ref={dropdownRef}>
<button
{...props}
type={type}
aria-label={iconLabel ?? ((t as any)["More"] || "More")}
onClick={(event) => {
props.onClick?.(event);
if (!event.defaultPrevented) {
setIsDropdownOpen((prev) => !prev);
}
}}
className={[
"inline-flex items-center justify-center rounded-[15px] p-2 cursor-pointer transition-transform active:scale-95",
variant === "transparent" ? "bg-white/20" : "bg-[#FFFFFF]",
className,
]
.filter(Boolean)
.join(" ")}
>
{iconNode}
</button>
{isDropdownOpen && (
<div className="absolute right-0 mt-2 w-48 origin-top-right rounded-2xl bg-white shadow-[0_12px_30px_rgba(0,0,0,0.15)] ring-1 ring-black/5 focus:outline-hidden z-50 overflow-hidden divide-y divide-gray-100 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="py-1">
<button
type="button"
onClick={() => {
setIsDropdownOpen(false);
setIsSupportOpen(true);
}}
className="flex w-full items-center gap-3 px-4 py-3 text-start text-sm font-semibold text-gray-700 hover:bg-gray-50 cursor-pointer border-0 bg-transparent"
>
<Image
src="/assets/images/support.svg"
alt=""
width={20}
height={20}
className="size-5 shrink-0"
/>
<span>{t["Support"]}</span>
</button>
<button
type="button"
onClick={() => {
setIsDropdownOpen(false);
setIsSubscriptionInfoOpen(true);
setIsSubscriptionLoading(true);
refetch().finally(() => {
setIsSubscriptionLoading(false);
});
}}
className="flex w-full items-center gap-3 px-4 py-3 text-start text-sm font-semibold text-gray-700 hover:bg-gray-50 cursor-pointer border-0 bg-transparent"
>
<Image
src="/assets/images/diamond-color.png"
alt=""
width={20}
height={20}
className="size-5 shrink-0"
/>
<span>{t["Subscription"]}</span>
</button>
</div>
</div>
)}
<SupportSheet
isOpen={isSupportOpen}
onClose={() => setIsSupportOpen(false)}
/>
{renderSubscriptionModal()}
{toastMessage && (
<ErrorToast
message={toastMessage}
variant={toastVariant}
onClose={() => setToastMessage(null)}
/>
)}
</div>
);
}
return (
<>
<button
@ -149,9 +401,9 @@ export function NavigationButton({
aria-label={
iconLabel ??
(icon === "back"
? t.common.back
? t["Back"]
: icon === "consultation"
? (t.common as any).consultation
? (t as any)["Consultation"]
: icon)
}
onClick={(event) => {
@ -173,6 +425,12 @@ export function NavigationButton({
router.back();
} else if (icon === "support") {
setIsSupportOpen(true);
} else if (icon === "subscription") {
setIsSubscriptionInfoOpen(true);
setIsSubscriptionLoading(true);
refetch().finally(() => {
setIsSubscriptionLoading(false);
});
}
}
}
@ -202,6 +460,16 @@ export function NavigationButton({
isOpen={isSupportOpen}
onClose={() => setIsSupportOpen(false)}
/>
{renderSubscriptionModal()}
{toastMessage && (
<ErrorToast
message={toastMessage}
variant={toastVariant}
onClose={() => setToastMessage(null)}
/>
)}
</>
);
}

4
src/components/Componentes/notice-box.tsx

@ -32,7 +32,9 @@ export function NoticeBox({
.filter(Boolean)
.join(" ")}
>
<p className={`text-[#F14B46] group-12 font-semibold leading-[1.6] ${alignClass}`}>
<p
className={`text-[#F14B46] group-12 font-semibold leading-[1.6] ${alignClass}`}
>
{text || children}
</p>
</div>

24
src/components/Componentes/outcome-selection-sheet.tsx

@ -85,8 +85,8 @@ export function OutcomeSelectionSheet({
}
const options = [
{ value: "success" as const, label: t.candidateContact.marriageSuccess },
{ value: "failure" as const, label: t.candidateContact.marriageFailure },
{ value: "success" as const, label: t["We reached an agreement"] },
{ value: "failure" as const, label: t["We did not reach an agreement"] },
];
return (
@ -101,7 +101,7 @@ export function OutcomeSelectionSheet({
.join(" ")}
role="dialog"
aria-modal="true"
aria-label={t.candidateContact.outcomeTitle}
aria-label={t["What was the outcome of your contact?"]}
tabIndex={-1}
onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) {
@ -131,7 +131,7 @@ export function OutcomeSelectionSheet({
>
<div className="mx-auto flex flex-col items-center">
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]">
{t.candidateContact.outcomeTitle}
{t["What was the outcome of your contact?"]}
</h2>
<div
@ -140,7 +140,7 @@ export function OutcomeSelectionSheet({
aria-labelledby={groupId}
>
<span id={groupId} className="sr-only">
{t.candidateContact.outcomeTitle}
{t["What was the outcome of your contact?"]}
</span>
<div className="flex flex-col gap-[14px]">
@ -184,27 +184,27 @@ export function OutcomeSelectionSheet({
</div>
</div>
<div className="mt-8 grid w-full grid-cols-[1fr_2fr] gap-3">
<div className="mt-8 grid w-full grid-cols-[33fr_67fr] gap-3">
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={closeSheet}
>
<div className="inline-flex w-full items-center justify-center rounded-[14px] border border-[#9A9A9A] bg-[#F7F7F7] px-4 py-[18px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90">
{t.common.cancel}
<div className="inline-flex w-full items-center justify-center rounded-[14px] border border-[#9A9A9A] bg-[#F7F7F7] px-2 h-[52px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t["Cancel"]}</span>
</div>
</button>
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={() => {
onSubmit?.(selectedStatus);
closeSheet();
}}
>
<div className="inline-flex w-full items-center justify-center rounded-[14px] bg-[#F0445B] px-4 py-[18px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90">
{t.common.submit}
<div className="inline-flex w-full items-center justify-center rounded-[14px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t["Submit"]}</span>
</div>
</button>
</div>

35
src/components/Componentes/page-header.tsx

@ -1,6 +1,7 @@
"use client";
import { useI18n } from "@/translations/provider";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
NavigationButton,
type NavigationButtonProps,
@ -20,24 +21,42 @@ export function PageHeader({
rightButton,
}: PageHeaderProps) {
const { dictionary: t } = useI18n();
const { data: profile } = useMarriageProfileQuery();
const isMale = profile?.gender === "male";
const iconFromProp = rightButton?.icon;
const isDefaultOrSubscription =
iconFromProp === undefined || iconFromProp === "subscription";
let finalIcon: NavigationButtonProps["icon"] = iconFromProp || "support";
if (isDefaultOrSubscription) {
if (isMale) {
if (profile?.active_subscription) {
finalIcon = "more";
} else {
finalIcon = "subscription";
}
} else {
finalIcon = "support";
}
}
const { icon: rightButtonIcon, ...rightButtonRest } = rightButton || {};
return (
<header
className={[
"flex items-center justify-between pb-3",
className,
]
className={["flex items-center justify-between pb-3", className]
.filter(Boolean)
.join(" ")}
>
<NavigationButton icon="back" {...leftButton} />
<h1 className="font-faminela text-[20px] font-normal text-foreground">
{t.common.appName}
{t["Habib Marriage"]}
</h1>
<NavigationButton
icon="support"
iconLabel={t.common.support}
{...rightButton}
icon={finalIcon}
{...(finalIcon === "support" ? { iconLabel: t["Support"] } : {})}
{...rightButtonRest}
/>
</header>
);

59
src/components/Componentes/progress-helper.ts

@ -169,16 +169,18 @@ export function getLocalSectionProgress(
required: isQuestionRequiredForProfile(question, profileContext),
}));
const findAnswer = (
questionTitle: string,
questionIndex: number,
originalIndex?: number,
) => {
const indexToUse =
originalIndex !== undefined ? originalIndex : questionIndex;
const key = `q${indexToUse + 1}_${slugifyTitle(questionTitle)}`;
const getQuestionFieldKey = (question: any, questionIndex: number) => {
const index =
question.originalIndex !== undefined
? question.originalIndex
: questionIndex;
return `q${index + 1}_${slugifyTitle(question.englishTitle || question.title)}`;
};
const findAnswer = (question: any, questionIndex: number) => {
const key = getQuestionFieldKey(question, questionIndex);
const field = fields.find(
(f) => f && (f.key === key || f.label === questionTitle),
(f) => f && (f.key === key || f.label === question.title),
);
return field?.value;
};
@ -195,11 +197,7 @@ export function getLocalSectionProgress(
let employmentSelectedOptionIndex = -1;
if (employmentQuestionIndex !== -1) {
const employmentQuestion = profileVisible[employmentQuestionIndex];
const ans = findAnswer(
employmentQuestion.title,
employmentQuestionIndex,
employmentQuestion.originalIndex,
);
const ans = findAnswer(employmentQuestion, employmentQuestionIndex);
if (ans) {
employmentSelectedOptionIndex =
employmentQuestion.extras?.options?.indexOf(String(ans)) ?? -1;
@ -217,11 +215,7 @@ export function getLocalSectionProgress(
let survivalSelectedOptionIndex = -1;
if (survivalStatusQuestionIndex !== -1) {
const survivalQuestion = profileVisible[survivalStatusQuestionIndex];
const ans = findAnswer(
survivalQuestion.title,
survivalStatusQuestionIndex,
survivalQuestion.originalIndex,
);
const ans = findAnswer(survivalQuestion, survivalStatusQuestionIndex);
if (ans) {
survivalSelectedOptionIndex =
survivalQuestion.extras?.options?.indexOf(String(ans)) ?? -1;
@ -238,11 +232,7 @@ export function getLocalSectionProgress(
let isCircumstancesSelected = false;
if (maritalStatusQuestionIndex !== -1) {
const maritalQuestion = profileVisible[maritalStatusQuestionIndex];
const ans = findAnswer(
maritalQuestion.title,
maritalStatusQuestionIndex,
maritalQuestion.originalIndex,
);
const ans = findAnswer(maritalQuestion, maritalStatusQuestionIndex);
if (ans) {
const selectedIdx =
maritalQuestion.extras?.options?.indexOf(String(ans)) ?? -1;
@ -260,11 +250,7 @@ export function getLocalSectionProgress(
let maritalSelectedIndex = -1;
if (currentMaritalQuestionIndex !== -1) {
const maritalQuestion = profileVisible[currentMaritalQuestionIndex];
const ans = findAnswer(
maritalQuestion.title,
currentMaritalQuestionIndex,
maritalQuestion.originalIndex,
);
const ans = findAnswer(maritalQuestion, currentMaritalQuestionIndex);
if (ans) {
maritalSelectedIndex =
maritalQuestion.extras?.options?.indexOf(String(ans)) ?? -1;
@ -284,11 +270,7 @@ export function getLocalSectionProgress(
if (custodyQuestionIndex !== -1) {
const custodyQuestion = profileVisible[custodyQuestionIndex];
const ans = findAnswer(
custodyQuestion.title,
custodyQuestionIndex,
custodyQuestion.originalIndex,
);
const ans = findAnswer(custodyQuestion, custodyQuestionIndex);
if (ans) {
const ansList = Array.isArray(ans) ? ans.map(String) : [String(ans)];
hasChildrenSelected = ansList.some(
@ -422,11 +404,7 @@ export function getLocalSectionProgress(
if (dependentQuestionIndex !== -1) {
const dependentQuestion = profileVisible[dependentQuestionIndex];
const answer = findAnswer(
dependentQuestion.title,
dependentQuestionIndex,
dependentQuestion.originalIndex,
);
const answer = findAnswer(dependentQuestion, dependentQuestionIndex);
if (Array.isArray(answer)) {
return answer.some((ans) => values.includes(String(ans)));
}
@ -490,8 +468,7 @@ export function getLocalSectionProgress(
const answeredCount = requiredQuestions.filter((q) => {
const idx = profileVisible.indexOf(q);
const indexToUse = q.originalIndex !== undefined ? q.originalIndex : idx;
const key = `q${indexToUse + 1}_${slugifyTitle(q.title)}`;
const key = getQuestionFieldKey(q, idx);
const field = fields.find(
(f) => f && (f.key === key || f.label === q.title),
);

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

@ -57,7 +57,7 @@ export function QuestionCard({
return (
<Link
href={localizePath(`/questions-list/${item.slug}`, locale)}
aria-label={t.questions.openQuestion.replace("{title}", item.title)}
aria-label={t["Open {title}"].replace("{title}", item.title)}
className="block rounded-[20px] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#F26C85]"
>
<article
@ -87,14 +87,14 @@ export function QuestionCard({
{item.title}
</h2>
<p className="mt-1.5 group-10 leading-none font-medium text-[#7A7A7A]">
{t.common.estimateTime}: {item.estimate}
{t["Estimate time"]}: {item.estimate}
</p>
</div>
<div className="flex min-h-[44px] shrink-0 flex-col items-end justify-between self-stretch">
{item.required ? (
<span className="flex items-center justify-center rounded-full bg-[#F8D7DA] dark:bg-semantic-danger-bg px-3 py-1 text-center text-[10px] font-bold text-[#D9383A] dark:text-semantic-danger-text whitespace-nowrap">
{t.common.required}
{t["Required"]}
</span>
) : item.showInfoBadge ? (
<button

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

@ -115,7 +115,7 @@ export function QuestionDropdown({
const isOtherQuestion =
isPersonalityTraits || isHobbies || isSmokingSubstances;
const otherOptionName = t.common.other || "Other";
const otherOptionName = t["Other"] || "Other";
let options = question.extras.options || [];
const isNationalityDropdown =
@ -308,7 +308,7 @@ export function QuestionDropdown({
onChange={(e) => handleOtherTextChange(e.target.value)}
placeholder={
isPersonalityTraits
? (t.questions.writeOtherTraits ?? "Write other options...")
? (t["Write other options..."] ?? "Write other options...")
: isSmokingSubstances
? locale === "fa"
? "خط قرمزهای دیگر را بنویسید..."

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

@ -365,7 +365,7 @@ export default function QuestionNumber({
</div>
{isOutOfRange ? (
<span className="block text-[10px] font-semibold text-[#F2465F]">
{t.common.rangeError ||
{t["The value entered seems incorrect. Please provide a realistic value."] ||
`Please enter a value between ${min} and ${max}`}
</span>
) : null}
@ -416,7 +416,7 @@ export default function QuestionNumber({
/>
{isOutOfRange ? (
<span className="block text-[10px] font-semibold text-[#F2465F]">
{t.common.rangeError ||
{t["The value entered seems incorrect. Please provide a realistic value."] ||
`Please enter a value between ${min} and ${max}`}
</span>
) : null}

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

@ -250,7 +250,11 @@ export function QuestionPhone({
// On first render: if backend is loading or we need an IP fetch, show loading
if (isLoading) return true;
// If there's already a saved value in the initial render, no loading needed
if (initialValue.phoneValue || (initialValue.codeValue && initialValue.codeValue !== defaultCodeValue)) return false;
if (
initialValue.phoneValue ||
(initialValue.codeValue && initialValue.codeValue !== defaultCodeValue)
)
return false;
return needsIpFetch();
});
@ -397,10 +401,11 @@ export function QuestionPhone({
}
// If user already has a saved value from backend, use it — no IP check needed
const hasSavedValue = value && (
(isMarriagePhoneFieldValue(value) && (value.countryCode || value.phoneNumber)) ||
(typeof value === "string" && value.trim().length > 0)
);
const hasSavedValue =
value &&
((isMarriagePhoneFieldValue(value) &&
(value.countryCode || value.phoneNumber)) ||
(typeof value === "string" && value.trim().length > 0));
if (hasSavedValue) {
hasFetchedIpRef.current = true;
@ -572,14 +577,20 @@ export function QuestionPhone({
/>
</svg>
</button>
<span aria-hidden="true" className="h-5 w-px bg-[#181818]/35 ml-1" />
<span
aria-hidden="true"
className="h-5 w-px bg-[#181818]/35 ml-1"
/>
</div>
<span className="flex min-w-0 flex-1 items-center pr-4">
<input
type="tel"
inputMode="tel"
disabled={disabled}
placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")}
placeholder={question.extras.placeHolder?.replace(
/^\+\d+\s*/,
"",
)}
value={phoneValue}
maxLength={getMaxLengthForCountry(codeValue)}
onChange={(event) => {
@ -587,7 +598,9 @@ export function QuestionPhone({
if (typeof window !== "undefined") {
localStorage.setItem("hasCheckedGeoIPPhone", "true");
}
const nextPhoneValue = sanitizePhoneNumber(event.target.value);
const nextPhoneValue = sanitizePhoneNumber(
event.target.value,
);
const maxLen = getMaxLengthForCountry(codeValue);
const truncatedPhone = nextPhoneValue.slice(0, maxLen);

8
src/components/Componentes/question-section-flow.tsx

@ -164,9 +164,7 @@ function SectionFlowContent({
const activeQuestion = questions?.[activeQuestionIndex];
const showNotice = activeQuestion?.showGuardianNotice;
const isUnder27 =
age !== null
? isFemale && age < 27
: (activeQuestion?.required ?? false);
age !== null ? isFemale && age < 27 : (activeQuestion?.required ?? false);
return (
<>
@ -174,8 +172,8 @@ function SectionFlowContent({
<div className="w-full px-[17px] mb-6 shrink-0">
<NoticeBox>
{isUnder27
? t.questions.guardianNoticeUnder27
: t.questions.guardianNoticeOver27}
? t["To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence."]
: t["Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."]}
</NoticeBox>
</div>
)}

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

@ -141,7 +141,7 @@ export function QuestionSlider({
{/* First Slider (From Age) */}
<div className="flex flex-col gap-2 pt-2">
<div className="flex justify-between items-center text-[14px] font-bold text-[#181818]">
<span>{t.questions.fromAge ?? "From"}</span>
<span>{t["From"] ?? "From"}</span>
<span className="rounded-[8px] bg-[#FCE7EA] text-[#F2465F] px-2.5 py-1 text-[13px] font-bold">
{fromVal}
</span>
@ -182,7 +182,7 @@ export function QuestionSlider({
{/* Second Slider (To Age) */}
<div className="flex flex-col gap-2 pt-2">
<div className="flex justify-between items-center text-[14px] font-bold text-[#181818]">
<span>{t.questions.toAge ?? "To"}</span>
<span>{t["To"] ?? "To"}</span>
<span className="rounded-[8px] bg-[#FCE7EA] text-[#F2465F] px-2.5 py-1 text-[13px] font-bold">
{toVal}
</span>

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

@ -133,7 +133,7 @@ export default function QuestionText({
className="h-[18px] w-[18px] shrink-0 rounded border-[#D0D5DD] text-[#F2465F] focus:ring-[#F2465F] accent-[#F2465F] cursor-pointer"
/>
<span className="text-[13.5px] font-semibold text-[#4F4F4F]">
{t.questions.notAPriority ?? "This is not a priority for me"}
{t["This is not a priority for me"] ?? "This is not a priority for me"}
</span>
</label>
</div>

16
src/components/Componentes/question-title.tsx

@ -5,6 +5,7 @@ import { IoEyeOff } from "react-icons/io5";
import HelpModal from "./help-modal";
import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
import pathMap from "@/translations/path_to_english.json";
type QuestionTitleProps = {
question: QuestionField;
@ -39,7 +40,7 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
className="shrink-0 text-[15px] text-[#D9383A]"
/>
<span>
{t.questions.privateFieldNotice ?? "Private (Advisors Only)"}
{t["Private (Advisors Only)"] ?? "Private (Advisors Only)"}
</span>
</span>
) : null}
@ -77,14 +78,11 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
<HelpModal
isOpen={isHelpOpen}
onClose={() => setIsHelpOpen(false)}
description={
(
t.questions as unknown as Record<
string,
string | undefined
>
)[question.tooltip] || question.tooltip
}
description={(() => {
const fullPath = "questions." + question.tooltip;
const englishVal = (pathMap as Record<string, string>)[fullPath];
return englishVal ? (t as any)[englishVal] || englishVal : question.tooltip;
})()}
/>
</>
) : null}

8
src/components/Componentes/required-steps-card.tsx

@ -136,20 +136,20 @@ export default function RequiredStepsCard({
)}
</span>
<h2 className="group-16 leading-none font-bold tracking-[-0.02em]">
{t.questions.requiredSteps}
{t["Required Steps"]}
</h2>
</div>
<p className="mt-2.5 max-w-[220px] group-12 leading-[1.25] font-medium text-white/80">
{isCompleted
? t.questions.requiredStepsDescriptionCompleted
: t.questions.requiredStepsDescription}
? t["You can now submit your request so we can start finding the right match for you"]
: t["Please complete the required information so we can find suitable matches for you"]}
</p>
</div>
<div
role="img"
aria-label={t.questions.requiredStepsProgress
aria-label={t["{completed} of {total} required steps completed"]
.replace("{completed}", String(completed))
.replace("{total}", String(total))}
className="relative flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-full"

7
src/components/Componentes/slider-slide-four.tsx

@ -27,7 +27,12 @@ export function SliderSlideFour({
<div className="min-h-0 flex-1 overflow-y-auto pb-28">
<SliderHeader index={index} title="Registration Type" />
<NoticeBox className="mt-8" align="justify">
You can register both for yourself and for your loved ones, but obtaining the explicit consent and permission of the person you are creating the profile for is mandatory. To ensure accuracy, it is best to let the individual complete the subsequent steps themselves. Remember that all matches are introduced with absolute privacy and strictly based on the information you provide.
You can register both for yourself and for your loved ones, but
obtaining the explicit consent and permission of the person you are
creating the profile for is mandatory. To ensure accuracy, it is best
to let the individual complete the subsequent steps themselves.
Remember that all matches are introduced with absolute privacy and
strictly based on the information you provide.
</NoticeBox>
<div className="flex min-h-[calc(100dvh-520px)] items-center justify-center pt-6">
<div className="w-full max-w-[440px] space-y-4">

18
src/components/Componentes/subscription-required-sheet.tsx

@ -39,27 +39,27 @@ export function SubscriptionRequiredSheet({
}
onClose={onClose}
buttons={({ close }) => (
<div className="grid w-full grid-cols-[1fr_2fr] gap-3">
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={close}
>
<div className="inline-flex w-full items-center justify-center rounded-[18px] border border-[#9A9A9A] bg-[#F7F7F7] px-4 py-[18px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90">
Back
<div className="inline-flex w-full items-center justify-center rounded-[18px] border border-[#9A9A9A] bg-[#F7F7F7] px-2 h-[52px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">Back</span>
</div>
</button>
<button
type="button"
disabled={isPaymentPending}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70"
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={onPayment}
>
<div className="inline-flex w-full items-center justify-center gap-3 rounded-[18px] bg-[#F0445B] px-4 py-[16px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90">
<span>Payment</span>
<div className="inline-flex w-full items-center justify-center gap-3 rounded-[18px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">Payment</span>
<span className="inline-flex items-center gap-1 rounded-full bg-[#E43B51] p-1.5 text-xs font-semibold leading-none text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.12)]">
<span className="inline-flex items-center gap-1 rounded-full bg-[#E43B51] p-1.5 text-xs font-semibold leading-none text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.12)] shrink-0 min-w-0 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
@ -68,7 +68,7 @@ export function SubscriptionRequiredSheet({
height={18}
className="shrink-0"
/>
<span>50 Coins</span>
<span className="truncate">50</span>
</span>
</div>
</button>

13
src/components/Componentes/support-sheet.tsx

@ -124,17 +124,20 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) {
<div className="flex flex-col items-center w-full gap-[11px]">
{/* Support Icon */}
<div className="flex h-[38px] w-[38px] items-center justify-center">
<FiHeadphones className="size-[27px] shrink-0 text-[#21C17D]" strokeWidth={2} />
<FiHeadphones
className="size-[27px] shrink-0 text-[#21C17D]"
strokeWidth={2}
/>
</div>
{/* Text Content */}
<div className="flex flex-col items-start gap-[14px] w-full">
<h3 className="w-full font-bold text-[16px] leading-[20px] text-[#111111] text-center">
{t.common.supportTitle}
{t["Contact Support"]}
</h3>
<p className="w-full font-normal text-[14px] leading-[20px] text-[#747474] text-center">
{t.common.supportDescription}
{t["If you encounter any issues, please feel free to contact our support specialists in WhatsApp"]}
</p>
</div>
</div>
@ -147,7 +150,7 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) {
onClick={closeSheet}
className="flex-1 h-[52px] rounded-[11px] border border-[#747474] bg-transparent text-[#747474] font-semibold text-[16px] flex items-center justify-center cursor-pointer transition-transform active:scale-[0.98]"
>
{t.common.cancel}
{t["Cancel"]}
</button>
{/* Contact WhatsApp Button */}
@ -163,7 +166,7 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) {
height={21}
className="shrink-0 brightness-0 invert"
/>
{t.common.supportSwipeText}
{t["Contact"]}
</button>
</div>
</section>

2
src/components/Componentes/swipe-button.tsx

@ -128,7 +128,7 @@ export function SwipeButton({
return (
<div
ref={containerRef}
className={`relative flex h-[56px] w-full items-center justify-center rounded-full ${containerBg} p-1 select-none overflow-hidden ${
className={`relative flex h-[56px] w-full items-center justify-center rounded-[15px] ${containerBg} p-1 select-none overflow-hidden ${
disabled ? "opacity-60 cursor-not-allowed" : "cursor-pointer"
}`}
onTouchStart={onTouchStart}

2
src/components/Componentes/test-intro-page.tsx

@ -84,7 +84,7 @@ export default function TestIntroPage({
style={{ paddingBottom: "calc(24px + var(--safe-bottom))" }}
>
<Button
className="rounded-[14px] py-[16px] font-bold text-[16px] !bg-linear-to-r !from-[#F2465F] !to-[#E03950] !text-white shadow-[0_12px_28px_rgba(242,70,95,0.38)]"
className="rounded-[14px] font-bold text-[16px] !bg-linear-to-r !from-[#F2465F] !to-[#E03950] !text-white shadow-[0_12px_28px_rgba(242,70,95,0.38)]"
onClick={onStart}
>
{startLabel}

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

@ -56,7 +56,11 @@ export function TokenSwitcher({
setClientCookie(TOKEN_COOKIE_NAME, newToken);
setClientCookie("habib_token", newToken);
try {
sessionStorage.setItem(TOKEN_COOKIE_NAME, newToken);
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
(window as any).HABIB_TOKEN = newToken;
setCurrentToken(newToken);
setIsOpen(false);
@ -113,7 +117,11 @@ export function TokenSwitcher({
setClientCookie(TOKEN_COOKIE_NAME, targetToken);
setClientCookie("habib_token", targetToken);
if (typeof window !== "undefined") {
try {
sessionStorage.setItem(TOKEN_COOKIE_NAME, targetToken);
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
(window as any).HABIB_TOKEN = targetToken;
setCurrentToken(targetToken);
}

4
src/data/question-data.ts

@ -149,9 +149,9 @@ export function getQuestionListItems(locale: Locale = defaultLocale) {
const dict = getDictionary(locale);
const mergedTitle =
dict.questions.familyMaritalTitle ||
(dict as any)["Family Background, Marital Status, and Children"] ||
"Family Background, Marital Status, and Children";
const mergedEstimate = dict.questions.familyMaritalEstimate || "20 minutes";
const mergedEstimate = (dict as any)["20 minutes"] || "20 minutes";
const fbQuestions = fbItem.questions.map((q, idx) => ({
...q,

7
src/hooks/marriage/types.ts

@ -67,6 +67,7 @@ export type MarriageActiveSubscription = {
total_usages: number;
is_active: boolean;
is_valid: boolean;
remaining_days?: number;
};
export type MarriageActiveCase = {
@ -110,6 +111,12 @@ export type MarriageProfile = {
match_summary: MarriageMatchSummary | null;
intro_video_url?: string | null;
intro_video_thumbnail_url?: string | null;
unseen_rejection?: {
id: number;
reason_code: string;
custom_note?: string;
created_at: string;
} | null;
};
export type MarriageProfileResponse = MarriageProfile;

29
src/hooks/marriage/use-rejection-seen.ts

@ -0,0 +1,29 @@
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { http } from "@/lib/http";
import { marriageQueryKeys } from "./query-keys";
import type { UseMutationOptions } from "@tanstack/react-query";
export interface RejectionSeenPayload {
rejection_id: number;
}
export function useRejectionSeenMutation(
options?: UseMutationOptions<void, Error, RejectionSeenPayload>,
) {
const queryClient = useQueryClient();
return useMutation({
...options,
mutationFn: async (payload: RejectionSeenPayload) => {
await http.post("/api/marriage/profile/main/rejection-seen/", payload);
},
onSuccess: (...args) => {
queryClient.invalidateQueries({
queryKey: marriageQueryKeys.profile(),
});
options?.onSuccess?.(...args);
},
});
}

17
src/lib/auth-bridge.ts

@ -125,7 +125,11 @@ class AuthBridge {
}
if (typeof window !== "undefined") {
try {
window.sessionStorage.setItem(REDIRECT_SESSION_KEY, "true");
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
}
this.loginRequested = true;
@ -214,12 +218,19 @@ class AuthBridge {
return null;
}
let sessionToken: string | null = null;
if (typeof window !== "undefined") {
try {
sessionToken = sessionStorage.getItem(TOKEN_COOKIE_NAME);
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
}
const cookieToken =
getClientCookie(TOKEN_COOKIE_NAME) ??
getClientCookie("habib_token") ??
(typeof window !== "undefined"
? sessionStorage.getItem(TOKEN_COOKIE_NAME)
: null);
sessionToken;
// Treat "NO_TOKEN" the same as having no cookie at all — fall through
// to the default token instead of returning null.

3
src/lib/http.ts

@ -87,8 +87,7 @@ http.interceptors.request.use((config) => {
config.headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
config.headers["Pragma"] = "no-cache";
const stripNoToken = (v: string | null) =>
v && v !== "NO_TOKEN" ? v : null;
const stripNoToken = (v: string | null) => (v && v !== "NO_TOKEN" ? v : null);
const token =
authBridge.getToken() ??

1009
src/translations/locales/ar.json
File diff suppressed because it is too large
View File

1009
src/translations/locales/az.json
File diff suppressed because it is too large
View File

1009
src/translations/locales/bn.json
File diff suppressed because it is too large
View File

1007
src/translations/locales/da.json
File diff suppressed because it is too large
View File

1007
src/translations/locales/de.json
File diff suppressed because it is too large
View File

1011
src/translations/locales/en.json
File diff suppressed because it is too large
View File

1007
src/translations/locales/es.json
File diff suppressed because it is too large
View File

1011
src/translations/locales/fa.json
File diff suppressed because it is too large
View File

1007
src/translations/locales/fr.json
File diff suppressed because it is too large
View File

1007
src/translations/locales/gu.json
File diff suppressed because it is too large
View File

1007
src/translations/locales/ha.json
File diff suppressed because it is too large
View File

4
src/translations/locales/he.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/hi.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/id.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/ks.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/pt.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/ru.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/sw.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/tg.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/tr.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/ul.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/ur.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/uz.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

4
src/translations/locales/zh.json

@ -263,5 +263,9 @@
"next": "Next",
"back": "Back",
"accept": "Accept"
},
"rejectionNotice": {
"title": "Your request was rejected",
"message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
}

248
src/translations/path_to_english.json

@ -0,0 +1,248 @@
{
"common.appName": "Habib Marriage",
"common.submit": "Submit",
"common.decline": "Decline",
"common.continue": "Continue",
"common.cancel": "Cancel",
"common.confirm": "Confirm",
"common.support": "Support",
"common.back": "Back",
"common.required": "Required",
"common.estimateTime": "Estimate time",
"common.rangeError": "The value entered seems incorrect. Please provide a realistic value.",
"common.help": "Help",
"common.gotIt": "Got it",
"common.helpDescription": "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance.",
"common.finalMatchIntroduce": "Final Match Introduction",
"common.confirmFinalMatch": "Confirm Final Match",
"common.confirmFinalMatchQuestion": "Are you sure you want to officially introduce these two candidates to each other?",
"common.page": "Page",
"common.totalPages": "Total Pages",
"common.nextPage": "Next Page",
"common.previousPage": "Previous Page",
"common.itemsPerPage": "Items Per Page",
"common.other": "Other",
"common.consultation": "Consultation",
"common.supportTitle": "Contact Support",
"common.supportDescription": "If you encounter any issues, please feel free to contact our support specialists in WhatsApp",
"common.supportSwipeText": "Contact",
"common.subscription": "Subscription",
"common.subscriptionActive": "Your subscription is active",
"common.subscriptionActiveDesc": "You have active access to view candidates.",
"common.subscriptionNoActiveTitle": "No Active Subscription",
"common.subscriptionNoActiveDesc": "You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.",
"common.subscriptionStatusTitle": "Subscription Status",
"common.subscriptionStatusDesc": "{days} days remaining of your subscription.",
"common.subscriptionRenewButton": "Renew Subscription",
"common.subscriptionRenewPending": "Renewing...",
"intro.imageAlt": "heavenly marriage",
"intro.title": "A Path to Heavenly Marriage",
"intro.description": "We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims",
"intro.userProfile": "user profiles",
"intro.matches": "matches",
"intro.marriage": "marriages",
"intro.videoAlt": "video",
"intro.playAlt": "play",
"questions.privateFieldNotice": "Private (Advisors Only)",
"questions.profileRegistration": "Profile registration",
"questions.closeQuestionsList": "Close questions list",
"questions.requiredSteps": "Required Steps",
"questions.requiredStepsDescription": "Please complete the required information so we can find suitable matches for you",
"questions.requiredStepsDescriptionCompleted": "You can now submit your request so we can start finding the right match for you",
"questions.requiredStepsProgress": "{completed} of {total} required steps completed",
"questions.findMatches": "Find Matches",
"questions.findingMatch": "Submit",
"questions.optionalInfoPromptTitle": "Important Note",
"questions.optionalInfoPromptDescription": "You've completed all required fields. However, filling in all sections will help us find better matches for you",
"questions.completeNecessaryForms": "(Complete Required Forms)",
"questions.openQuestion": "Open {title}",
"questions.answerAtYourOwnPace": "Answer at Your Own Pace",
"questions.answerAtYourOwnPaceDescription": "You can pause the survey anytime and resume later. Your progress is saved automatically.",
"questions.testIntroDisclaimer": "All provided information is held in strict confidence and will not be disclosed to third parties. This data is utilized exclusively to ensure optimal matchmaking accuracy. Consequently, it is imperative that this assessment be completed with the utmost diligence, precision, and integrity.",
"questions.testIntroStart": "Start",
"questions.testIntroEstimateLabel": "Estimate time",
"questions.testIntroBullets.personality.0": "Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?",
"questions.testIntroBullets.personality.1": "Taking this test is not mandatory, but it will help you better search for a spouse. The Personality test is a test for self-knowledge and better understanding of your spouse.",
"questions.testIntroBullets.personality.2": "Do you operate based on superficial behavioral adaptations, or are you aware of the deep \"source traits\" that fundamentally control your decision-making processes?",
"questions.testIntroBullets.glasser.0": "Understanding your five basic needs helps you recognize what truly drives your behavior in relationships.",
"questions.testIntroBullets.glasser.1": "Taking this test is not mandatory, but it will help you better understand your priorities and find a more compatible spouse.",
"questions.testIntroBullets.glasser.2": "By identifying your strongest needs, you can better communicate your expectations and build healthier relationships.",
"questions.startMatchFailed": "Sending the match request failed. Please check your connection and try again.",
"questions.moveToEnd": "Move to the End",
"questions.guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"questions.guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"questions.familyMaritalTitle": "Family Background, Marital Status, and Children",
"questions.familyMaritalEstimate": "5 minutes",
"questions.notAPriority": "This is not a priority for me",
"questions.writeOtherTraits": "Write other options...",
"questions.fromAge": "From",
"questions.toAge": "To",
"questions.familyResponsibilityTooltip": "Please briefly explain the type of responsibility, its duration, the extent of financial or caregiving support, and its potential impact on your place of residence, relocation, or future married life conditions.",
"questions.childCustodyExplanationTooltip": "Please briefly explain the custody status, the child's visitation or presence schedule, potential restrictions on relocation or immigration, and related financial obligations. Avoid including the child's name, the other parent's name, or unnecessary personal details.",
"questions.currentMaritalStatusTooltip": "This private field requires the user to accurately declare their current marital status and relationship history from the specific options provided.",
"match.title": "New Match",
"match.goBack": "Go back",
"match.loadingProfile": "Loading match profile...",
"match.acceptProfile": "Accept Profile",
"match.requestProceedTitle": "Request to Proceed",
"match.requestProceedDescription": "Upon your approval, your request will be sent to the lady, and we must await her response. This process may take between 2 to 4 days. Thank you for your patience.",
"match.acceptDescription": "Are you sure you've fully reviewed the profile and are ready to proceed?",
"match.twoColumnComparison": "Two-Column Side-by-Side Match Comparison",
"match.horizontalAlignment": "Strict Horizontal Field Alignment",
"match.sourceCandidate": "Source Candidate (Right Column)",
"match.targetCandidate": "Opposite Sex Candidate (Left Column)",
"match.tabs.identity": "Identity & Demographics",
"match.tabs.bio": "Bio & Expectations",
"match.tabs.sections": "Form Sections Data",
"match.tabs.tests": "Psychological Assessments",
"match.fields.birthYear": "Year of birth",
"match.fields.nationality": "Nationality",
"match.fields.residence": "City and country of residence",
"match.fields.futureResidence": "City and country of future residence",
"match.fields.religion": "Religion",
"match.fields.countryCity": "Country / city",
"match.fields.currentlyLivingIn": "(currently living in)",
"match.fields.education": "Education",
"match.fields.occupation": "Occupation",
"match.values.iranian": "Iranian",
"match.values.iran": "Iran",
"match.values.tehran": "Tehran",
"match.values.muslim": "Muslim",
"match.values.education": "Bachelor's degree in architecture",
"match.values.occupation": "Interior designer",
"match.viewMoreDetails": "View more details",
"match.newMatchTitleFemale": "New Marriage Proposal",
"match.newMatchTitleMale": "YOU HAVE A NEW MATCH!",
"match.newMatchDescriptionFemale": "A suitable match has been found for you. If approved, your profile will be evaluated to proceed with the introduction process.",
"match.newMatchDescriptionMale": "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information.",
"match.femaleConsentTitle": "Final Consent",
"match.femaleConsentDescription": "Approving this profile will **display your contact number** to the gentleman. Ensure **family consensus** before proceeding.",
"match.femaleConsentCheckbox": "I certify that the lady and her family have thoroughly reviewed this profile and grant preliminary consent for further communication.",
"match.femaleConsentCheckboxLabel": "I confirm the family has **reviewed this profile** and **consents to communicate**.",
"match.rejectProfileTitle": "Reject Profile",
"match.rejectProfileDescription": "Are you sure you've fully reviewed the profile and want to reject this profile?",
"match.reject": "Reject",
"requestAccepted.imageAlt": "Request accepted",
"requestAccepted.title": "Request Accepted",
"requestAccepted.description": "You can now view their family's contact details and arrange further steps.",
"requestAccepted.viewContact": "View Contact",
"requestAccepted.penalty": "Please note: Failure to contact within 2 days may result in a penalty",
"requestAccepted.profileLocked": "Profile is locked",
"requestAccepted.lockedDescription": "You can't edit your profile while we're searching for matches",
"requestAccepted.titleFemale": "Request Approved",
"requestAccepted.titleMalePaymentDone": "Contact info released",
"requestAccepted.titleMalePaymentPending": "Request approved!",
"requestAccepted.primaryFemale": "Report no contact",
"requestAccepted.primaryMale": "View profile",
"requestAccepted.secondaryFemale": "Record call result",
"requestAccepted.secondaryMalePaymentDone": "View contact number",
"requestAccepted.secondaryMalePaymentPending": "Pay and get contact",
"requestAccepted.titleContactReleased": "Contact Information Released",
"requestAccepted.titleMaleApproved": "Request Approved!",
"requestAccepted.actionReportNoContact": "Report No Contact",
"requestAccepted.actionViewProfile": "View Profile",
"requestAccepted.actionSubmitCallResult": "Submit Call Result",
"requestAccepted.actionViewContact": "View Contact Details",
"requestAccepted.actionPayAndGetContact": "Pay & Get Contact",
"requestAccepted.contactDetailTitle": "Contact Detail",
"requestAccepted.contactDetailDescription": "Please mention during the call that you were introduced by the Habib Marriage app.",
"requestAccepted.contactNotAvailable": "Contact information is not available yet.",
"requestAccepted.contactWarning": "Please be informed that from the time of this introduction, you have 48 hours (2 days) to contact the person or their respected family to declare your readiness and begin the acquaintance process. At this stage, merely an initial call to announce your presence is sufficient, and planning for further steps (such as an in-person meeting) depends entirely on your subsequent mutual agreements.\n\nSince failing to make contact within the specified time might be considered socially disrespectful, if no action is taken within these 2 days, the introduced match will be removed according to the platform's rules. We also remind you that this issue may lead to restrictions such as delays in future introductions and financial penalties.",
"findingMatch.title": "SEARCH IN PROGRESS",
"findingMatch.description": "Our system is actively looking for compatible partners based on your criteria. This process requires time and patience. We will notify you immediately once a profile is ready for your review.",
"findingMatch.advisorTitle": "Get an advisor",
"findingMatch.advisorDescription": "Not sure what to do next? Our psychology section is here to guide you at every step.",
"findingMatch.getAdvisor": "Get Advisor",
"findingMatch.editProfile": "Edit Profile",
"candidateContact.imageAlt": "Selected candidate contact status",
"candidateContact.title": "The selected candidate will contact your family shortly.",
"candidateContact.contacted": "Confirm Contacted",
"candidateContact.noContactYet": "Report No Contact",
"candidateContact.afterTwoDays": "(after 2 days)",
"candidateContact.contactWarning": "To keep the process moving smoothly, the other party has a 48-hour (2-day) window to make initial contact with you or your family. If no contact is established after 2 days, you have the option to decline his request or notify us that he hasn't reached out.",
"candidateContact.submitOutcome": "Submit Final Outcome",
"candidateContact.outcomeGuide": "Please report the final outcome of the proposal and communication to the system.",
"candidateContact.outcomeTitle": "What was the outcome of your contact?",
"candidateContact.marriageSuccess": "We reached an agreement",
"candidateContact.marriageFailure": "We did not reach an agreement",
"candidateContact.congratsTitle": "Congratulations! 🎉",
"candidateContact.congratsMessage": "Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed.",
"candidateContact.thankYouFeedback": "Thank you for giving us feedback, we would be very happy if you also let us know the final result.",
"sheets.informationSheet": "Information sheet",
"sheets.callResult": "Call result",
"sheets.selectCallResult": "Select call result",
"sheets.callOptions.0": "Not a good personal fit",
"sheets.callOptions.1": "No mutual interest",
"sheets.callOptions.2": "Different expectations",
"sheets.callOptions.3": "No connection felt",
"sheets.callOptions.4": "Location not suitable",
"sheets.callOptions.5": "Other reasons",
"sheets.dismissReasons": "Dismiss reasons",
"sheets.dismissDescription": "Please provide the full reason for rejecting the submitted item",
"sheets.dismissPlaceholder": "Feel free to briefly explain your decision...",
"requestSent.title": "Request Sent",
"requestSent.description": "Your request has been sent. Once the lady reviews your request, you will be notified.",
"requestSent.matchProfile": "View More Details",
"requestSent.profileLocked": "Profile is locked",
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors.",
"paymentModal.title": "Verification & Subscription Activation",
"paymentModal.verificationText": "This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins.",
"paymentModal.disclaimerText": "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users.",
"paymentModal.swipeToPay": "Swipe to pay 50 Habib Coins",
"paymentModal.insufficientCoins": "Insufficient coin balance. Please recharge your account.",
"paymentModal.activeFor3Months": "Valid for 3 months",
"paymentModal.cost": "50 Coins",
"paymentModal.close": "Exit",
"paymentModal.payment": "Payment",
"paymentModal.pay": "Pay",
"paymentModal.paymentSuccess": "Payment successful",
"maleRejectionWarning.title": "Rejection Warning",
"maleRejectionWarning.carefulReview": "Before making a final decision, please carefully review the other person's profile again completely to make an informed choice.",
"maleRejectionWarning.friendlyDelay": "Please note that rejecting this case might cause a delay in recommending the next match, but there is absolutely no obligation to accept and you are fully free to choose.",
"maleRejectionWarning.noPenalty": "Confirming this rejection will not result in any penalty. Instead, it simply enters your status into a 2-day decision window to finalize the case.",
"maleRejectionWarning.swipeText": "Swipe to confirm rejection",
"onboarding.submitProcess": "Submit Process",
"onboarding.termsAndConditions": "terms & conditions",
"onboarding.welcomeWarning": "Dear user, while welcoming you to the Habib Marij application, please read the following rules and regulations carefully before using the services of this platform. Your registration and use of this application constitutes full acceptance of these rules.",
"onboarding.eligibilityTitle": "1. Eligibility and Membership Requirements",
"onboarding.legalAgeLabel": "Legal Age:",
"onboarding.legalAgeValue": "Users must meet the minimum legal age for independent registration.",
"onboarding.identityVerificationLabel": "Identity Verification:",
"onboarding.identityVerificationValue": "Mandatory submission of valid government-issued ID upon registration.",
"onboarding.singleStatusLabel": "Single Status Commitment:",
"onboarding.singleStatusValue": "Users must provide proof of being single, or documents confirming divorce or spouse's death upon request.",
"onboarding.intentLabel": "Intent:",
"onboarding.intentValue": "Commitment to monogamy and intention for permanent marriage only (no simultaneous or temporary relationships).",
"onboarding.healthLabel": "General Health:",
"onboarding.healthValue": "Self-declaration regarding mental health, absence of addiction, and no criminal record.",
"onboarding.privacyTitle": "2. Privacy and Data Management",
"onboarding.contentSecurityLabel": "Content Security:",
"onboarding.contentSecurityValue": "Technical prevention of screenshots from profiles and chat environments.",
"onboarding.progressiveDisclosureLabel": "Progressive Disclosure:",
"onboarding.progressiveDisclosureValue": "Sensitive information (face, contact details) revealed step-by-step only with mutual consent.",
"onboarding.watchVideo": "Watch Video",
"onboarding.videoSpeaker": "Dr. Hasti Masoudi",
"onboarding.videoDescription": "We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims",
"onboarding.selectGender": "Select Gender",
"onboarding.submitMan": "Submit Man",
"onboarding.submitWoman": "Submit Woman",
"onboarding.registrationType": "Registration Type",
"onboarding.readTermsNotice": "Read the terms and conditions carefully so that you don't run into any problems in the next process - reading this section is mandatory.",
"onboarding.registerForSelf": "Registering for myself",
"onboarding.registerForOther": "Registering for someone else",
"onboarding.finalNotice": "Final Notice",
"onboarding.finalNoticeDesc": "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.",
"onboarding.noticeItem1": "Your information is kept strictly confidential.",
"onboarding.noticeItem2": "Your details are only used for the matching process.",
"onboarding.noticeItem3": "Nothing is shared without your consent.",
"onboarding.noticeItem4": "You are always in control of what happens next.",
"onboarding.noticeItem5": "Contact details are shared only after your approval.",
"onboarding.noticeItem6": "We provide a safe and respectful environment at every step.",
"onboarding.closeSlider": "Close slider",
"onboarding.finish": "Finish",
"onboarding.next": "Next",
"onboarding.back": "Back",
"onboarding.accept": "Accept",
"rejectionNotice.title": "Your request was rejected",
"rejectionNotice.message": "Your request was rejected by the lady. You will be introduced to other candidates in the future."
}
Loading…
Cancel
Save