diff --git a/scratch/update_translations.js b/scratch/update_translations.js new file mode 100644 index 0000000..f043fce --- /dev/null +++ b/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}`); +}); diff --git a/src/app/candidate-contact/page.tsx b/src/app/candidate-contact/page.tsx index c04c925..a2f6f84 100644 --- a/src/app/candidate-contact/page.tsx +++ b/src/app/candidate-contact/page.tsx @@ -133,10 +133,10 @@ export default function CandidateContactPage() { 🎉

- {t.candidateContact.congratsTitle} + {t["Congratulations! 🎉"]}

- {t.candidateContact.congratsMessage} + {t["Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed."]}

) : ( @@ -145,7 +145,7 @@ export default function CandidateContactPage() {
{t.candidateContact.imageAlt} - {t.candidateContact.title} + {t["The selected candidate will contact your family shortly."]} {caseStatus === "contacted" ? ( @@ -162,7 +162,7 @@ export default function CandidateContactPage() { {/* Outcome instructions and button */}

- {t.candidateContact.thankYouFeedback} + {t["Thank you for giving us feedback, we would be very happy if you also let us know the final result."]}

@@ -176,7 +176,7 @@ export default function CandidateContactPage() { } className="flex-1 h-[52px] rounded-[15px] border border-[#BFBFBF] bg-white text-[#36363C] font-semibold group-14 flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F5F5F5]" > - {t.requestAccepted.actionViewProfile} + {t["View Profile"]}
@@ -206,7 +206,7 @@ export default function CandidateContactPage() { {contactStatusMutation.isPending ? ( ) : ( - t.candidateContact.noContactYet + t["Report No Contact"] )} @@ -225,7 +225,7 @@ export default function CandidateContactPage() { {contactStatusMutation.isPending ? ( ) : ( - t.candidateContact.contacted + t["Confirm Contacted"] )} @@ -233,7 +233,7 @@ export default function CandidateContactPage() { {/* Red warning box */}

- {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."]}

@@ -245,11 +245,11 @@ export default function CandidateContactPage() {
{/* Advisor section */} @@ -267,7 +267,7 @@ export default function CandidateContactPage() { alt="lock" />

- {t.requestSent.profileLocked} + {t["Profile is locked"]}

@@ -320,13 +320,13 @@ export default function CandidateContactPage() {
{t.candidateContact.imageAlt}

- {t.candidateContact.title} + {t["The selected candidate will contact your family shortly."]}

@@ -340,7 +340,7 @@ export default function CandidateContactPage() { onClick={() => setIsCallResultSheetOpen(true)} className="flex-1 h-[52px] rounded-[11px] bg-gradient-to-tl from-[#FE6F82] to-[#E03950] text-white font-semibold flex items-center justify-center cursor-pointer transition-opacity active:opacity-90" > - {t.candidateContact.contacted} + {t["Confirm Contacted"]} diff --git a/src/app/finding-match/page.tsx b/src/app/finding-match/page.tsx index dadb079..bf9046a 100644 --- a/src/app/finding-match/page.tsx +++ b/src/app/finding-match/page.tsx @@ -4,7 +4,7 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useMemo } from "react"; -import { DotsLoader } from "@/components/Componentes/button"; +import Button, { DotsLoader } from "@/components/Componentes/button"; import { FaLock, FaPen } from "react-icons/fa6"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import PageHeader from "@/components/Componentes/page-header"; @@ -13,6 +13,9 @@ import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; +import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end"; +import { useRejectionSeenMutation } from "@/hooks/marriage/use-rejection-seen"; +import { IoAlertCircle } from "react-icons/io5"; const advisorAvatars = [ { id: "advisor-primary", src: "/assets/images/Avatar Image.png" }, @@ -27,6 +30,9 @@ export default function FindingMatchPage() { refetchInterval: 3000, }); + const { mutate: markRejectionSeen, isPending: isMarkingSeen } = + useRejectionSeenMutation(); + useEffect(() => { if (!profile) { return; @@ -53,7 +59,14 @@ export default function FindingMatchPage() { ); } - const copy = t.findingMatch; + const copy = { + title: t["SEARCH IN PROGRESS"], + description: t["Our system is actively looking for compatible partners based on your criteria. This process requires time and patience. We will notify you immediately once a profile is ready for your review."], + advisorTitle: t["Get an advisor"], + advisorDescription: t["Not sure what to do next? Our psychology section is here to guide you at every step."], + getAdvisor: t["Get Advisor"], + editProfile: t["Edit Profile"] + }; const matchImageSrc = "/assets/images/Group 1597880466.svg"; return ( @@ -61,7 +74,7 @@ export default function FindingMatchPage() {
- {profile?.can_edit_profile === false ? ( -
-
- ) : ( - - + + {profile?.can_edit_profile === false ? ( +
+
+ ) : ( + + )} +
+ + {profile?.unseen_rejection && ( +
+
+
+ +
+ +

+ {t["Your request was rejected"]} +

+ +

+ {t["Your request was rejected by the lady. You will be introduced to other candidates in the future."]} +

+ +
+ +
+
+
+ )} ); } diff --git a/src/app/globals.css b/src/app/globals.css index 2042ff2..1304c4b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -284,3 +284,32 @@ body[data-page-background="custom"] .app-shell { animation: button-dot-bounce 0.9s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation-delay: 0.3s; } + +/* ─── Premium Shimmer Animations ─── */ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} +.shimmer-bg { + background: linear-gradient( + 90deg, + rgba(0, 0, 0, 0.05) 25%, + rgba(0, 0, 0, 0.1) 37%, + rgba(0, 0, 0, 0.05) 63% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite linear; +} +.dark .shimmer-bg { + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.04) 25%, + rgba(255, 255, 255, 0.09) 37%, + rgba(255, 255, 255, 0.04) 63% + ); + background-size: 200% 100%; +} diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx index 676d221..a7357ca 100644 --- a/src/app/intro/page.tsx +++ b/src/app/intro/page.tsx @@ -6,14 +6,14 @@ import { useEffect, useRef, useState } from "react"; import Button from "@/components/Componentes/button"; import PageHeader from "@/components/Componentes/page-header"; import ReportActionsSheet from "@/components/Componentes/report-actions-sheet"; +import VideoPlayer from "@/components/Componentes/video-player"; import type { MarriageProfileResponse } from "@/hooks/marriage/types"; -import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config"; +import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { authBridge } from "@/lib/auth-bridge"; import { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; -import VideoPlayer from "@/components/Componentes/video-player"; const REDIRECT_SESSION_KEY = "redirect"; @@ -40,33 +40,44 @@ export default function Intro() { 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 nextPath = localizePath(getSubmitPath(profileResponse), locale); 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) { - router.replace(nextPath); + const currentPath = + typeof window !== "undefined" ? window.location.pathname : ""; + if (currentPath === nextPath) { + setIsCheckingRedirect(false); + } else { + router.replace(nextPath); + } return; } - } catch { + } catch (error) { + console.error("Error in redirectIfNeeded:", error); if (!isCancelled) { setIsCheckingRedirect(false); } @@ -148,57 +159,57 @@ export default function Intro() {
{t.intro.imageAlt}

- {t.intro.title} + {t["A Path to Heavenly Marriage"]}

- {t.intro.description} + {t["We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims"]}

{t.intro.userProfile}

120

- {t.intro.userProfile} + {t["user profiles"]}

{t.intro.matches}

14

- {t.intro.matches} + {t["matches"]}

{t.intro.marriage}

14

- {t.intro.marriage} + {t["marriages"]}

@@ -212,7 +223,7 @@ export default function Intro() { config?.intro_video_thumbnail_url || "/assets/images/Frame 2095586523.png" } - alt={t.intro.videoAlt} + alt={t["video"]} fill sizes="344px" className="object-cover transition-transform duration-300 group-hover:scale-105" @@ -221,7 +232,7 @@ export default function Intro() {
{t.intro.playAlt} - {t.common.submit} + {t["Submit"]}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bdb7654..4129deb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -121,11 +121,17 @@ export default function RootLayout({ this._habib_token = value; if (value) { writeCookie(HABIB_TOKEN_COOKIE, value); - sessionStorage.setItem(HABIB_TOKEN_COOKIE, value); + try { + sessionStorage.setItem(HABIB_TOKEN_COOKIE, value); + } catch (e) {} } }, get: function() { - return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || sessionStorage.getItem(HABIB_TOKEN_COOKIE) || undefined; + var ssVal; + try { + ssVal = sessionStorage.getItem(HABIB_TOKEN_COOKIE); + } catch (e) {} + return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || ssVal || undefined; } }); } @@ -137,20 +143,30 @@ export default function RootLayout({ this._habib_coins = value; if (value !== undefined && value !== null && value !== '') { writeCookie(HABIB_COINS_COOKIE, value); - sessionStorage.setItem(HABIB_COINS_COOKIE, String(value)); + try { + sessionStorage.setItem(HABIB_COINS_COOKIE, String(value)); + } catch (e) {} } }, get: function() { var cookieValue = readCookie(HABIB_COINS_COOKIE); - return this._habib_coins || parseInt(cookieValue || sessionStorage.getItem(HABIB_COINS_COOKIE) || '0'); + var ssVal; + try { + ssVal = sessionStorage.getItem(HABIB_COINS_COOKIE); + } catch (e) {} + return this._habib_coins || parseInt(cookieValue || ssVal || '0'); } }); } // Performance monitoring window.addEventListener('load', function() { - 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) {} }); } `, diff --git a/src/app/new-match/page.tsx b/src/app/new-match/page.tsx index da84e8b..eae551b 100644 --- a/src/app/new-match/page.tsx +++ b/src/app/new-match/page.tsx @@ -10,6 +10,7 @@ import { DotsLoader } from "@/components/Componentes/button"; import PageHeader from "@/components/Componentes/page-header"; import { PageBackground } from "@/components/Componentes/page-background"; import { IoClose } from "react-icons/io5"; +import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; import { useHabcoinPaymentMutation } from "@/hooks/marriage/use-habcoin-payment"; import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; import type { @@ -276,30 +277,86 @@ export default function NewMatchPage() { return getSubmitPath(profile) !== "/new-match"; }, [profile]); + const matchSummary = profile?.match_summary ?? null; + const matchDisplay = useMatchSummaryDisplay(matchSummary); + if (isLoading || isRedirecting) { return ( <> -
- +
+ + +
+ {/* Header Section Skeleton */} +
+ + + + +
+ +
+ {/* Match Card Skeleton */} +
+ {/* Name line */} + + + {/* Subtitle / Details lines */} +
+ + + +
+ + {/* Button skeleton */} + +
+ + {/* Advisor Card Skeleton */} +
+
+ +
+ + +
+ +
+
+ + + + +
+ + +
+
+
+
+
); } - - const matchSummary = profile?.match_summary ?? null; - const matchDisplay = useMatchSummaryDisplay(matchSummary); const pairedFields = [matchDisplay.age, matchDisplay.city].filter( (field): field is DisplayField => Boolean(field), ); const isFemaleProfile = profile?.gender === "female"; const matchHeadingTitle = isFemaleProfile - ? t.match.newMatchTitleFemale - : t.match.newMatchTitleMale; + ? t["New Marriage Proposal"] + : t["YOU HAVE A NEW MATCH!"]; const matchHeadingDescription = isFemaleProfile - ? t.match.newMatchDescriptionFemale - : t.match.newMatchDescriptionMale; + ? t["A suitable match has been found for you. If approved, your profile will be evaluated to proceed with the introduction process."] + : t["If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information."]; const isMale = profile?.gender === "male"; const hasActiveSub = !!profile?.active_subscription; @@ -339,79 +396,93 @@ export default function NewMatchPage() {

-
- {isLoading ? ( -
- -
- ) : isError ? ( -

- Unable to load match summary. -

- ) : matchSummary ? ( - <> -

- Name: - {matchDisplay.name} -

- -
- {matchDisplay.occupation ? ( - - ) : null} - - {pairedFields.length ? ( -

- {pairedFields.map((field, index) => ( - - {index > 0 ? | : null} - - {field.label}: {field.value} - - - ))} -

- ) : null} - - {matchDisplay.maritalStatus ? ( - - ) : null} - {matchDisplay.cityPreference ? ( - - ) : null} + {isLoading ? ( +
+
+ {/* Name line */} + + + {/* Subtitle / Details lines */} +
+ + +
- - - ) : ( -

- No match summary is available yet. -

- )} -
+ {/* Button skeleton */} + +
+
+ ) : ( +
+ {isError ? ( +

+ Unable to load match summary. +

+ ) : matchSummary ? ( + <> +

+ Name: + {matchDisplay.name} +

+ +
+ {matchDisplay.occupation ? ( + + ) : null} + + {pairedFields.length ? ( +

+ {pairedFields.map((field, index) => ( + + {index > 0 ? | : null} + + {field.label}: {field.value} + + + ))} +

+ ) : null} + + {matchDisplay.maritalStatus ? ( + + ) : null} + {matchDisplay.cityPreference ? ( + + ) : null} +
+ + + + ) : ( +

+ No match summary is available yet. +

+ )} +
+ )}
-
@@ -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" >
@@ -463,24 +534,27 @@ export default function NewMatchPage() {

- {t.paymentModal?.title || "Verification & Subscription Activation"} + {t["Verification & Subscription Activation"] || + "Verification & Subscription Activation"}

- {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."}

- {t.paymentModal?.activeFor3Months || "Valid for 3 months"} + {t["Valid for 3 months"] || "Valid for 3 months"} - {t.paymentModal?.cost || "50 Habib Coins"} + {t["50 Coins"] || "50 Habib Coins"}

- {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."}

{paymentError && ( @@ -489,28 +563,36 @@ export default function NewMatchPage() { )} -
+
diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index 5568e29..8dce08c 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -3,10 +3,11 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; -import Button, { DotsLoader } from "@/components/Componentes/button"; +import Button from "@/components/Componentes/button"; import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet"; import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet"; import InformationSheet from "@/components/Componentes/information-sheet"; +import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; import NavigationButton from "@/components/Componentes/navigation-button"; import { PageBackground } from "@/components/Componentes/page-background"; import StickyHeader from "@/components/Componentes/sticky-header"; @@ -187,49 +188,49 @@ function NewMatchProfileSkeleton() { return ( <> -
+

- {t.match.title} + {t["New Match"]}

-
-
-
+
+ +
-
-
+
+
-
-
+ +
-
-
+ +
-
-
+ +
-
-
+ +
@@ -241,8 +242,8 @@ function NewMatchProfileSkeleton() { >
-
-
+ +
@@ -306,7 +307,6 @@ export default function NewMatchProfilePage() { const caseId = profile?.active_case?.case_id; const caseStatus = profile?.active_case?.status; const isFemaleProfile = profile?.gender === "female"; - const isMaleAccepted = caseStatus === "male_accepted"; const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { onSuccess: async (_, variables) => { if (variables.action === "accept") { @@ -364,11 +364,11 @@ export default function NewMatchProfilePage() { } const isSubmitting = respondMutation.isPending; - const isAcceptProfileEnabled = isFemaleProfile - ? !isSubmitting - : Boolean(caseId) && - !isSubmitting && - canAcceptProfile(profile?.gender, caseStatus); + const isAcceptProfileEnabled = + Boolean(caseId) && + !isSubmitting && + canAcceptProfile(profile?.gender, caseStatus); + const isRejectProfileEnabled = isAcceptProfileEnabled; const nameParts = candidateName.trim().split(/\s+/); const firstName = nameParts[0] || ""; @@ -377,10 +377,10 @@ export default function NewMatchProfilePage() { const mainClass = "-mx-[17px] flex min-h-screen flex-col pb-10"; const mainStyle = { - backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg"), linear-gradient(178.25deg, rgba(255, 197, 196, 0.2) 1.48%, rgba(251, 237, 237, 0.2) 20.64%)`, + backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, backgroundColor: "#F5F5F5", backgroundRepeat: "repeat-y", - backgroundSize: "100% auto, 100% 100%", + backgroundSize: "100% auto", }; return ( @@ -389,8 +389,8 @@ export default function NewMatchProfilePage() { {isRequestSheetOpen ? ( isFemaleProfile ? ( (
@@ -422,7 +422,7 @@ export default function NewMatchProfilePage() { className="py-[18px] text-[18px]" onClick={close} > - {t.common.cancel} + {t["Cancel"]}
@@ -454,8 +454,8 @@ export default function NewMatchProfilePage() { ) : ( (
)} @@ -491,21 +491,20 @@ export default function NewMatchProfilePage() { {isRejectSheetOpen ? ( (
)} @@ -515,7 +514,7 @@ export default function NewMatchProfilePage() { {isMaleRejectWarningOpen ? (

- {t.maleRejectionWarning.carefulReview} + {t["Before making a final decision, please carefully review the other person's profile again completely to make an informed choice."]}

💡

- {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."]}

- {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."]}

@@ -549,7 +548,7 @@ export default function NewMatchProfilePage() { buttons={({ close }) => (
{ close(); setIsDismissReasonSheetOpen(true); @@ -557,10 +556,10 @@ export default function NewMatchProfilePage() { />
)} @@ -589,13 +588,13 @@ export default function NewMatchProfilePage() {

- {t.match.title} + {t["New Match"]}

@@ -670,13 +669,13 @@ export default function NewMatchProfilePage() { onClick={() => router.back()} className="w-full h-[52px] flex items-center justify-center rounded-[12px] bg-[#F5F5F5] text-[#36363C] font-semibold text-[16px] transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#EBEBEB]" > - {t.common.back} + {t["Back"]} ) : (
)} diff --git a/src/app/questions-list/[slug]/answer-pace-sheet.tsx b/src/app/questions-list/[slug]/answer-pace-sheet.tsx index 4d93f9b..7514a52 100644 --- a/src/app/questions-list/[slug]/answer-pace-sheet.tsx +++ b/src/app/questions-list/[slug]/answer-pace-sheet.tsx @@ -109,7 +109,12 @@ export default function AnswerPaceSheet({ isSuccess && !hasSeenSheet && !hasLocalProgress && !hasAnySectionProgress; 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)); }, [sectionSlugs, storageKey]); @@ -118,7 +123,11 @@ export default function AnswerPaceSheet({ return; } - window.sessionStorage.setItem(storageKey, "true"); + try { + window.sessionStorage.setItem(storageKey, "true"); + } catch (e) { + console.warn("sessionStorage is not accessible:", e); + } }, [isOpen, storageKey]); if (!isOpen) { diff --git a/src/app/questions-list/[slug]/page.tsx b/src/app/questions-list/[slug]/page.tsx index 6113089..d4d88e6 100644 --- a/src/app/questions-list/[slug]/page.tsx +++ b/src/app/questions-list/[slug]/page.tsx @@ -46,11 +46,11 @@ export default async function QuestionDetailPage({ itemSlug={item.slug} locale={locale} questionsListHref={questionsListHref} - title={t.questions.answerAtYourOwnPace} - description={t.questions.answerAtYourOwnPaceDescription} - closeLabel={t.questions.closeQuestionsList} - informationLabel={t.sheets.informationSheet} - continueLabel={t.common.submit} + title={t["Answer at Your Own Pace"]} + description={t["You can pause the survey anytime and resume later. Your progress is saved automatically."]} + closeLabel={t["Close questions list"]} + informationLabel={t["Information sheet"]} + continueLabel={t["Submit"]} /> ); } diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 49ca667..ef3fea6 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -759,7 +759,15 @@ export default function QuestionDetailClient({ const bulletKey = item.slug === "glasser_5_needs_test" ? "glasser" : "personality"; - const bullets = t.questions.testIntroBullets[bulletKey]; + const bullets = bulletKey === "glasser" ? [ + t["Understanding your five basic needs helps you recognize what truly drives your behavior in relationships."], + t["Taking this test is not mandatory, but it will help you better understand your priorities and find a more compatible spouse."], + t["By identifying your strongest needs, you can better communicate your expectations and build healthier relationships."] + ] : [ + t["Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?"], + t["Taking this test is not mandatory, but it will help you better search for a spouse. The Personality test is a test for self-knowledge and better understanding of your spouse."], + t["Do you operate based on superficial behavioral adaptations, or are you aware of the deep \"source traits\" that fundamentally control your decision-making processes?"] + ]; return ( <> @@ -791,10 +799,10 @@ export default function QuestionDetailClient({ { setIsTestStarted(true); }} diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx index 8522cc8..97e530b 100644 --- a/src/app/questions-list/page.tsx +++ b/src/app/questions-list/page.tsx @@ -14,6 +14,8 @@ import RequiredStepsCard from "@/components/Componentes/required-steps-card"; import Button from "@/components/Componentes/button"; import InformationSheet from "@/components/Componentes/information-sheet"; import NavigationButton from "@/components/Componentes/navigation-button"; +import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end"; +import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; import { PageBackground } from "@/components/Componentes/page-background"; import ErrorToast from "@/components/Componentes/error-toast"; import { @@ -40,10 +42,12 @@ export default function QuestionsListPage() { const { dictionary: t, locale } = useI18n(); const router = useRouter(); 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({ onSuccess: () => { @@ -142,9 +146,9 @@ export default function QuestionsListPage() { useEffect(() => { if (startMatchMutation.isError || isSyncError) { - setToastMessage(t.questions.startMatchFailed); + setToastMessage(t["Sending the match request failed. Please check your connection and try again."]); } - }, [startMatchMutation.isError, isSyncError, t.questions.startMatchFailed]); + }, [startMatchMutation.isError, isSyncError, t["Sending the match request failed. Please check your connection and try again."]]); const handleCloseToast = () => { setToastMessage(null); @@ -217,6 +221,78 @@ export default function QuestionsListPage() { } }; + if (isProfileLoading || isSectionsLoading) { + return ( + <> + +
+
+
+ +
+ { + e.preventDefault(); + if (typeof window !== "undefined" && (window as any).HabibApp) { + (window as any).HabibApp.postMessage( + JSON.stringify({ action: "close_service" }), + ); + } else { + router.push("/"); + } + }} + /> +

+ {t["Profile registration"]} +

+ +
+ +
+ {/* Required Steps Card Skeleton */} + + + {/* Section Cards Skeletons */} +
+ {Array.from({ length: 6 }).map((_, idx) => ( +
+ +
+ + +
+
+ + +
+
+ ))} +
+
+ + + + +
+ + ); + } + return ( <> {toastMessage && ( @@ -225,31 +301,31 @@ export default function QuestionsListPage() { {isOptionalInfoSheetOpen ? ( - {t.questions.optionalInfoPromptDescription} + {t["You've completed all required fields. However, filling in all sections will help us find better matches for you"]}

} onClose={() => setIsOptionalInfoSheetOpen(false)} buttons={({ close }) => ( -
+
)} @@ -298,7 +374,7 @@ export default function QuestionsListPage() { > { e.preventDefault(); if (typeof window !== "undefined" && (window as any).HabibApp) { @@ -311,11 +387,11 @@ export default function QuestionsListPage() { }} />

- {t.questions.profileRegistration} + {t["Profile registration"]}

@@ -340,12 +416,9 @@ export default function QuestionsListPage() {
-
+ -
+
); diff --git a/src/app/questions-list/sections-request.tsx b/src/app/questions-list/sections-request.tsx index 024bcbb..9e10a78 100644 --- a/src/app/questions-list/sections-request.tsx +++ b/src/app/questions-list/sections-request.tsx @@ -36,9 +36,14 @@ export default function SectionsRequest() { const isOpen = isSuccess && hasNoProgression && !hasSeenSheet; 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(() => { @@ -46,7 +51,11 @@ export default function SectionsRequest() { 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]); if (!isOpen) { @@ -79,7 +88,7 @@ export default function SectionsRequest() { } buttons={({ close }) => ( - )} diff --git a/src/app/request-accepted/page.tsx b/src/app/request-accepted/page.tsx index 53fddc8..6121206 100644 --- a/src/app/request-accepted/page.tsx +++ b/src/app/request-accepted/page.tsx @@ -226,18 +226,18 @@ export default function RequestAcceptedPage() { } const titleText = isFemaleProfile - ? t.requestAccepted.titleFemale + ? t["Request Approved"] : caseStatus === "payment_done" || caseStatus === "contacted" - ? t.requestAccepted.titleMalePaymentDone - : t.requestAccepted.titleMalePaymentPending; + ? t["Contact info released"] + : t["Request approved!"]; const primaryActionText = isFemaleProfile - ? t.requestAccepted.primaryFemale - : t.requestAccepted.primaryMale; + ? t["Report no contact"] + : t["View profile"]; const secondaryActionText = isFemaleProfile - ? t.requestAccepted.secondaryFemale + ? t["Record call result"] : caseStatus === "payment_done" || caseStatus === "contacted" - ? t.requestAccepted.secondaryMalePaymentDone - : t.requestAccepted.secondaryMalePaymentPending; + ? t["View contact number"] + : t["Pay and get contact"]; const contactInfoPhoneItems = getContactInfoPhoneItems( contactInfoQuery.data?.contact_info, ); @@ -356,8 +356,8 @@ export default function RequestAcceptedPage() { {isContactInfoSheetOpen ? ( @@ -367,7 +367,7 @@ export default function RequestAcceptedPage() {
) : (
- {t.requestAccepted.contactNotAvailable} + {t["Contact information is not available yet."]}
) } @@ -394,10 +394,10 @@ export default function RequestAcceptedPage() { 🎉

- {t.candidateContact.congratsTitle} + {t["Congratulations! 🎉"]}

- {t.candidateContact.congratsMessage} + {t["Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed."]}

) : ( @@ -405,7 +405,7 @@ export default function RequestAcceptedPage() {
{t.requestAccepted.imageAlt}

- {t.candidateContact.thankYouFeedback} + {t["Thank you for giving us feedback, we would be very happy if you also let us know the final result."]}

) : (

{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."]}

)} @@ -440,7 +440,7 @@ export default function RequestAcceptedPage() { } className="flex-1 h-[52px] rounded-[15px] border border-[#BFBFBF] bg-white text-[#36363C] font-semibold group-14 flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F5F5F5]" > - {t.requestAccepted.actionViewProfile} + {t["View Profile"]} @@ -503,7 +503,7 @@ export default function RequestAcceptedPage() { {caseStatus !== "contacted" ? (

- {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."]}

) : null} @@ -514,11 +514,11 @@ export default function RequestAcceptedPage() {
{/* Advisor section */} @@ -536,7 +536,7 @@ export default function RequestAcceptedPage() { />

- {t.requestAccepted.profileLocked} + {t["Profile is locked"]}

diff --git a/src/app/request-sent/page.tsx b/src/app/request-sent/page.tsx index 45eb242..d8ee754 100644 --- a/src/app/request-sent/page.tsx +++ b/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 ( diff --git a/src/components/Componentes/button.tsx b/src/components/Componentes/button.tsx index 69122e0..5efce95 100644 --- a/src/components/Componentes/button.tsx +++ b/src/components/Componentes/button.tsx @@ -12,7 +12,12 @@ import { GoArrowRight } from "react-icons/go"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; -type ButtonVariant = "default" | "secondary" | "countdown" | "outlined"; +type ButtonVariant = + | "default" + | "secondary" + | "countdown" + | "outlined" + | "dark"; type ArrowDirection = "left" | "right"; export type ButtonProps = Omit< @@ -123,13 +128,15 @@ export function Button({ "items-center", "justify-center", "h-[52px]", - "rounded-[11px]", + variant === "dark" ? "rounded-[9px]" : "rounded-[11px]", "px-4", "text-center", "transition-opacity", - isOutlined + variant === "outlined" ? "border border-[#8B8B8B] bg-transparent text-[#8B8B8B]" - : "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", className, ] @@ -169,7 +176,7 @@ export function Button({ {renderArrow("left")} - + {children} {description && !isOutlined ? ( diff --git a/src/components/Componentes/call-result-sheet.tsx b/src/components/Componentes/call-result-sheet.tsx index 3745737..5062423 100644 --- a/src/components/Componentes/call-result-sheet.tsx +++ b/src/components/Componentes/call-result-sheet.tsx @@ -25,7 +25,14 @@ export function CallResultSheet({ ...props }: CallResultSheetProps) { const { dictionary: t } = useI18n(); - const callResultOptions = t.sheets.callOptions; + const callResultOptions = [ + t["Not a good personal fit"], + t["No mutual interest"], + t["Different expectations"], + t["No connection felt"], + t["Location not suitable"], + t["Other reasons"] + ]; const groupId = useId(); const [isVisible, setIsVisible] = useState(true); const [isEntering, setIsEntering] = useState(true); @@ -100,7 +107,7 @@ export function CallResultSheet({ .join(" ")} role="dialog" aria-modal="true" - aria-label={t.sheets.callResult} + aria-label={t["Call result"]} tabIndex={-1} onClick={(event) => { if (closeOnOutside && event.target === event.currentTarget) { @@ -130,7 +137,7 @@ export function CallResultSheet({ >

- {t.sheets.callResult} + {t["Call result"]}

- {t.sheets.selectCallResult} + {t["Select call result"]}
@@ -191,27 +198,27 @@ export function CallResultSheet({
-
+
diff --git a/src/components/Componentes/dismiss-reason-sheet.tsx b/src/components/Componentes/dismiss-reason-sheet.tsx index d814cf0..48c205a 100644 --- a/src/components/Componentes/dismiss-reason-sheet.tsx +++ b/src/components/Componentes/dismiss-reason-sheet.tsx @@ -1,9 +1,9 @@ "use client"; import type { HTMLAttributes } from "react"; -import { useEffect, useId, useState } from "react"; -import Button from "./button"; +import { useEffect, useId, useRef, useState } from "react"; import { useI18n } from "@/translations/provider"; +import Button from "./button"; const EXIT_ANIMATION_MS = 220; @@ -24,13 +24,34 @@ export function DismissReasonSheet({ ...props }: DismissReasonSheetProps) { const { dictionary: t } = useI18n(); - const options = t.sheets.callOptions; + const options = [ + t["Not a good personal fit"], + t["No mutual interest"], + t["Different expectations"], + t["No connection felt"], + t["Location not suitable"], + t["Other reasons"] + ]; const groupId = useId(); const [isVisible, setIsVisible] = useState(true); const [isEntering, setIsEntering] = useState(true); const [isClosing, setIsClosing] = useState(false); - const [selectedReason, setSelectedReason] = useState(options[0]); + const [selectedReasons, setSelectedReasons] = useState([]); const [reasonText, setReasonText] = useState(""); + const textareaRef = useRef(null); + + useEffect(() => { + const otherReason = options[options.length - 1]; + if (selectedReasons.includes(otherReason)) { + const timeoutId = setTimeout(() => { + textareaRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + }); + }, 100); + return () => clearTimeout(timeoutId); + } + }, [selectedReasons, options]); const closeSheet = () => { if (isClosing) { @@ -98,7 +119,7 @@ export function DismissReasonSheet({ .join(" ")} role="dialog" aria-modal="true" - aria-label={t.sheets.dismissReasons} + aria-label={t["Dismiss reasons"]} tabIndex={-1} onClick={(event) => { if (closeOnOutside && event.target === event.currentTarget) { @@ -119,104 +140,125 @@ export function DismissReasonSheet({
-
-

- {t.sheets.dismissReasons} -

- -
- - {t.sheets.dismissReasons} - - -
- {options.map((option) => { - const checked = selectedReason === option; - const showTextArea = option === options[options.length - 1]; - - return ( -
- - - {checked && showTextArea ? ( -