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. 86
      src/app/finding-match/page.tsx
  4. 29
      src/app/globals.css
  5. 67
      src/app/intro/page.tsx
  6. 28
      src/app/layout.tsx
  7. 260
      src/app/new-match/page.tsx
  8. 109
      src/app/new-match/profile/page.tsx
  9. 13
      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. 119
      src/app/questions-list/page.tsx
  13. 19
      src/app/questions-list/sections-request.tsx
  14. 48
      src/app/request-accepted/page.tsx
  15. 17
      src/app/request-sent/page.tsx
  16. 17
      src/components/Componentes/button.tsx
  17. 29
      src/components/Componentes/call-result-sheet.tsx
  18. 210
      src/components/Componentes/dismiss-reason-sheet.tsx
  19. 25
      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. 88
      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. 12
      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. 19
      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. 1009
      src/translations/locales/da.json
  54. 1009
      src/translations/locales/de.json
  55. 1013
      src/translations/locales/en.json
  56. 1009
      src/translations/locales/es.json
  57. 1013
      src/translations/locales/fa.json
  58. 1009
      src/translations/locales/fr.json
  59. 1009
      src/translations/locales/gu.json
  60. 1009
      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> </div>
<h1 className="mt-8 text-center text-[24px] leading-[1.25] font-bold text-[#E03950]"> <h1 className="mt-8 text-center text-[24px] leading-[1.25] font-bold text-[#E03950]">
{t.candidateContact.congratsTitle}
{t["Congratulations! 🎉"]}
</h1> </h1>
<p className="mt-6 text-[#4A4A4A] group-14 font-medium leading-relaxed"> <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> </p>
</div> </div>
) : ( ) : (
@ -145,7 +145,7 @@ export default function CandidateContactPage() {
<div className="relative mt-4 flex items-center justify-center"> <div className="relative mt-4 flex items-center justify-center">
<Image <Image
src="/assets/images/Group 159788fd0467.svg" src="/assets/images/Group 159788fd0467.svg"
alt={t.candidateContact.imageAlt}
alt={t["Selected candidate contact status"]}
width={160} width={160}
height={152} height={152}
priority priority
@ -154,7 +154,7 @@ export default function CandidateContactPage() {
{/* Title / Status message */} {/* Title / Status message */}
<h1 className="mt-8 text-center text-[20px] leading-[1.25] font-bold text-[#1A1A1A] max-w-[280px]"> <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> </h1>
{caseStatus === "contacted" ? ( {caseStatus === "contacted" ? (
@ -162,7 +162,7 @@ export default function CandidateContactPage() {
{/* Outcome instructions and button */} {/* 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"> <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"> <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> </p>
</div> </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]" 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>
<button <button
@ -188,7 +188,7 @@ export default function CandidateContactPage() {
{outcomeMutation.isPending ? ( {outcomeMutation.isPending ? (
<DotsLoader className="text-white" /> <DotsLoader className="text-white" />
) : ( ) : (
t.candidateContact.submitOutcome
t["Submit Final Outcome"]
)} )}
</button> </button>
</div> </div>
@ -206,7 +206,7 @@ export default function CandidateContactPage() {
{contactStatusMutation.isPending ? ( {contactStatusMutation.isPending ? (
<DotsLoader className="text-[#8E8E93]" /> <DotsLoader className="text-[#8E8E93]" />
) : ( ) : (
t.candidateContact.noContactYet
t["Report No Contact"]
)} )}
</button> </button>
@ -225,7 +225,7 @@ export default function CandidateContactPage() {
{contactStatusMutation.isPending ? ( {contactStatusMutation.isPending ? (
<DotsLoader className="text-white" /> <DotsLoader className="text-white" />
) : ( ) : (
t.candidateContact.contacted
t["Confirm Contacted"]
)} )}
</button> </button>
</div> </div>
@ -233,7 +233,7 @@ export default function CandidateContactPage() {
{/* Red warning box */} {/* Red warning box */}
<div className="w-full border border-[#F0445B] bg-[#FFF5F6] rounded-[15px] mt-4 px-4 py-3 text-center"> <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"> <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> </p>
</div> </div>
</> </>
@ -245,11 +245,11 @@ export default function CandidateContactPage() {
<div className="space-y-4 pb-20"> <div className="space-y-4 pb-20">
{/* Advisor section */} {/* Advisor section */}
<AdvisorActionsCard <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} avatars={advisorAvatars}
extraCount={7} extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorLabel={t["Get Advisor"]}
getAdvisorHref="/questions-list" getAdvisorHref="/questions-list"
/> />
@ -267,7 +267,7 @@ export default function CandidateContactPage() {
alt="lock" alt="lock"
/> />
<h2 className="group-14 leading-none font-semibold text-[#747474]"> <h2 className="group-14 leading-none font-semibold text-[#747474]">
{t.requestSent.profileLocked}
{t["Profile is locked"]}
</h2> </h2>
</div> </div>
</div> </div>
@ -320,13 +320,13 @@ export default function CandidateContactPage() {
<div className="flex max-w-[300px] flex-col items-center"> <div className="flex max-w-[300px] flex-col items-center">
<Image <Image
src="/assets/images/Group 1597880467.svg" src="/assets/images/Group 1597880467.svg"
alt={t.candidateContact.imageAlt}
alt={t["Selected candidate contact status"]}
width={131} width={131}
height={125} height={125}
/> />
<h1 className="mt-10 text-center text-[18px] leading-[1.2] font-bold text-[#1A1A1A]"> <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> </h1>
</div> </div>
</section> </section>
@ -340,7 +340,7 @@ export default function CandidateContactPage() {
onClick={() => setIsCallResultSheetOpen(true)} 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" 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>
<button <button
type="button" type="button"
@ -351,7 +351,7 @@ export default function CandidateContactPage() {
{contactStatusMutation.isPending ? ( {contactStatusMutation.isPending ? (
<DotsLoader className="text-[#8B8B8B]" /> <DotsLoader className="text-[#8B8B8B]" />
) : ( ) : (
t.candidateContact.noContactYet
t["Report No Contact"]
)} )}
</button> </button>
</section> </section>

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

@ -4,7 +4,7 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useMemo } from "react"; 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 { FaLock, FaPen } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import PageHeader from "@/components/Componentes/page-header"; 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 { getSubmitPath } from "@/lib/get-submit-path";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider"; 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 = [ const advisorAvatars = [
{ id: "advisor-primary", src: "/assets/images/Avatar Image.png" }, { id: "advisor-primary", src: "/assets/images/Avatar Image.png" },
@ -27,6 +30,9 @@ export default function FindingMatchPage() {
refetchInterval: 3000, refetchInterval: 3000,
}); });
const { mutate: markRejectionSeen, isPending: isMarkingSeen } =
useRejectionSeenMutation();
useEffect(() => { useEffect(() => {
if (!profile) { if (!profile) {
return; 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"; const matchImageSrc = "/assets/images/Group 1597880466.svg";
return ( return (
@ -61,7 +74,7 @@ export default function FindingMatchPage() {
<PageBackground /> <PageBackground />
<main <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" className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(16px,calc(var(--safe-top)+8px))] text-center"
> >
<PageHeader <PageHeader
@ -99,28 +112,57 @@ export default function FindingMatchPage() {
getAdvisorHref="/questions-list" getAdvisorHref="/questions-list"
/> />
{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"
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}
</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">
<FixToTheEnd>
{profile?.can_edit_profile === false ? (
<div
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["Profile is locked"]}
</span>
</div>
) : (
<Button variant="dark" href="/questions-list">
<FaPen aria-hidden="true" className="h-3.5 w-3.5 shrink-0" /> <FaPen aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span>{copy.editProfile}</span> <span>{copy.editProfile}</span>
</span>
</Link>
)}
</Button>
)}
</FixToTheEnd>
</main> </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: button-dot-bounce 0.9s cubic-bezier(0.4, 0, 0.6, 1) infinite;
animation-delay: 0.3s; 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%;
}

67
src/app/intro/page.tsx

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

28
src/app/layout.tsx

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

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

@ -10,6 +10,7 @@ import { DotsLoader } from "@/components/Componentes/button";
import PageHeader from "@/components/Componentes/page-header"; import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background"; import { PageBackground } from "@/components/Componentes/page-background";
import { IoClose } from "react-icons/io5"; import { IoClose } from "react-icons/io5";
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
import { useHabcoinPaymentMutation } from "@/hooks/marriage/use-habcoin-payment"; import { useHabcoinPaymentMutation } from "@/hooks/marriage/use-habcoin-payment";
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import type { import type {
@ -276,30 +277,86 @@ export default function NewMatchPage() {
return getSubmitPath(profile) !== "/new-match"; return getSubmitPath(profile) !== "/new-match";
}, [profile]); }, [profile]);
const matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary);
if (isLoading || isRedirecting) { if (isLoading || isRedirecting) {
return ( return (
<> <>
<PageBackground /> <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> </main>
</> </>
); );
} }
const matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary);
const pairedFields = [matchDisplay.age, matchDisplay.city].filter( const pairedFields = [matchDisplay.age, matchDisplay.city].filter(
(field): field is DisplayField => Boolean(field), (field): field is DisplayField => Boolean(field),
); );
const isFemaleProfile = profile?.gender === "female"; const isFemaleProfile = profile?.gender === "female";
const matchHeadingTitle = isFemaleProfile const matchHeadingTitle = isFemaleProfile
? t.match.newMatchTitleFemale
: t.match.newMatchTitleMale;
? t["New Marriage Proposal"]
: t["YOU HAVE A NEW MATCH!"];
const matchHeadingDescription = isFemaleProfile 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 isMale = profile?.gender === "male";
const hasActiveSub = !!profile?.active_subscription; const hasActiveSub = !!profile?.active_subscription;
@ -339,79 +396,93 @@ export default function NewMatchPage() {
</p> </p>
</section> </section>
<div className="flex h-full flex-col justify-between"> <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 />
</div>
) : isError ? (
<p className="py-8 text-[13px] font-semibold">
Unable to load match summary.
</p>
) : matchSummary ? (
<>
<h2 className="break-words text-[13px] leading-[1.4] font-bold">
<span>Name: </span>
<span>{matchDisplay.name}</span>
</h2>
<div className="mt-[3px] min-h-[48px]">
{matchDisplay.occupation ? (
<FieldLine field={matchDisplay.occupation} />
) : null}
{pairedFields.length ? (
<p className="break-words text-[10px] leading-[1.85] font-medium text-white">
{pairedFields.map((field, index) => (
<span key={field.id}>
{index > 0 ? <span> | </span> : null}
<span>
{field.label}: {field.value}
</span>
</span>
))}
</p>
) : null}
{matchDisplay.maritalStatus ? (
<FieldLine field={matchDisplay.maritalStatus} />
) : null}
{matchDisplay.cityPreference ? (
<FieldLine field={matchDisplay.cityPreference} />
) : null}
{isLoading ? (
<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> </div>
<button
type="button"
onClick={() => {
if (isMale && !hasActiveSub) {
setIsPaymentSheetOpen(true);
} else {
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"
>
{t.match.viewMoreDetails}
</button>
</>
) : (
<p className="py-8 text-[13px] font-semibold">
No match summary is available yet.
</p>
)}
</section>
{/* 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>
) : matchSummary ? (
<>
<h2 className="break-words text-[13px] leading-[1.4] font-bold">
<span>Name: </span>
<span>{matchDisplay.name}</span>
</h2>
<div className="mt-[3px] min-h-[48px]">
{matchDisplay.occupation ? (
<FieldLine field={matchDisplay.occupation} />
) : null}
{pairedFields.length ? (
<p className="break-words text-[10px] leading-[1.85] font-medium text-white">
{pairedFields.map((field, index) => (
<span key={field.id}>
{index > 0 ? <span> | </span> : null}
<span>
{field.label}: {field.value}
</span>
</span>
))}
</p>
) : null}
{matchDisplay.maritalStatus ? (
<FieldLine field={matchDisplay.maritalStatus} />
) : null}
{matchDisplay.cityPreference ? (
<FieldLine field={matchDisplay.cityPreference} />
) : null}
</div>
<button
type="button"
onClick={() => {
if (isMale && !hasActiveSub) {
setIsPaymentSheetOpen(true);
} else {
router.push(localizePath("/new-match/profile", locale));
}
}}
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["View more details"]}
</button>
</>
) : (
<p className="py-8 text-[13px] font-semibold">
No match summary is available yet.
</p>
)}
</section>
)}
<div className="w-full space-y-3"> <div className="w-full space-y-3">
<AdvisorActionsCard <AdvisorActionsCard
className="mt-[36px]" 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} avatars={advisorAvatars}
extraCount={7} extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorLabel={t["Get Advisor"]}
getAdvisorHref="/questions-list" getAdvisorHref="/questions-list"
/> />
</div> </div>
</div> </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" 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 <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" role="status"
> >
<FaLock aria-hidden="true" className="h-3.5 w-3.5 shrink-0" /> <FaLock aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span className="group-16 leading-none font-semibold"> <span className="group-16 leading-none font-semibold">
{t.requestSent.profileLocked}
{t["Profile is locked"]}
</span> </span>
</div> </div>
</div> </div>
@ -463,24 +534,27 @@ export default function NewMatchPage() {
</div> </div>
<h3 className="group-16 font-bold text-gray-900 mb-2"> <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> </h3>
<p className="text-xs text-gray-500 leading-relaxed mb-4 text-center"> <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> </p>
<div className="w-full bg-gray-50 rounded-xl p-3 mb-4 text-center"> <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"> <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>
<span className="text-base font-bold text-[#FF4E67]"> <span className="text-base font-bold text-[#FF4E67]">
{t.paymentModal?.cost || "50 Habib Coins"}
{t["50 Coins"] || "50 Habib Coins"}
</span> </span>
</div> </div>
<p className="text-[10px] text-gray-400 leading-normal mb-6 text-center"> <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> </p>
{paymentError && ( {paymentError && (
@ -489,28 +563,36 @@ export default function NewMatchPage() {
</div> </div>
)} )}
<div className="grid w-full grid-cols-[1fr_2fr] gap-3">
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
<button <button
type="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" className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={handleDecline} 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> </div>
</button> </button>
<button <button
type="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" className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={handlePayment} 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 <Image
src="/assets/images/Inner Plugdsain Iframe.svg" src="/assets/images/Inner Plugdsain Iframe.svg"
alt="" alt=""
@ -519,7 +601,7 @@ export default function NewMatchPage() {
height={18} height={18}
className="shrink-0" className="shrink-0"
/> />
<span className="truncate">{t.paymentModal?.cost || "50 Coins"}</span>
<span className="truncate">50</span>
</span> </span>
</div> </div>
</button> </button>

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

@ -3,10 +3,11 @@
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; 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 DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet"; import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet";
import InformationSheet from "@/components/Componentes/information-sheet"; import InformationSheet from "@/components/Componentes/information-sheet";
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
import NavigationButton from "@/components/Componentes/navigation-button"; import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background"; import { PageBackground } from "@/components/Componentes/page-background";
import StickyHeader from "@/components/Componentes/sticky-header"; import StickyHeader from "@/components/Componentes/sticky-header";
@ -187,49 +188,49 @@ function NewMatchProfileSkeleton() {
return ( return (
<> <>
<PageBackground /> <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> <StickyHeader>
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<NavigationButton <NavigationButton
variant="transparent" variant="transparent"
icon="close" 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"> <h1 className="min-w-0 flex-1 text-center group-16 font-bold text-white">
{t.match.title}
{t["New Match"]}
</h1> </h1>
<div className="size-10 shrink-0" /> <div className="size-10 shrink-0" />
</div> </div>
</StickyHeader> </StickyHeader>
<section className="px-[17px] pb-32 pt-5"> <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>
<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-4 pt-1">
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> <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>
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> <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>
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> <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>
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> <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> </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="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="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> </div>
</div> </div>
@ -306,7 +307,6 @@ export default function NewMatchProfilePage() {
const caseId = profile?.active_case?.case_id; const caseId = profile?.active_case?.case_id;
const caseStatus = profile?.active_case?.status; const caseStatus = profile?.active_case?.status;
const isFemaleProfile = profile?.gender === "female"; const isFemaleProfile = profile?.gender === "female";
const isMaleAccepted = caseStatus === "male_accepted";
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", {
onSuccess: async (_, variables) => { onSuccess: async (_, variables) => {
if (variables.action === "accept") { if (variables.action === "accept") {
@ -364,11 +364,11 @@ export default function NewMatchProfilePage() {
} }
const isSubmitting = respondMutation.isPending; const isSubmitting = respondMutation.isPending;
const isAcceptProfileEnabled = isFemaleProfile
? !isSubmitting
: Boolean(caseId) &&
!isSubmitting &&
canAcceptProfile(profile?.gender, caseStatus);
const isAcceptProfileEnabled =
Boolean(caseId) &&
!isSubmitting &&
canAcceptProfile(profile?.gender, caseStatus);
const isRejectProfileEnabled = isAcceptProfileEnabled;
const nameParts = candidateName.trim().split(/\s+/); const nameParts = candidateName.trim().split(/\s+/);
const firstName = nameParts[0] || ""; 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 mainClass = "-mx-[17px] flex min-h-screen flex-col pb-10";
const mainStyle = { 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", backgroundColor: "#F5F5F5",
backgroundRepeat: "repeat-y", backgroundRepeat: "repeat-y",
backgroundSize: "100% auto, 100% 100%",
backgroundSize: "100% auto",
}; };
return ( return (
@ -389,8 +389,8 @@ export default function NewMatchProfilePage() {
{isRequestSheetOpen ? ( {isRequestSheetOpen ? (
isFemaleProfile ? ( isFemaleProfile ? (
<FemaleConsentSheet <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 }) => ( buttons={({ close }) => (
<div className="space-y-5"> <div className="space-y-5">
<button <button
@ -412,7 +412,7 @@ export default function NewMatchProfilePage() {
) : null} ) : null}
</span> </span>
<span className="text-xs leading-[1.35] text-[#3F3F3F]"> <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> </span>
</button> </button>
@ -422,7 +422,7 @@ export default function NewMatchProfilePage() {
className="py-[18px] text-[18px]" className="py-[18px] text-[18px]"
onClick={close} onClick={close}
> >
{t.common.cancel}
{t["Cancel"]}
</Button> </Button>
<Button <Button
className="py-[18px] text-[18px]" className="py-[18px] text-[18px]"
@ -441,7 +441,7 @@ export default function NewMatchProfilePage() {
await respondMutation.mutateAsync({ action: "accept" }); await respondMutation.mutateAsync({ action: "accept" });
}} }}
> >
{t.common.confirm}
{t["Confirm"]}
</Button> </Button>
</div> </div>
</div> </div>
@ -454,8 +454,8 @@ export default function NewMatchProfilePage() {
) : ( ) : (
<InformationSheet <InformationSheet
icon="check" 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 }) => ( buttons={({ close }) => (
<div className="grid w-full grid-cols-2 gap-3"> <div className="grid w-full grid-cols-2 gap-3">
<Button <Button
@ -463,7 +463,7 @@ export default function NewMatchProfilePage() {
className="py-[18px]" className="py-[18px]"
onClick={close} onClick={close}
> >
{t.common.cancel}
{t["Cancel"]}
</Button> </Button>
<Button <Button
className="py-[18px]" className="py-[18px]"
@ -480,7 +480,7 @@ export default function NewMatchProfilePage() {
await respondMutation.mutateAsync({ action: "accept" }); await respondMutation.mutateAsync({ action: "accept" });
}} }}
> >
{t.common.confirm}
{t["Confirm"]}
</Button> </Button>
</div> </div>
)} )}
@ -491,21 +491,20 @@ export default function NewMatchProfilePage() {
{isRejectSheetOpen ? ( {isRejectSheetOpen ? (
<InformationSheet <InformationSheet
icon="warning" 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 }) => ( buttons={({ close }) => (
<div className="grid w-full grid-cols-2 gap-3"> <div className="grid w-full grid-cols-2 gap-3">
<Button variant="outlined" className="py-[18px]" onClick={close}> <Button variant="outlined" className="py-[18px]" onClick={close}>
{t.common.cancel}
{t["Cancel"]}
</Button> </Button>
<Button <Button
className="py-[18px]"
onClick={() => { onClick={() => {
close(); close();
setIsDismissReasonSheetOpen(true); setIsDismissReasonSheetOpen(true);
}} }}
> >
Reject
{t["Reject"]}
</Button> </Button>
</div> </div>
)} )}
@ -515,7 +514,7 @@ export default function NewMatchProfilePage() {
{isMaleRejectWarningOpen ? ( {isMaleRejectWarningOpen ? (
<InformationSheet <InformationSheet
icon="warning" icon="warning"
title={t.maleRejectionWarning.title}
title={t["Rejection Warning"]}
description={ description={
<div <div
className="space-y-4 text-right" 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]"> <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> </p>
<div className="flex gap-2.5 items-start p-3 bg-gray-50 rounded-xl border border-gray-100"> <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> <span className="text-lg shrink-0">💡</span>
<p className="text-xs text-[#6B7280] leading-relaxed"> <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> </p>
</div> </div>
<div className="flex gap-2.5 items-start p-3 bg-gray-50 rounded-xl border border-gray-100"> <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> <span className="text-lg shrink-0"></span>
<p className="text-xs text-[#6B7280] leading-relaxed"> <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> </p>
</div> </div>
</div> </div>
@ -549,7 +548,7 @@ export default function NewMatchProfilePage() {
buttons={({ close }) => ( buttons={({ close }) => (
<div className="w-full space-y-3"> <div className="w-full space-y-3">
<SwipeButton <SwipeButton
text={t.maleRejectionWarning.swipeText}
text={t["Swipe to confirm rejection"]}
onSuccess={() => { onSuccess={() => {
close(); close();
setIsDismissReasonSheetOpen(true); setIsDismissReasonSheetOpen(true);
@ -557,10 +556,10 @@ export default function NewMatchProfilePage() {
/> />
<Button <Button
variant="outlined" 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} onClick={close}
> >
{t.common.cancel}
{t["Cancel"]}
</Button> </Button>
</div> </div>
)} )}
@ -589,13 +588,13 @@ export default function NewMatchProfilePage() {
<NavigationButton <NavigationButton
variant="transparent" variant="transparent"
icon="close" icon="close"
iconLabel={t.match.goBack}
iconLabel={t["Go back"]}
/> />
<h1 <h1
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white"
style={{ fontFamily: "'Segoe UI', sans-serif" }} style={{ fontFamily: "'Segoe UI', sans-serif" }}
> >
{t.match.title}
{t["New Match"]}
</h1> </h1>
<div className="size-10 shrink-0" /> <div className="size-10 shrink-0" />
</div> </div>
@ -670,13 +669,13 @@ export default function NewMatchProfilePage() {
onClick={() => router.back()} 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]" 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> </button>
) : ( ) : (
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
type="button" type="button"
disabled={!caseId || isSubmitting || isMaleAccepted}
disabled={!isRejectProfileEnabled}
onClick={() => { onClick={() => {
if (isFemaleProfile) { if (isFemaleProfile) {
setIsRejectSheetOpen(true); setIsRejectSheetOpen(true);
@ -684,9 +683,9 @@ export default function NewMatchProfilePage() {
setIsMaleRejectWarningOpen(true); 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>
<button <button
type="button" type="button"
@ -710,7 +709,7 @@ export default function NewMatchProfilePage() {
width={28} width={28}
height={28} height={28}
/> />
<span>{t.match.acceptProfile}</span>
<span>{t["Accept Profile"]}</span>
</button> </button>
</div> </div>
)} )}

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

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

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

@ -46,11 +46,11 @@ export default async function QuestionDetailPage({
itemSlug={item.slug} itemSlug={item.slug}
locale={locale} locale={locale}
questionsListHref={questionsListHref} 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 = const bulletKey =
item.slug === "glasser_5_needs_test" ? "glasser" : "personality"; 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 ( return (
<> <>
@ -791,10 +799,10 @@ export default function QuestionDetailClient({
<TestIntroPage <TestIntroPage
title={item.title} title={item.title}
estimateTime={item.estimate} estimateTime={item.estimate}
description={t.questions.testIntroEstimateLabel}
description={t["Estimate time"]}
bulletPoints={bullets} 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={() => { onStart={() => {
setIsTestStarted(true); setIsTestStarted(true);
}} }}

119
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 Button from "@/components/Componentes/button";
import InformationSheet from "@/components/Componentes/information-sheet"; import InformationSheet from "@/components/Componentes/information-sheet";
import NavigationButton from "@/components/Componentes/navigation-button"; 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 { PageBackground } from "@/components/Componentes/page-background";
import ErrorToast from "@/components/Componentes/error-toast"; import ErrorToast from "@/components/Componentes/error-toast";
import { import {
@ -40,10 +42,12 @@ export default function QuestionsListPage() {
const { dictionary: t, locale } = useI18n(); const { dictionary: t, locale } = useI18n();
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: profile } = useMarriageProfileQuery();
const { data: sections } = useMarriageSectionsQuery({
refetchOnMount: "always",
});
const { data: profile, isLoading: isProfileLoading } =
useMarriageProfileQuery();
const { data: sections, isLoading: isSectionsLoading } =
useMarriageSectionsQuery({
refetchOnMount: "always",
});
const startMatchMutation = useStartMarriageMatchMutation({ const startMatchMutation = useStartMarriageMatchMutation({
onSuccess: () => { onSuccess: () => {
@ -142,9 +146,9 @@ export default function QuestionsListPage() {
useEffect(() => { useEffect(() => {
if (startMatchMutation.isError || isSyncError) { 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 = () => { const handleCloseToast = () => {
setToastMessage(null); 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 ( return (
<> <>
{toastMessage && ( {toastMessage && (
@ -225,31 +301,31 @@ export default function QuestionsListPage() {
{isOptionalInfoSheetOpen ? ( {isOptionalInfoSheetOpen ? (
<InformationSheet <InformationSheet
icon="warning" icon="warning"
title={t.questions.optionalInfoPromptTitle}
title={t["Important Note"]}
description={ description={
<p className="px-0.5 text-sm leading-[1.45] text-[#2D2D2D]"> <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> </p>
} }
onClose={() => setIsOptionalInfoSheetOpen(false)} onClose={() => setIsOptionalInfoSheetOpen(false)}
buttons={({ close }) => ( 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 <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} onClick={close}
variant="outlined" variant="outlined"
> >
{t.common.cancel}
<span className="truncate">{t["Cancel"]}</span>
</Button> </Button>
<Button <Button
className="rounded-[11px] py-[18px]"
className="rounded-[11px] min-w-0"
disabled={isStartMatchDisabled} disabled={isStartMatchDisabled}
onClick={() => { onClick={() => {
close(); close();
handleStartMatch(); handleStartMatch();
}} }}
> >
{t.questions.findingMatch}
<span className="truncate">{t["Submit"]}</span>
</Button> </Button>
</div> </div>
)} )}
@ -298,7 +374,7 @@ export default function QuestionsListPage() {
> >
<NavigationButton <NavigationButton
icon="back" icon="back"
iconLabel={t.questions.closeQuestionsList}
iconLabel={t["Close questions list"]}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
if (typeof window !== "undefined" && (window as any).HabibApp) { if (typeof window !== "undefined" && (window as any).HabibApp) {
@ -311,11 +387,11 @@ export default function QuestionsListPage() {
}} }}
/> />
<h1 className="group-16 font-semibold text-[#151515]"> <h1 className="group-16 font-semibold text-[#151515]">
{t.questions.profileRegistration}
{t["Profile registration"]}
</h1> </h1>
<NavigationButton <NavigationButton
icon="document" icon="document"
iconLabel={t.common.support}
iconLabel={t["Support"]}
className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]" className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]"
/> />
</header> </header>
@ -340,12 +416,9 @@ export default function QuestionsListPage() {
</section> </section>
</div> </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 <Button
aria-label={t.questions.findMatches}
aria-label={t["Find Matches"]}
disabled={isStartMatchDisabled} disabled={isStartMatchDisabled}
isLoading={startMatchMutation.isPending || isSyncing} isLoading={startMatchMutation.isPending || isSyncing}
onClick={() => { onClick={() => {
@ -367,11 +440,11 @@ export default function QuestionsListPage() {
className="shrink-0" className="shrink-0"
/> />
<span className="leading-none font-semibold"> <span className="leading-none font-semibold">
{t.questions.findingMatch}
{t["Submit"]}
</span> </span>
</span> </span>
</Button> </Button>
</div>
</FixToTheEnd>
</main> </main>
</> </>
); );

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

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

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

@ -226,18 +226,18 @@ export default function RequestAcceptedPage() {
} }
const titleText = isFemaleProfile const titleText = isFemaleProfile
? t.requestAccepted.titleFemale
? t["Request Approved"]
: caseStatus === "payment_done" || caseStatus === "contacted" : caseStatus === "payment_done" || caseStatus === "contacted"
? t.requestAccepted.titleMalePaymentDone
: t.requestAccepted.titleMalePaymentPending;
? t["Contact info released"]
: t["Request approved!"];
const primaryActionText = isFemaleProfile const primaryActionText = isFemaleProfile
? t.requestAccepted.primaryFemale
: t.requestAccepted.primaryMale;
? t["Report no contact"]
: t["View profile"];
const secondaryActionText = isFemaleProfile const secondaryActionText = isFemaleProfile
? t.requestAccepted.secondaryFemale
? t["Record call result"]
: caseStatus === "payment_done" || caseStatus === "contacted" : caseStatus === "payment_done" || caseStatus === "contacted"
? t.requestAccepted.secondaryMalePaymentDone
: t.requestAccepted.secondaryMalePaymentPending;
? t["View contact number"]
: t["Pay and get contact"];
const contactInfoPhoneItems = getContactInfoPhoneItems( const contactInfoPhoneItems = getContactInfoPhoneItems(
contactInfoQuery.data?.contact_info, contactInfoQuery.data?.contact_info,
); );
@ -356,8 +356,8 @@ export default function RequestAcceptedPage() {
{isContactInfoSheetOpen ? ( {isContactInfoSheetOpen ? (
<FemaleConsentSheet <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={ buttons={
contactInfoPhoneItems.length ? ( contactInfoPhoneItems.length ? (
<div className="space-y-4"> <div className="space-y-4">
@ -367,7 +367,7 @@ export default function RequestAcceptedPage() {
</div> </div>
) : ( ) : (
<div className="rounded-[12px] bg-[#ECECEC] px-4 py-3 group-12 font-semibold text-[#555]"> <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> </div>
) )
} }
@ -394,10 +394,10 @@ export default function RequestAcceptedPage() {
🎉 🎉
</div> </div>
<h1 className="mt-8 text-center text-[24px] leading-[1.25] font-bold text-[#E03950]"> <h1 className="mt-8 text-center text-[24px] leading-[1.25] font-bold text-[#E03950]">
{t.candidateContact.congratsTitle}
{t["Congratulations! 🎉"]}
</h1> </h1>
<p className="mt-6 text-[#4A4A4A] group-14 font-medium leading-relaxed"> <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> </p>
</div> </div>
) : ( ) : (
@ -405,7 +405,7 @@ export default function RequestAcceptedPage() {
<div className="relative isolate flex items-center justify-center"> <div className="relative isolate flex items-center justify-center">
<Image <Image
src="/assets/images/Group 15978804fdasf68.svg" src="/assets/images/Group 15978804fdasf68.svg"
alt={t.requestAccepted.imageAlt}
alt={t["Request accepted"]}
width={131} width={131}
height={125} height={125}
priority priority
@ -420,14 +420,14 @@ export default function RequestAcceptedPage() {
{caseStatus === "contacted" ? ( {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]"> <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"> <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> </p>
</div> </div>
) : ( ) : (
<p className="mt-4 max-w-[315px] group-12 leading-[1.45] font-semibold text-[#777777]"> <p className="mt-4 max-w-[315px] group-12 leading-[1.45] font-semibold text-[#777777]">
{isFemaleProfile {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> </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]" 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>
<button <button
@ -452,7 +452,7 @@ export default function RequestAcceptedPage() {
{outcomeMutation.isPending ? ( {outcomeMutation.isPending ? (
<DotsLoader className="text-white" /> <DotsLoader className="text-white" />
) : ( ) : (
t.candidateContact.submitOutcome
t["Submit Final Outcome"]
)} )}
</button> </button>
</div> </div>
@ -503,7 +503,7 @@ export default function RequestAcceptedPage() {
{caseStatus !== "contacted" ? ( {caseStatus !== "contacted" ? (
<div className="border border-[#F0445B] bg-[#F0445B]/10 rounded-xl mt-4"> <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"> <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> </p>
</div> </div>
) : null} ) : null}
@ -514,11 +514,11 @@ export default function RequestAcceptedPage() {
<div className="space-y-8 pb-20"> <div className="space-y-8 pb-20">
{/* Advisor section */} {/* Advisor section */}
<AdvisorActionsCard <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} avatars={advisorAvatars}
extraCount={7} extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorLabel={t["Get Advisor"]}
getAdvisorHref="/questions-list" getAdvisorHref="/questions-list"
/> />
@ -536,7 +536,7 @@ export default function RequestAcceptedPage() {
/> />
<h2 className="group-14 leading-none font-semibold text-[#747474]"> <h2 className="group-14 leading-none font-semibold text-[#747474]">
{t.requestAccepted.profileLocked}
{t["Profile is locked"]}
</h2> </h2>
</div> </div>
</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 ( return (

17
src/components/Componentes/button.tsx

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

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

@ -25,7 +25,14 @@ export function CallResultSheet({
...props ...props
}: CallResultSheetProps) { }: CallResultSheetProps) {
const { dictionary: t } = useI18n(); 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 groupId = useId();
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true); const [isEntering, setIsEntering] = useState(true);
@ -100,7 +107,7 @@ export function CallResultSheet({
.join(" ")} .join(" ")}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={t.sheets.callResult}
aria-label={t["Call result"]}
tabIndex={-1} tabIndex={-1}
onClick={(event) => { onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) { if (closeOnOutside && event.target === event.currentTarget) {
@ -130,7 +137,7 @@ export function CallResultSheet({
> >
<div className="mx-auto flex flex-col items-center"> <div className="mx-auto flex flex-col items-center">
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]"> <h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]">
{t.sheets.callResult}
{t["Call result"]}
</h2> </h2>
<div <div
@ -139,7 +146,7 @@ export function CallResultSheet({
aria-labelledby={groupId} aria-labelledby={groupId}
> >
<span id={groupId} className="sr-only"> <span id={groupId} className="sr-only">
{t.sheets.selectCallResult}
{t["Select call result"]}
</span> </span>
<div className="flex flex-col gap-[14px]"> <div className="flex flex-col gap-[14px]">
@ -191,27 +198,27 @@ export function CallResultSheet({
</div> </div>
</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 <button
type="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} 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> </div>
</button> </button>
<button <button
type="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={() => { onClick={() => {
onSubmit?.(selectedReason); onSubmit?.(selectedReason);
closeSheet(); 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> </div>
</button> </button>
</div> </div>

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

@ -1,9 +1,9 @@
"use client"; "use client";
import type { HTMLAttributes } from "react"; 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 { useI18n } from "@/translations/provider";
import Button from "./button";
const EXIT_ANIMATION_MS = 220; const EXIT_ANIMATION_MS = 220;
@ -24,13 +24,34 @@ export function DismissReasonSheet({
...props ...props
}: DismissReasonSheetProps) { }: DismissReasonSheetProps) {
const { dictionary: t } = useI18n(); 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 groupId = useId();
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true); const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
const [selectedReason, setSelectedReason] = useState<string>(options[0]);
const [selectedReasons, setSelectedReasons] = useState<string[]>([]);
const [reasonText, setReasonText] = useState(""); 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 = () => { const closeSheet = () => {
if (isClosing) { if (isClosing) {
@ -98,7 +119,7 @@ export function DismissReasonSheet({
.join(" ")} .join(" ")}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={t.sheets.dismissReasons}
aria-label={t["Dismiss reasons"]}
tabIndex={-1} tabIndex={-1}
onClick={(event) => { onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) { if (closeOnOutside && event.target === event.currentTarget) {
@ -119,104 +140,125 @@ export function DismissReasonSheet({
<section <section
{...props} {...props}
className={[ 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", isClosing || isEntering ? "translate-y-full" : "translate-y-0",
className, className,
] ]
.filter(Boolean) .filter(Boolean)
.join(" ")} .join(" ")}
> >
<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.dismissReasons}
</h2>
<div
className="mt-5 w-full"
role="radiogroup"
aria-labelledby={groupId}
>
<span id={groupId} className="sr-only">
{t.sheets.dismissReasons}
</span>
<div className="flex flex-col gap-[14px]">
{options.map((option) => {
const checked = selectedReason === 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(" ")}
>
<button
type="button"
className={[
"flex w-full items-start gap-3 text-left",
checked ? "text-[#171717]" : "text-[#7B7B7B]",
]
.filter(Boolean)
.join(" ")}
onClick={() => setSelectedReason(option)}
>
<span
aria-hidden="true"
<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["Dismiss reasons"]}
</h2>
<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>
<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 = selectedReasons.includes(option);
const showTextArea = option === options[options.length - 1];
return (
<div key={option} className="w-full flex flex-col gap-2">
<button
type="button"
className={[ className={[
"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border",
checked ? "border-[#F0445B]" : "border-[#9E9E9E]",
"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) .filter(Boolean)
.join(" ")} .join(" ")}
onClick={() => {
setSelectedReasons((prev) =>
prev.includes(option)
? prev.filter((r) => r !== option)
: [...prev, option],
);
}}
> >
{checked ? (
<span className="h-3 w-3 rounded-full bg-[#F0445B]" />
) : null}
</span>
<span className="min-w-0 flex-1">
<span className="text-[16px] leading-[1.2] font-semibold">
{option}
<span
aria-hidden="true"
className={[
"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 ? (
<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> </span>
</span>
</button>
{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}
value={reasonText}
onChange={(event) => setReasonText(event.target.value)}
/>
) : null}
</div>
);
})}
</div>
</div>
<p className="mt-4 w-full text-left text-[14px] leading-[1.45] text-[#2C2C2C]">
{t.sheets.dismissDescription}
</p>
<span className="min-w-0 flex-1">
<span className="text-[16px] leading-[1.2] font-semibold">
{option}
</span>
</span>
</button>
{checked && showTextArea ? (
<textarea
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)
}
/>
) : null}
</div>
);
})}
</div>
</fieldset>
</div>
<div className="mt-[14px] w-full">
<div className="mt-4 w-full">
<Button <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={() => { 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(); closeSheet();
}} }}
> >
{t.common.decline}
{t["Decline"]}
</Button> </Button>
</div> </div>
</div> </div>

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

@ -1,18 +1,20 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { IoAlertCircle, IoClose } from "react-icons/io5";
import { IoAlertCircle, IoClose, IoCheckmarkCircle } from "react-icons/io5";
type ErrorToastProps = { type ErrorToastProps = {
message: string; message: string;
onClose: () => void; onClose: () => void;
duration?: number; duration?: number;
variant?: "error" | "success";
}; };
export default function ErrorToast({ export default function ErrorToast({
message, message,
onClose, onClose,
duration = 4000, duration = 4000,
variant = "error",
}: ErrorToastProps) { }: ErrorToastProps) {
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
@ -30,6 +32,8 @@ export default function ErrorToast({
setTimeout(onClose, 300); setTimeout(onClose, 300);
}; };
const isSuccess = variant === "success";
return ( return (
<div <div
className={[ className={[
@ -37,19 +41,30 @@ export default function ErrorToast({
isVisible isVisible
? "translate-y-0 opacity-100 scale-100" ? "translate-y-0 opacity-100 scale-100"
: "-translate-y-4 opacity-0 scale-95", : "-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(" ")} ].join(" ")}
role="alert" role="alert"
> >
<IoAlertCircle className="size-5 shrink-0 text-red-500" />
{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"> <span className="flex-1 text-sm font-semibold leading-normal">
{message} {message}
</span> </span>
<button <button
type="button" type="button"
onClick={handleClose} 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" /> <IoClose className="size-4" />
</button> </button>

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

@ -102,7 +102,7 @@ export function FemaleConsentSheet({
.join(" ")} .join(" ")}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={t.sheets.informationSheet}
aria-label={t["Information sheet"]}
tabIndex={-1} tabIndex={-1}
onClick={(event) => { onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) { 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 resolvedTitle = "Tips";
const resolvedDescription = const resolvedDescription =
description ?? 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."; "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 = const resolvedButtonText =
buttonText ?? buttonText ??
(t.common as Record<string, string | undefined>)?.gotIt ??
t["Got it"] ??
"Got it"; "Got it";
const closeSheet = useCallback(() => { const closeSheet = useCallback(() => {
@ -203,9 +203,7 @@ export function HelpModal({
})()} })()}
<div className="mt-5 w-full"> <div className="mt-5 w-full">
<Button onClick={closeSheet} className="py-[14px]">
{resolvedButtonText}
</Button>
<Button onClick={closeSheet}>{resolvedButtonText}</Button>
</div> </div>
</div> </div>
</section> </section>

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

@ -4,6 +4,7 @@ import Image, { type StaticImageData } from "next/image";
import type { HTMLAttributes, ReactNode } from "react"; import type { HTMLAttributes, ReactNode } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Button from "./button"; import Button from "./button";
import { LoadingSkeleton } from "./loading-skeleton";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220; const EXIT_ANIMATION_MS = 220;
@ -33,11 +34,12 @@ export type InformationSheetProps = Omit<
"children" | "title" "children" | "title"
> & { > & {
icon?: InformationSheetIcon | null; icon?: InformationSheetIcon | null;
title: ReactNode | ((controls: { close: () => void }) => ReactNode);
title?: ReactNode | ((controls: { close: () => void }) => ReactNode);
description?: ReactNode; description?: ReactNode;
buttons?: ReactNode | ((controls: { close: () => void }) => ReactNode); buttons?: ReactNode | ((controls: { close: () => void }) => ReactNode);
closeOnOutside?: boolean; closeOnOutside?: boolean;
onClose?: () => void; onClose?: () => void;
isLoading?: boolean;
}; };
const DEFAULT_ICON = { const DEFAULT_ICON = {
@ -132,9 +134,16 @@ export function InformationSheet({
closeOnOutside = true, closeOnOutside = true,
onClose, onClose,
className, className,
isLoading = false,
...props ...props
}: InformationSheetProps) { }: 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 [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true); const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
@ -150,9 +159,7 @@ export function InformationSheet({
const resolvedTitle = typeof title === "function" ? title(controls) : title; const resolvedTitle = typeof title === "function" ? title(controls) : title;
const resolvedButtons = const resolvedButtons =
typeof buttons === "string" ? ( typeof buttons === "string" ? (
<Button onClick={closeSheet} className="py-[18px]">
{buttons}
</Button>
<Button onClick={closeSheet}>{buttons}</Button>
) : typeof buttons === "function" ? ( ) : typeof buttons === "function" ? (
buttons(controls) buttons(controls)
) : ( ) : (
@ -217,8 +224,9 @@ export function InformationSheet({
.join(" ")} .join(" ")}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={t.sheets.informationSheet}
aria-label={t["Information sheet"]}
tabIndex={-1} tabIndex={-1}
dir={isRtl ? "rtl" : "ltr"}
onClick={(event) => { onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) { if (closeOnOutside && event.target === event.currentTarget) {
closeSheet(); closeSheet();
@ -245,35 +253,51 @@ export function InformationSheet({
.filter(Boolean) .filter(Boolean)
.join(" ")} .join(" ")}
> >
<div className="mx-auto flex flex-col items-center">
{resolvedIcon ? (
<Image
src={resolvedIcon.src}
alt={resolvedIcon.alt}
width={resolvedIcon.width}
height={resolvedIcon.height}
priority
/>
) : null}
<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}
alt={resolvedIcon.alt}
width={resolvedIcon.width}
height={resolvedIcon.height}
priority
/>
) : null}
<h2
className={[
resolvedIcon ? "mt-2.5" : "mt-0",
"w-full group-16 leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]",
].join(" ")}
>
{resolvedTitle}
</h2>
<h2
className={[
resolvedIcon ? "mt-2.5" : "mt-0",
"w-full group-16 leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]",
].join(" ")}
>
{resolvedTitle}
</h2>
{description ? (
<div className="mt-3 group-12 text-[#4D4D4D]">{description}</div>
) : null}
{description ? (
<div className="mt-3 group-12 text-[#4D4D4D]">{description}</div>
) : null}
{resolvedButtons ? (
<div className="mt-4 flex w-full flex-col gap-3">
{resolvedButtons}
</div>
) : null}
{resolvedButtons ? (
<div className="mt-4 flex w-full flex-col gap-3">
{resolvedButtons}
</div>
) : null}
</>
)}
</div> </div>
</section> </section>
</div> </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 Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { ButtonHTMLAttributes, ReactNode } from "react"; import type { ButtonHTMLAttributes, ReactNode } from "react";
import { useState } from "react";
import { useState, useRef, useEffect } from "react";
import { GoArrowLeft } from "react-icons/go"; import { GoArrowLeft } from "react-icons/go";
import { HiEllipsisVertical } from "react-icons/hi2";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
import HelpModal from "./help-modal"; import HelpModal from "./help-modal";
import SupportSheet from "./support-sheet"; 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 = type NavigationButtonIcon =
| "back" | "back"
@ -17,7 +27,8 @@ type NavigationButtonIcon =
| "info" | "info"
| "document" | "document"
| "subscription" | "subscription"
| "consultation";
| "consultation"
| "more";
type NavigationButtonVariant = "default" | "transparent"; type NavigationButtonVariant = "default" | "transparent";
export type NavigationButtonProps = Omit< export type NavigationButtonProps = Omit<
@ -49,11 +60,157 @@ export function NavigationButton({
const { dictionary: t } = useI18n(); const { dictionary: t } = useI18n();
const [isHelpOpen, setIsHelpOpen] = useState(false); const [isHelpOpen, setIsHelpOpen] = useState(false);
const [isSupportOpen, setIsSupportOpen] = 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 isFemale = profile?.gender === "female";
const hasActiveSubscription = !!profile?.active_subscription; 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; const hasSupportAccess = true;
@ -124,6 +281,13 @@ export function NavigationButton({
height={24} height={24}
/> />
); );
case "more":
return (
<HiEllipsisVertical
aria-hidden="true"
className={`size-6 ${variant === "transparent" ? "text-white" : "text-[#111111]"}`}
/>
);
default: default:
return null; return null;
} }
@ -138,9 +302,97 @@ export function NavigationButton({
const resolvedHelpDescription = const resolvedHelpDescription =
helpDescription ?? helpDescription ??
(icon === "document" (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); : 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 ( return (
<> <>
<button <button
@ -149,9 +401,9 @@ export function NavigationButton({
aria-label={ aria-label={
iconLabel ?? iconLabel ??
(icon === "back" (icon === "back"
? t.common.back
? t["Back"]
: icon === "consultation" : icon === "consultation"
? (t.common as any).consultation
? (t as any)["Consultation"]
: icon) : icon)
} }
onClick={(event) => { onClick={(event) => {
@ -173,6 +425,12 @@ export function NavigationButton({
router.back(); router.back();
} else if (icon === "support") { } else if (icon === "support") {
setIsSupportOpen(true); setIsSupportOpen(true);
} else if (icon === "subscription") {
setIsSubscriptionInfoOpen(true);
setIsSubscriptionLoading(true);
refetch().finally(() => {
setIsSubscriptionLoading(false);
});
} }
} }
} }
@ -202,6 +460,16 @@ export function NavigationButton({
isOpen={isSupportOpen} isOpen={isSupportOpen}
onClose={() => setIsSupportOpen(false)} 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) .filter(Boolean)
.join(" ")} .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} {text || children}
</p> </p>
</div> </div>

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

@ -85,8 +85,8 @@ export function OutcomeSelectionSheet({
} }
const options = [ 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 ( return (
@ -101,7 +101,7 @@ export function OutcomeSelectionSheet({
.join(" ")} .join(" ")}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label={t.candidateContact.outcomeTitle}
aria-label={t["What was the outcome of your contact?"]}
tabIndex={-1} tabIndex={-1}
onClick={(event) => { onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) { if (closeOnOutside && event.target === event.currentTarget) {
@ -131,7 +131,7 @@ export function OutcomeSelectionSheet({
> >
<div className="mx-auto flex flex-col items-center"> <div className="mx-auto flex flex-col items-center">
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]"> <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> </h2>
<div <div
@ -140,7 +140,7 @@ export function OutcomeSelectionSheet({
aria-labelledby={groupId} aria-labelledby={groupId}
> >
<span id={groupId} className="sr-only"> <span id={groupId} className="sr-only">
{t.candidateContact.outcomeTitle}
{t["What was the outcome of your contact?"]}
</span> </span>
<div className="flex flex-col gap-[14px]"> <div className="flex flex-col gap-[14px]">
@ -184,27 +184,27 @@ export function OutcomeSelectionSheet({
</div> </div>
</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 <button
type="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} 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> </div>
</button> </button>
<button <button
type="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={() => { onClick={() => {
onSubmit?.(selectedStatus); onSubmit?.(selectedStatus);
closeSheet(); 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> </div>
</button> </button>
</div> </div>

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

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

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

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

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

@ -57,7 +57,7 @@ export function QuestionCard({
return ( return (
<Link <Link
href={localizePath(`/questions-list/${item.slug}`, locale)} 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]" className="block rounded-[20px] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#F26C85]"
> >
<article <article
@ -87,14 +87,14 @@ export function QuestionCard({
{item.title} {item.title}
</h2> </h2>
<p className="mt-1.5 group-10 leading-none font-medium text-[#7A7A7A]"> <p className="mt-1.5 group-10 leading-none font-medium text-[#7A7A7A]">
{t.common.estimateTime}: {item.estimate}
{t["Estimate time"]}: {item.estimate}
</p> </p>
</div> </div>
<div className="flex min-h-[44px] shrink-0 flex-col items-end justify-between self-stretch"> <div className="flex min-h-[44px] shrink-0 flex-col items-end justify-between self-stretch">
{item.required ? ( {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"> <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> </span>
) : item.showInfoBadge ? ( ) : item.showInfoBadge ? (
<button <button

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

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

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

@ -365,7 +365,7 @@ export default function QuestionNumber({
</div> </div>
{isOutOfRange ? ( {isOutOfRange ? (
<span className="block text-[10px] font-semibold text-[#F2465F]"> <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}`} `Please enter a value between ${min} and ${max}`}
</span> </span>
) : null} ) : null}
@ -416,7 +416,7 @@ export default function QuestionNumber({
/> />
{isOutOfRange ? ( {isOutOfRange ? (
<span className="block text-[10px] font-semibold text-[#F2465F]"> <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}`} `Please enter a value between ${min} and ${max}`}
</span> </span>
) : null} ) : 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 // On first render: if backend is loading or we need an IP fetch, show loading
if (isLoading) return true; if (isLoading) return true;
// If there's already a saved value in the initial render, no loading needed // 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(); return needsIpFetch();
}); });
@ -397,10 +401,11 @@ export function QuestionPhone({
} }
// If user already has a saved value from backend, use it — no IP check needed // 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) { if (hasSavedValue) {
hasFetchedIpRef.current = true; hasFetchedIpRef.current = true;
@ -572,14 +577,20 @@ export function QuestionPhone({
/> />
</svg> </svg>
</button> </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> </div>
<span className="flex min-w-0 flex-1 items-center pr-4"> <span className="flex min-w-0 flex-1 items-center pr-4">
<input <input
type="tel" type="tel"
inputMode="tel" inputMode="tel"
disabled={disabled} disabled={disabled}
placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")}
placeholder={question.extras.placeHolder?.replace(
/^\+\d+\s*/,
"",
)}
value={phoneValue} value={phoneValue}
maxLength={getMaxLengthForCountry(codeValue)} maxLength={getMaxLengthForCountry(codeValue)}
onChange={(event) => { onChange={(event) => {
@ -587,7 +598,9 @@ export function QuestionPhone({
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem("hasCheckedGeoIPPhone", "true"); localStorage.setItem("hasCheckedGeoIPPhone", "true");
} }
const nextPhoneValue = sanitizePhoneNumber(event.target.value);
const nextPhoneValue = sanitizePhoneNumber(
event.target.value,
);
const maxLen = getMaxLengthForCountry(codeValue); const maxLen = getMaxLengthForCountry(codeValue);
const truncatedPhone = nextPhoneValue.slice(0, maxLen); 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 activeQuestion = questions?.[activeQuestionIndex];
const showNotice = activeQuestion?.showGuardianNotice; const showNotice = activeQuestion?.showGuardianNotice;
const isUnder27 = const isUnder27 =
age !== null
? isFemale && age < 27
: (activeQuestion?.required ?? false);
age !== null ? isFemale && age < 27 : (activeQuestion?.required ?? false);
return ( return (
<> <>
@ -174,8 +172,8 @@ function SectionFlowContent({
<div className="w-full px-[17px] mb-6 shrink-0"> <div className="w-full px-[17px] mb-6 shrink-0">
<NoticeBox> <NoticeBox>
{isUnder27 {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> </NoticeBox>
</div> </div>
)} )}

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

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

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

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

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

@ -136,20 +136,20 @@ export default function RequiredStepsCard({
)} )}
</span> </span>
<h2 className="group-16 leading-none font-bold tracking-[-0.02em]"> <h2 className="group-16 leading-none font-bold tracking-[-0.02em]">
{t.questions.requiredSteps}
{t["Required Steps"]}
</h2> </h2>
</div> </div>
<p className="mt-2.5 max-w-[220px] group-12 leading-[1.25] font-medium text-white/80"> <p className="mt-2.5 max-w-[220px] group-12 leading-[1.25] font-medium text-white/80">
{isCompleted {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> </p>
</div> </div>
<div <div
role="img" role="img"
aria-label={t.questions.requiredStepsProgress
aria-label={t["{completed} of {total} required steps completed"]
.replace("{completed}", String(completed)) .replace("{completed}", String(completed))
.replace("{total}", String(total))} .replace("{total}", String(total))}
className="relative flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-full" 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"> <div className="min-h-0 flex-1 overflow-y-auto pb-28">
<SliderHeader index={index} title="Registration Type" /> <SliderHeader index={index} title="Registration Type" />
<NoticeBox className="mt-8" align="justify"> <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> </NoticeBox>
<div className="flex min-h-[calc(100dvh-520px)] items-center justify-center pt-6"> <div className="flex min-h-[calc(100dvh-520px)] items-center justify-center pt-6">
<div className="w-full max-w-[440px] space-y-4"> <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} onClose={onClose}
buttons={({ close }) => ( 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 <button
type="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} 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> </div>
</button> </button>
<button <button
type="button" type="button"
disabled={isPaymentPending} 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} 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 <Image
src="/assets/images/Inner Plugdsain Iframe.svg" src="/assets/images/Inner Plugdsain Iframe.svg"
alt="" alt=""
@ -68,7 +68,7 @@ export function SubscriptionRequiredSheet({
height={18} height={18}
className="shrink-0" className="shrink-0"
/> />
<span>50 Coins</span>
<span className="truncate">50</span>
</span> </span>
</div> </div>
</button> </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]"> <div className="flex flex-col items-center w-full gap-[11px]">
{/* Support Icon */} {/* Support Icon */}
<div className="flex h-[38px] w-[38px] items-center justify-center"> <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> </div>
{/* Text Content */} {/* Text Content */}
<div className="flex flex-col items-start gap-[14px] w-full"> <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"> <h3 className="w-full font-bold text-[16px] leading-[20px] text-[#111111] text-center">
{t.common.supportTitle}
{t["Contact Support"]}
</h3> </h3>
<p className="w-full font-normal text-[14px] leading-[20px] text-[#747474] text-center"> <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> </p>
</div> </div>
</div> </div>
@ -147,7 +150,7 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) {
onClick={closeSheet} 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]" 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> </button>
{/* Contact WhatsApp Button */} {/* Contact WhatsApp Button */}
@ -163,7 +166,7 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) {
height={21} height={21}
className="shrink-0 brightness-0 invert" className="shrink-0 brightness-0 invert"
/> />
{t.common.supportSwipeText}
{t["Contact"]}
</button> </button>
</div> </div>
</section> </section>

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

@ -128,7 +128,7 @@ export function SwipeButton({
return ( return (
<div <div
ref={containerRef} 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" disabled ? "opacity-60 cursor-not-allowed" : "cursor-pointer"
}`} }`}
onTouchStart={onTouchStart} 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))" }} style={{ paddingBottom: "calc(24px + var(--safe-bottom))" }}
> >
<Button <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} onClick={onStart}
> >
{startLabel} {startLabel}

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

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

4
src/data/question-data.ts

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

7
src/hooks/marriage/types.ts

@ -67,6 +67,7 @@ export type MarriageActiveSubscription = {
total_usages: number; total_usages: number;
is_active: boolean; is_active: boolean;
is_valid: boolean; is_valid: boolean;
remaining_days?: number;
}; };
export type MarriageActiveCase = { export type MarriageActiveCase = {
@ -110,6 +111,12 @@ export type MarriageProfile = {
match_summary: MarriageMatchSummary | null; match_summary: MarriageMatchSummary | null;
intro_video_url?: string | null; intro_video_url?: string | null;
intro_video_thumbnail_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; 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);
},
});
}

19
src/lib/auth-bridge.ts

@ -125,7 +125,11 @@ class AuthBridge {
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.sessionStorage.setItem(REDIRECT_SESSION_KEY, "true");
try {
window.sessionStorage.setItem(REDIRECT_SESSION_KEY, "true");
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
} }
this.loginRequested = true; this.loginRequested = true;
@ -214,12 +218,19 @@ class AuthBridge {
return null; 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 = const cookieToken =
getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie(TOKEN_COOKIE_NAME) ??
getClientCookie("habib_token") ?? 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 // Treat "NO_TOKEN" the same as having no cookie at all — fall through
// to the default token instead of returning null. // 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["Cache-Control"] = "no-cache, no-store, must-revalidate";
config.headers["Pragma"] = "no-cache"; 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 = const token =
authBridge.getToken() ?? 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

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

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

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

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

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

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

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

1009
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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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", "next": "Next",
"back": "Back", "back": "Back",
"accept": "Accept" "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