Browse Source

feat: implement match profile overlay, add Amiri fonts, and integrate discount validation logic.

master
mortezaei 7 days ago
parent
commit
cfb568af35
  1. BIN
      public/fonts/Amiri/Amiri-Bold.ttf
  2. BIN
      public/fonts/Amiri/Amiri-BoldItalic.ttf
  3. BIN
      public/fonts/Amiri/Amiri-Italic.ttf
  4. BIN
      public/fonts/Amiri/Amiri-Regular.ttf
  5. 93
      public/fonts/Amiri/OFL.txt
  6. 29
      src/app/candidate-contact/candidate-contact-client.tsx
  7. 5
      src/app/finding-match/finding-match-client.tsx
  8. 30
      src/app/layout.tsx
  9. 6
      src/app/marriage-advisors/page.tsx
  10. 544
      src/app/new-match/new-match-client.tsx
  11. 40
      src/app/new-match/profile/page.tsx
  12. 18
      src/app/request-accepted/request-accepted-client.tsx
  13. 23
      src/app/request-sent/request-sent-client.tsx
  14. 25
      src/components/Componentes/data-error-state.tsx
  15. 218
      src/components/Componentes/discount-widget.tsx
  16. 29
      src/components/Componentes/information-sheet.tsx
  17. 140
      src/components/Componentes/match-profile-overlay.test.tsx
  18. 77
      src/components/Componentes/match-profile-overlay.tsx
  19. 323
      src/components/Componentes/navigation-button.tsx
  20. 65
      src/components/Componentes/page-header.tsx
  21. 4
      src/components/Componentes/sticky-header.tsx
  22. 198
      src/components/Componentes/subscription-required-sheet.tsx
  23. 19
      src/components/Componentes/support-access.ts
  24. 806
      src/components/Componentes/token-switcher.tsx
  25. 27
      src/hooks/marriage/use-habcoin-inventory.ts
  26. 32
      src/hooks/marriage/use-habcoin-payment.ts
  27. 36
      src/hooks/marriage/use-profile-main.ts
  28. 77
      src/hooks/marriage/use-validate-discount.ts
  29. 26
      src/lib/webview-actions.test.ts
  30. 14
      src/lib/webview-actions.ts
  31. 18
      src/translations/locales/ar.json
  32. 18
      src/translations/locales/az.json
  33. 18
      src/translations/locales/bn.json
  34. 18
      src/translations/locales/da.json
  35. 18
      src/translations/locales/de.json
  36. 17
      src/translations/locales/en.json
  37. 18
      src/translations/locales/es.json
  38. 17
      src/translations/locales/fa.json
  39. 18
      src/translations/locales/fr.json
  40. 18
      src/translations/locales/gu.json
  41. 18
      src/translations/locales/ha.json
  42. 21
      src/translations/locales/he.json
  43. 18
      src/translations/locales/hi.json
  44. 21
      src/translations/locales/id.json
  45. 21
      src/translations/locales/ks.json
  46. 21
      src/translations/locales/pt.json
  47. 17
      src/translations/locales/ru.json
  48. 21
      src/translations/locales/sw.json
  49. 21
      src/translations/locales/tg.json
  50. 21
      src/translations/locales/tr.json
  51. 21
      src/translations/locales/ul.json
  52. 21
      src/translations/locales/ur.json
  53. 21
      src/translations/locales/uz.json
  54. 18
      src/translations/locales/zh.json

BIN
public/fonts/Amiri/Amiri-Bold.ttf

BIN
public/fonts/Amiri/Amiri-BoldItalic.ttf

BIN
public/fonts/Amiri/Amiri-Italic.ttf

BIN
public/fonts/Amiri/Amiri-Regular.ttf

93
public/fonts/Amiri/OFL.txt

@ -0,0 +1,93 @@
Copyright 2010-2022 The Amiri Project Authors (https://github.com/aliftype/amiri).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

29
src/app/candidate-contact/candidate-contact-client.tsx

@ -14,6 +14,9 @@ import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay";
import MatchProfileOverlay, {
useMatchProfileOverlay,
} from "@/components/Componentes/match-profile-overlay";
import { PageBackground } from "@/components/Componentes/page-background";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
@ -39,6 +42,8 @@ export default function CandidateContactClient() {
useState(false);
const { isAdvisorOpen, openAdvisors, closeAdvisors } =
useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } =
useMatchProfileOverlay();
const { data: profile, isLoading: isProfileLoading } =
useMarriageProfileQuery({
@ -128,6 +133,7 @@ export default function CandidateContactClient() {
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(12px,calc(var(--safe-top)+4px))] pb-[calc(20px+var(--safe-bottom))]">
<PageHeader
className="-mx-[6px]"
profile={profile}
leftButton={
isFemale ? { icon: "back", className: "hidden" } : undefined
}
@ -189,11 +195,7 @@ export default function CandidateContactClient() {
<div className="flex mt-6 w-full gap-3">
<button
type="button"
onClick={() =>
router.push(
localizePath("/new-match/profile", locale),
)
}
onClick={() => openProfile()}
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["View Profile"]}
@ -308,6 +310,11 @@ export default function CandidateContactClient() {
open={isAdvisorOpen}
onClose={closeAdvisors}
/>
<MatchProfileOverlay
open={isProfileOpen}
onClose={closeProfile}
/>
</>
);
}
@ -347,7 +354,7 @@ export default function CandidateContactClient() {
) : null}
<main className="-mx-[17px] flex min-h-screen flex-col px-[17px] pt-[max(12px,calc(var(--safe-top)+4px))] pb-36">
<PageHeader />
<PageHeader profile={profile} />
<section className="flex flex-1 items-center justify-center">
<div className="flex max-w-[300px] flex-col items-center">
@ -389,6 +396,16 @@ export default function CandidateContactClient() {
</button>
</section>
</main>
<MarriageAdvisorsOverlay
open={isAdvisorOpen}
onClose={closeAdvisors}
/>
<MatchProfileOverlay
open={isProfileOpen}
onClose={closeProfile}
/>
</>
);
}

5
src/app/finding-match/finding-match-client.tsx

@ -89,10 +89,7 @@ export default function FindingMatchClient() {
style={{ paddingBottom: "calc(100px + var(--safe-bottom))" }}
className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(12px,calc(var(--safe-top)+4px))] text-center"
>
<PageHeader
className="-mx-[6px]"
rightButton={{ icon: "subscription", iconLabel: "Subscribe" }}
/>
<PageHeader className="-mx-[6px]" />
<section className="mt-20 flex flex-1 flex-col items-center">
<div className="relative h-[124px] w-[130px]" aria-hidden="true">

30
src/app/layout.tsx

@ -1,5 +1,4 @@
import type { Metadata, Viewport } from "next";
import { Amiri } from "next/font/google";
import localFont from "next/font/local";
import { cookies, headers } from "next/headers";
import Providers from "./providers";
@ -20,12 +19,29 @@ const faminela = localFont({
fallback: ["Arial", "sans-serif"],
});
// Amiri has no UI consumer above the fold (arabic fonts are mapped to Segoe
// UI in globals.css), so preloading it only competes with critical resources
// on a cold start. `display: swap` lets it load lazily if a consumer appears.
const amiri = Amiri({
weight: ["400", "700"],
subsets: ["arabic"],
const amiri = localFont({
src: [
{
path: "../../public/fonts/Amiri/Amiri-Regular.ttf",
weight: "400",
style: "normal",
},
{
path: "../../public/fonts/Amiri/Amiri-Italic.ttf",
weight: "400",
style: "italic",
},
{
path: "../../public/fonts/Amiri/Amiri-Bold.ttf",
weight: "700",
style: "normal",
},
{
path: "../../public/fonts/Amiri/Amiri-BoldItalic.ttf",
weight: "700",
style: "italic",
},
],
variable: "--font-amiri",
display: "swap",
preload: false,

6
src/app/marriage-advisors/page.tsx

@ -46,10 +46,10 @@ export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPagePr
<>
<PageBackground />
<main
className="-mx-[17px] flex min-h-screen flex-col pb-10"
className="-mx-[17px] flex h-dvh flex-col overflow-hidden pb-10"
style={mainStyle}
>
<StickyHeader>
<StickyHeader sticky={false} className="shrink-0">
<div className="flex items-center justify-between gap-3">
<NavigationButton
variant="transparent"
@ -67,7 +67,7 @@ export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPagePr
</div>
</StickyHeader>
<section className="px-[17px] pb-32 pt-5 flex flex-col items-center gap-4">
<section className="flex-1 overflow-y-auto overflow-x-hidden px-[17px] pb-32 pt-5 flex flex-col items-center gap-4">
{isLoading && <AdvisorsSkeleton />}
{isError && (

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

@ -10,12 +10,18 @@ import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay";
import MatchProfileOverlay, {
useMatchProfileOverlay,
} from "@/components/Componentes/match-profile-overlay";
import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background";
import { IoClose } from "react-icons/io5";
import InformationSheet from "@/components/Componentes/information-sheet";
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot";
import { DiscountWidget } from "@/components/Componentes/discount-widget";
import type { CheckDiscountResult } from "@/hooks/marriage/use-validate-discount";
import { useHabcoinPaymentMutation } from "@/hooks/marriage/use-habcoin-payment";
import { useHabcoinInventoryQuery } from "@/hooks/marriage/use-habcoin-inventory";
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import type {
MarriageField,
@ -39,38 +45,51 @@ const advisorAvatars = [
{ id: "advisor-tertiary", src: "/assets/images/Avatar Image.png" },
];
const fieldCandidates = {
name: ["name", "full_name", "fullname", "first_name", "display_name"],
const fieldCandidateMatchers = {
name: [
"name",
"full_name",
"fullname",
"first_name",
"last_name",
"display_name",
],
occupation: [
"job_title",
"occupation",
"job",
"profession",
"career",
"work",
"education",
"highest_level_of_education",
"field_of_study",
"employment_status",
],
age: ["age"],
age: ["age", "date_of_birth", "birth_date", "dob"],
city: [
"current_residence",
"city",
"current_city",
"residence_city",
"location",
"residence",
"birthplace",
"birth_city",
],
maritalStatus: [
"current_marital_status",
"marital_status",
"maritalstatus",
"relationship_status",
"current_marital_status",
],
cityPreference: [
"willingness_to_relocate",
"city_preference",
"citypreference",
"preferred_city",
"preferred_location",
"future_residence",
"residence_preference_after_marriage",
],
} as const;
@ -87,6 +106,69 @@ function normalizeFieldName(value: string) {
.replace(/[^a-z0-9]/g, "");
}
function matchesCandidate(
field: MarriageField,
candidates: readonly string[],
): boolean {
const rawKey = (field.key || "").toLowerCase();
const keyParts = rawKey.split(".");
const suffix = keyParts[keyParts.length - 1];
const normalizedKey = rawKey.replace(/[^a-z0-9]/g, "");
const normalizedSuffix = suffix.replace(/[^a-z0-9]/g, "");
for (const c of candidates) {
const normC = c.toLowerCase().replace(/[^a-z0-9]/g, "");
if (
normalizedSuffix === normC ||
normalizedKey.endsWith(normC) ||
rawKey === c.toLowerCase() ||
suffix === c.toLowerCase()
) {
return true;
}
}
return false;
}
function calculateAgeFromDob(dobString: string): number | null {
if (!dobString) return null;
const match = dobString.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if (!match) return null;
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10) - 1;
const day = parseInt(match[3], 10);
const birthDate = new Date(year, month, day);
if (isNaN(birthDate.getTime())) return null;
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const m = today.getMonth() - birthDate.getMonth();
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age > 0 && age < 120 ? age : null;
}
function formatOptionValue(
value: MarriageFieldValue,
dictionary?: Record<string, string>,
): string | null {
const base = formatFieldValue(value);
if (!base) return null;
if (!dictionary) return base;
if (dictionary[base]) return dictionary[base];
// Try replacing underscores with spaces: "single;_never_married" -> "single; never married"
const withSpaces = base.replace(/_/g, " ").trim();
if (dictionary[withSpaces]) return dictionary[withSpaces];
// Try capitalized first letter: "Single; never married"
const capitalized = withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
if (dictionary[capitalized]) return dictionary[capitalized];
return withSpaces;
}
function formatFieldValue(value: MarriageFieldValue) {
if (value === null || value === "") {
return null;
@ -127,8 +209,11 @@ function titleFromKey(key: string) {
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function toDisplayField(field: MarriageField): DisplayField | null {
const value = formatFieldValue(field.value);
function toDisplayField(
field: MarriageField,
dictionary?: Record<string, string>,
): DisplayField | null {
const value = formatOptionValue(field.value, dictionary);
if (!value) {
return null;
@ -145,69 +230,155 @@ function pickField(
fields: MarriageField[],
candidates: readonly string[],
usedIndexes: Set<number>,
) {
const candidateSet = new Set(candidates.map(normalizeFieldName));
dictionary?: Record<string, string>,
): DisplayField | null {
for (const [fieldIndex, field] of fields.entries()) {
if (usedIndexes.has(fieldIndex)) {
continue;
}
const displayField = toDisplayField(field);
if (
displayField &&
[field.key, field.label].some((value) =>
candidateSet.has(normalizeFieldName(value)),
)
) {
usedIndexes.add(fieldIndex);
return displayField;
if (matchesCandidate(field, candidates)) {
const displayField = toDisplayField(field, dictionary);
if (displayField) {
usedIndexes.add(fieldIndex);
return displayField;
}
}
}
return null;
}
function useMatchSummaryDisplay(matchSummary: MarriageMatchSummary | null) {
function useMatchSummaryDisplay(
matchSummary: MarriageMatchSummary | null,
t?: Record<string, string>,
) {
return useMemo(() => {
const fields = matchSummary?.public_info ?? [];
const usedIndexes = new Set<number>();
const name = pickField(fields, fieldCandidates.name, usedIndexes);
// 1. Name: Combine first_name and last_name if available, or find general name
let displayName: string | null = null;
const firstNameIdx = fields.findIndex(
(f) =>
f.key === "personal_identity.first_name" ||
f.key?.endsWith(".first_name"),
);
const lastNameIdx = fields.findIndex(
(f) =>
f.key === "personal_identity.last_name" ||
f.key?.endsWith(".last_name"),
);
if (firstNameIdx !== -1 && fields[firstNameIdx].value) {
usedIndexes.add(firstNameIdx);
const firstName = formatFieldValue(fields[firstNameIdx].value);
if (lastNameIdx !== -1 && fields[lastNameIdx].value) {
usedIndexes.add(lastNameIdx);
const lastName = formatFieldValue(fields[lastNameIdx].value);
displayName = `${firstName} ${lastName}`.trim();
} else {
displayName = firstName;
}
} else {
const nameField = pickField(
fields,
fieldCandidateMatchers.name,
usedIndexes,
t,
);
if (nameField) {
displayName = nameField.value;
}
}
if (!displayName && matchSummary?.id) {
displayName = `Profile #${matchSummary.id}`;
}
// 2. Occupation
const occupation = pickField(
fields,
fieldCandidates.occupation,
fieldCandidateMatchers.occupation,
usedIndexes,
t,
);
const age = pickField(fields, fieldCandidates.age, usedIndexes);
const city = pickField(fields, fieldCandidates.city, usedIndexes);
// 3. Age (extract and calculate from date_of_birth if available)
const dobIdx = fields.findIndex(
(f) =>
f.key === "personal_identity.date_of_birth" ||
f.key?.endsWith(".date_of_birth") ||
f.key?.toLowerCase().includes("date_of_birth") ||
f.key?.toLowerCase().includes("birth_date"),
);
let age: DisplayField | null = null;
if (dobIdx !== -1 && fields[dobIdx].value) {
usedIndexes.add(dobIdx);
const calculatedAge = calculateAgeFromDob(String(fields[dobIdx].value));
if (calculatedAge) {
age = {
id: fields[dobIdx].key,
label: t ? t["Age"] || "Age" : "Age",
value: `${calculatedAge}`,
};
}
}
if (!age) {
age = pickField(fields, fieldCandidateMatchers.age, usedIndexes, t);
}
// 4. City / Current Residence
const city = pickField(fields, fieldCandidateMatchers.city, usedIndexes, t);
// 5. Marital Status
const maritalStatus = pickField(
fields,
fieldCandidates.maritalStatus,
fieldCandidateMatchers.maritalStatus,
usedIndexes,
t,
);
// 6. City Preference / Relocation
const cityPreference = pickField(
fields,
fieldCandidates.cityPreference,
fieldCandidateMatchers.cityPreference,
usedIndexes,
t,
);
const extraFields = fields
.filter((_, index) => !usedIndexes.has(index))
.map(toDisplayField)
.map((f) => toDisplayField(f, t))
.filter((field): field is DisplayField => Boolean(field))
.slice(0, 4);
if (typeof window !== "undefined") {
console.log(
"🔍 [useMatchSummaryDisplay] input matchSummary:",
matchSummary,
);
console.log("🔍 [useMatchSummaryDisplay] computed output:", {
displayName,
occupation,
age,
city,
maritalStatus,
cityPreference,
extraFieldsCount: extraFields.length,
});
}
return {
age,
city,
cityPreference,
extraFields,
maritalStatus,
name:
name?.value ??
(matchSummary?.id ? `Profile #${matchSummary.id}` : null),
name: displayName,
occupation,
};
}, [matchSummary]);
}, [matchSummary, t]);
}
function FieldLine({ field }: { field: DisplayField }) {
@ -225,12 +396,29 @@ export default function NewMatchClient() {
const { top, bottom } = useViewPaddings();
const { isAdvisorOpen, openAdvisors, closeAdvisors } =
useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay();
const { data: profile, isError, isLoading } = useMarriageProfileQuery();
const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(null);
const [isInsufficientCoins, setIsInsufficientCoins] = useState(false);
const [appliedDiscount, setAppliedDiscount] =
useState<CheckDiscountResult | null>(null);
const { data: inventory, isLoading: isInventoryLoading } =
useHabcoinInventoryQuery({
enabled: isPaymentSheetOpen,
});
const planPrice = Number(profile?.recommended_plan?.price) || 50;
const finalPrice = appliedDiscount?.valid
? appliedDiscount.discountedPrice
: planPrice;
const coinBalance = inventory?.coin_balance ?? 0;
const hasEnoughCoins =
!isInsufficientCoins &&
(inventory === undefined ? true : coinBalance >= finalPrice);
const paymentMutation = useHabcoinPaymentMutation();
const caseId = profile?.active_case?.case_id;
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", {
@ -246,9 +434,12 @@ export default function NewMatchClient() {
try {
setPaymentError(null);
setIsInsufficientCoins(false);
await paymentMutation.mutateAsync(recommendedPlanId);
await paymentMutation.mutateAsync({
objectId: recommendedPlanId,
discountCode: appliedDiscount?.valid ? appliedDiscount.code : undefined,
});
setIsPaymentSheetOpen(false);
router.push(localizePath("/new-match/profile", locale));
openProfile();
} catch (err: any) {
console.error("Payment failed", err);
const msg =
@ -276,6 +467,19 @@ export default function NewMatchClient() {
};
useEffect(() => {
console.log("🔍 [NewMatchClient] Current React Query Profile State:", {
isLoading,
isError,
hasProfile: Boolean(profile),
profileId: profile?.id,
status: profile?.status,
active_case: profile?.active_case,
hasMatchSummary: Boolean(profile?.match_summary),
matchSummaryId: profile?.match_summary?.id,
publicInfoCount: profile?.match_summary?.public_info?.length,
fullProfileObject: profile,
});
if (!profile) {
return;
}
@ -283,7 +487,7 @@ export default function NewMatchClient() {
if (targetPath !== "/new-match") {
router.replace(localizePath(targetPath, locale));
}
}, [profile, locale, router]);
}, [profile, locale, router, isLoading, isError]);
// Signal Flutter to lift its loading cover once the profile is available.
useHabibWebReady(!!profile && !isLoading);
@ -294,7 +498,21 @@ export default function NewMatchClient() {
}, [profile]);
const matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary);
const matchDisplay = useMatchSummaryDisplay(matchSummary, t);
if (typeof window !== "undefined") {
if (!matchSummary && !isLoading && !isRedirecting) {
console.warn(
"⚠️ [NewMatchClient] match_summary is null on profile! 'No match summary is available yet.' will be displayed. Profile:",
profile,
);
} else if (matchSummary) {
console.log(
"✅ [NewMatchClient] Rendering match summary card with:",
matchDisplay,
);
}
}
if (isLoading || isRedirecting) {
return (
@ -307,7 +525,7 @@ export default function NewMatchClient() {
}}
className="-mx-[17px] min-h-screen h-full text-center px-4"
>
<PageHeader />
<PageHeader profile={profile} />
<div>
{/* Header Section Skeleton */}
@ -379,7 +597,10 @@ export default function NewMatchClient() {
];
const isMale = profile?.gender === "male";
const hasActiveSub = !!profile?.active_subscription;
const hasActiveSub =
!!profile?.active_subscription &&
profile.active_subscription.is_active !== false &&
profile.active_subscription.is_valid !== false;
const isMatchAvailable = !!profile?.match_summary;
return (
@ -393,7 +614,7 @@ export default function NewMatchClient() {
}}
className="-mx-[17px] min-h-screen h-full text-center px-4"
>
<PageHeader />
<PageHeader profile={profile} />
<div className="">
<section className="flex flex-col items-center mt-9">
@ -442,7 +663,6 @@ export default function NewMatchClient() {
) : matchSummary ? (
<>
<h2 className="break-words text-[13px] leading-[1.4] font-bold">
<span>Name: </span>
<span>{matchDisplay.name}</span>
</h2>
@ -478,9 +698,7 @@ export default function NewMatchClient() {
if (isMale && !hasActiveSub) {
setIsPaymentSheetOpen(true);
} else {
router.push(
localizePath("/new-match/profile", locale),
);
openProfile();
}
}}
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"
@ -531,91 +749,94 @@ export default function NewMatchClient() {
</div>
)}
{isPaymentSheetOpen && (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/50 backdrop-blur-xs p-4 animate-in fade-in duration-200">
<div
style={{ paddingBottom: `calc(20px + ${bottom}px)` }}
className="relative w-full max-w-[375px] rounded-[24px] bg-white px-5 pt-6 shadow-[0_-8px_30px_rgba(0,0,0,0.12)] border border-slate-100 animate-in slide-in-from-bottom duration-300 text-center"
>
<button
type="button"
className="absolute top-4 right-4 text-[#8F8F8F] hover:text-[#5F5F5F] transition-colors"
onClick={() => {
setIsPaymentSheetOpen(false);
setPaymentError(null);
setIsInsufficientCoins(false);
}}
>
<IoClose className="text-[22px]" />
</button>
<div className="flex flex-col items-center">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[#F0445B] text-white mb-4">
<Image
src="/assets/images/noun-wedding-rings-6540466 1.svg"
alt=""
width={28}
height={28}
className="text-white"
/>
</div>
<h3 className="group-16 font-bold text-gray-900 mb-2">
{t["Verification & Subscription Activation"] ||
"Verification & Subscription Activation"}
</h3>
{isPaymentSheetOpen && isInventoryLoading && (
<InformationSheet
isLoading={true}
style={{
paddingBottom: bottom > 0 ? `${14 + bottom}px` : undefined,
}}
onClose={() => {
setIsPaymentSheetOpen(false);
setPaymentError(null);
setIsInsufficientCoins(false);
}}
/>
)}
<p className="text-xs text-gray-500 leading-relaxed mb-4 text-center">
{isPaymentSheetOpen && !isInventoryLoading && (
<InformationSheet
style={{
paddingBottom: bottom > 0 ? `${14 + bottom}px` : undefined,
}}
icon="coin"
title={
!hasEnoughCoins ? (
<span className="text-[#BD3F3F]">
{t["You do not have enough Habib Coins"] ||
"You do not have enough Habib Coins"}
</span>
) : (
t["Verification & Subscription Activation"] ||
"Verification & Subscription Activation"
)
}
description={
!hasEnoughCoins ? (
<p className="text-[14px] text-[#4D4D4D] leading-[1.3] font-normal text-center">
{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."
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content"
] ||
"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."}
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content"}
</p>
) : (
<div className="space-y-3.5 text-center w-full">
<p className="text-[14px] text-[#4D4D4D] leading-[1.3] font-normal">
{t[
"This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."
] ||
"This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."}
</p>
<div className="w-full bg-white rounded-[11px] p-3 border border-gray-200/60 shadow-2xs">
<span className="text-[11px] text-[#8B8B8B] block mb-0.5 font-medium">
{t["Valid for 3 months"] || "Valid for 3 months"}
</span>
<span className="text-[16px] font-bold text-[#FF4E67]">
{finalPrice} {t["Coins"] || "Habib Coins"}
</span>
</div>
<div className="w-full bg-gray-50 rounded-xl p-3 mb-4 text-center">
<span className="text-[10px] text-gray-400 block mb-0.5">
{t["Valid for 3 months"] || "Valid for 3 months"}
</span>
<span className="text-base font-bold text-[#FF4E67]">
{t["50 Coins"] || "50 Habib Coins"}
</span>
<p className="text-[11px] text-[#8B8B8B] leading-normal">
{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>
</div>
<p className="text-[10px] text-gray-400 leading-normal mb-6 text-center">
{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>
)
}
onClose={() => {
setIsPaymentSheetOpen(false);
setPaymentError(null);
setIsInsufficientCoins(false);
setAppliedDiscount(null);
}}
buttons={
<div className="space-y-3 w-full">
{profile?.recommended_plan?.id ? (
<DiscountWidget
objectId={profile.recommended_plan.id}
onDiscountApplied={(res) => setAppliedDiscount(res)}
onDiscountCleared={() => setAppliedDiscount(null)}
/>
) : null}
{paymentError && (
<div className="w-full bg-[#FEF2F2] text-[#B91C1C] text-xs p-3 rounded-xl mb-4 text-center font-medium">
<div className="w-full bg-[#FEF2F2] text-[#B91C1C] text-xs p-3 rounded-[11px] text-center font-medium">
{paymentError}
</div>
)}
{isInsufficientCoins && isInFlutterWebView() && (
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left w-full mb-4"
onClick={() => buyHabibCoinPackages()}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[18px] bg-[#00AC78] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(0,172,120,0.28)] transition-opacity active:opacity-90">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={18}
height={18}
className="shrink-0"
/>
<span className="truncate">
{t["Buy Habib Coins"] || "Buy Habib Coins"}
</span>
</div>
</button>
)}
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
<button
type="button"
@ -625,7 +846,7 @@ export default function NewMatchClient() {
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={handleDecline}
>
<div className="inline-flex w-full items-center justify-center rounded-[18px] border border-[#9A9A9A] bg-[#F7F7F7] px-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">
<div className="inline-flex w-full items-center justify-center rounded-[11px] border border-[#747474] bg-transparent px-2 h-[52px] text-[16px] font-semibold text-[#747474] transition-opacity active:opacity-90 min-w-0">
{respondMutation.isPending ? (
<LoadingThreeDot />
) : (
@ -636,47 +857,60 @@ export default function NewMatchClient() {
</div>
</button>
<button
type="button"
disabled={
paymentMutation.isPending || respondMutation.isPending
}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={handlePayment}
>
<div className="inline-flex w-full items-center justify-center gap-3 rounded-[18px] bg-[#F0445B] px-4 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">
{paymentMutation.isPending ? (
<LoadingThreeDot />
) : (
<>
<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 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={18}
height={18}
className="shrink-0"
/>
<span className="truncate">50</span>
</span>
</>
)}
</div>
</button>
{!hasEnoughCoins ? (
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={() => buyHabibCoinPackages()}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] 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["Buy Habib Coins"] || "Buy Habib Coins"}
</span>
</div>
</button>
) : (
<button
type="button"
disabled={
paymentMutation.isPending || respondMutation.isPending
}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={handlePayment}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] 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">
{paymentMutation.isPending ? (
<LoadingThreeDot />
) : (
<>
<span className="truncate min-w-0">
{t["Pay"] || "Pay"}
</span>
<span className="inline-flex items-center gap-1 rounded-[12px] bg-black/15 px-[9px] py-[4px] text-xs font-semibold leading-none text-white shrink-0 min-w-0 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={16}
height={16}
className="shrink-0"
/>
<span className="truncate">{finalPrice}</span>
</span>
</>
)}
</div>
</button>
)}
</div>
</div>
</div>
</div>
}
/>
)}
<MarriageAdvisorsOverlay
open={isAdvisorOpen}
onClose={closeAdvisors}
/>
<MarriageAdvisorsOverlay open={isAdvisorOpen} onClose={closeAdvisors} />
<MatchProfileOverlay open={isProfileOpen} onClose={closeProfile} />
</>
);
}

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

@ -185,34 +185,40 @@ function MatchPublicProfileFields({
function NewMatchProfileSkeleton({
hideBackButton = false,
onClose,
}: {
hideBackButton?: boolean;
onClose?: () => void;
}) {
const { dictionary: t } = useI18n();
return (
<>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col bg-[#F5F5F5] pb-10">
<StickyHeader>
<main className="-mx-[17px] flex h-dvh flex-col overflow-hidden bg-[#F5F5F5]">
<StickyHeader sticky={false} className="shrink-0">
<div className="flex items-center justify-between gap-3">
{!hideBackButton ? (
<NavigationButton
variant="transparent"
icon="close"
iconLabel={t["Go back"]}
onClick={onClose}
/>
) : (
<div className="size-10 shrink-0" />
)}
<h1 className="min-w-0 flex-1 text-center group-16 font-bold text-white">
{t["New Match"]}
<h1
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white"
style={{ fontFamily: "'Segoe UI', sans-serif" }}
>
{t["More detail"]}
</h1>
<div className="size-10 shrink-0" />
</div>
</StickyHeader>
<section className="px-[17px] pb-32 pt-5">
<section className="flex-1 overflow-y-auto overflow-x-hidden px-[17px] pb-6 pt-5">
<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" />
@ -247,7 +253,7 @@ function NewMatchProfileSkeleton({
<div
style={{ paddingBottom: `var(--safe-bottom)` }}
className="fixed inset-x-0 bottom-0 z-30 px-[17px]"
className="shrink-0 z-30 px-[17px]"
>
<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">
@ -276,7 +282,11 @@ function formatBoldText(text: string) {
});
}
export default function NewMatchProfilePage() {
type NewMatchProfilePageProps = {
onClose?: () => void;
};
export default function NewMatchProfilePage({ onClose }: NewMatchProfilePageProps = {}) {
const { dictionary: t, locale } = useI18n();
const router = useRouter();
const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false);
@ -371,7 +381,12 @@ export default function NewMatchProfilePage() {
const isSubmitting = respondMutation.isPending;
if (isLoading || !profile || isRedirecting || isSubmitting) {
return <NewMatchProfileSkeleton hideBackButton={isSubmitting} />;
return (
<NewMatchProfileSkeleton
hideBackButton={isSubmitting}
onClose={onClose}
/>
);
}
const isAcceptProfileEnabled =
Boolean(caseId) &&
@ -383,7 +398,7 @@ export default function NewMatchProfilePage() {
const _firstName = nameParts[0] || "";
const _lastName = nameParts.slice(1).join(" ") || "";
const mainClass = "-mx-[17px] flex min-h-screen flex-col pb-10";
const mainClass = "-mx-[17px] flex h-dvh flex-col overflow-hidden";
const mainStyle = {
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`,
@ -583,12 +598,13 @@ export default function NewMatchProfilePage() {
) : null}
<main className={mainClass} style={mainStyle}>
<StickyHeader>
<StickyHeader sticky={false} className="shrink-0">
<div className="flex items-center justify-between gap-3">
<NavigationButton
variant="transparent"
icon="close"
iconLabel={t["Go back"]}
onClick={onClose}
/>
<h1
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white"
@ -600,7 +616,7 @@ export default function NewMatchProfilePage() {
</div>
</StickyHeader>
<section className="px-[17px] pb-32 pt-5">
<section className="flex-1 overflow-y-auto overflow-x-hidden px-[17px] pb-6 pt-5">
<div
className="mx-auto flex flex-col items-center justify-center"
style={{
@ -657,7 +673,7 @@ export default function NewMatchProfilePage() {
<div
style={{ paddingBottom: `var(--safe-bottom)` }}
className="fixed inset-x-0 bottom-0 z-30 px-[17px]"
className="shrink-0 z-30 px-[17px]"
>
<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)]">
{caseStatus === "payment_done" ||

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

@ -9,6 +9,9 @@ import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay";
import MatchProfileOverlay, {
useMatchProfileOverlay,
} from "@/components/Componentes/match-profile-overlay";
import CallResultSheet from "@/components/Componentes/call-result-sheet";
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet";
@ -179,6 +182,8 @@ export default function RequestAcceptedClient() {
const router = useRouter();
const { isAdvisorOpen, openAdvisors, closeAdvisors } =
useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } =
useMatchProfileOverlay();
const [isCallResultSheetOpen, setIsCallResultSheetOpen] = useState(false);
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] =
useState(false);
@ -522,7 +527,7 @@ export default function RequestAcceptedClient() {
) : null}
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(12px,calc(var(--safe-top)+4px))] pb-[calc(20px+var(--safe-bottom))] text-center">
<PageHeader className="-mx-[6px]" />
<PageHeader className="-mx-[6px]" profile={profile} />
<div className="flex flex-1 flex-col justify-between gap-6 pt-4">
<section className="flex flex-col items-center">
@ -616,11 +621,7 @@ export default function RequestAcceptedClient() {
<>
<button
type="button"
onClick={() =>
router.push(
localizePath("/new-match/profile", locale),
)
}
onClick={() => openProfile()}
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["View Profile"]}
@ -762,6 +763,11 @@ export default function RequestAcceptedClient() {
open={isAdvisorOpen}
onClose={closeAdvisors}
/>
<MatchProfileOverlay
open={isProfileOpen}
onClose={closeProfile}
/>
</>
);
}

23
src/app/request-sent/request-sent-client.tsx

@ -10,6 +10,9 @@ import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay";
import MatchProfileOverlay, {
useMatchProfileOverlay,
} from "@/components/Componentes/match-profile-overlay";
import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
@ -28,6 +31,8 @@ export default function RequestSentClient() {
const { dictionary: t, locale } = useI18n();
const { isAdvisorOpen, openAdvisors, closeAdvisors } =
useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } =
useMatchProfileOverlay();
const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000,
});
@ -80,7 +85,7 @@ export default function RequestSentClient() {
style={{ paddingBottom: "calc(40px + var(--safe-bottom))" }}
className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(12px,calc(var(--safe-top)+4px))] text-center"
>
<PageHeader className="-mx-[6px]" />
<PageHeader className="-mx-[6px]" profile={profile} />
<div className="flex flex-1 flex-col justify-between gap-20 pt-[109px]">
<section className="flex flex-col items-center">
@ -103,14 +108,15 @@ export default function RequestSentClient() {
{requestSentCopy.description}
</p>
<Link
href={localizePath("/new-match/profile", locale)}
className="mt-9 w-full max-w-[212px]"
<button
type="button"
onClick={openProfile}
className="mt-9 w-full max-w-[212px] cursor-pointer"
>
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C]">
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C] transition-transform active:scale-[0.98]">
{requestSentCopy.matchProfile}
</div>
</Link>
</button>
</section>
<div className="space-y-8 pb-20">
@ -149,6 +155,11 @@ export default function RequestSentClient() {
open={isAdvisorOpen}
onClose={closeAdvisors}
/>
<MatchProfileOverlay
open={isProfileOpen}
onClose={closeProfile}
/>
</>
);
}

25
src/components/Componentes/data-error-state.tsx

@ -11,6 +11,8 @@ type DataErrorStateProps = {
onRetry?: () => void;
/** Optional className for the outermost wrapper */
className?: string;
/** Render full-screen fixed overlay covering any headers/footers (default: true) */
fullScreen?: boolean;
};
/**
@ -30,6 +32,7 @@ export default function DataErrorState({
message,
onRetry,
className = "",
fullScreen = true,
}: DataErrorStateProps) {
const { dictionary: t } = useI18n();
@ -43,20 +46,22 @@ export default function DataErrorState({
"Something went wrong. Please check your internet connection and try again.";
const retryLabel = t["Try again"] ?? "Try again";
const containerClasses = fullScreen
? `fixed inset-0 z-[9999] flex h-[100dvh] w-full flex-col items-center justify-center bg-[#F5F5F5] px-6 text-center ${className}`
: `flex min-h-[60svh] w-full flex-col items-center justify-center gap-0 px-6 text-center ${className}`;
return (
<div
className={`flex min-h-[60svh] flex-col items-center justify-center gap-0 px-6 text-center ${className}`}
>
<div className={containerClasses}>
{/* ── Disconnected-plug illustration (inline SVG — no network request) ── */}
<DisconnectedIllustration />
{/* ── Title ── */}
<h2 className="mt-8 text-[18px] font-bold leading-snug text-[#1B1B1B]">
{/* ── Title (matching Flutter fontSize: 22, normal weight, #1B1B1B) ── */}
<h2 className="mt-10 text-[22px] font-normal leading-snug text-[#1B1B1B]">
{displayTitle}
</h2>
{/* ── Subtitle / message ── */}
<p className="mt-2 max-w-[280px] text-[13px] font-semibold leading-[1.55] text-[#8B8B8B]">
{/* ── Subtitle / message (matching Flutter fontSize: 14, fontWeight: 600, #8B8B8B) ── */}
<p className="mt-2 max-w-[320px] text-[14px] font-semibold leading-[1.4] text-[#8B8B8B]">
{displayMessage}
</p>
@ -65,11 +70,11 @@ export default function DataErrorState({
<button
type="button"
onClick={onRetry}
className="mt-6 flex items-center gap-2 rounded-[15px] bg-white px-5 py-2.5 text-[13px] font-semibold text-[#202227] shadow-[0_4px_16px_rgba(0,0,0,0.08)] transition-all duration-200 active:scale-95 hover:shadow-[0_6px_20px_rgba(0,0,0,0.12)]"
className="mt-6 inline-flex items-center gap-2 rounded-[15px] bg-white px-5 py-2.5 text-[14px] font-semibold text-[#202227] shadow-[0_4px_16px_rgba(0,0,0,0.08)] transition-all duration-200 active:scale-95 hover:shadow-[0_6px_20px_rgba(0,0,0,0.12)] cursor-pointer"
>
<svg
aria-hidden="true"
className="size-4"
className="size-4.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -80,7 +85,7 @@ export default function DataErrorState({
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
<path d="M21 4v5h-5" />
</svg>
{retryLabel}
<span>{retryLabel}</span>
</button>
)}
</div>

218
src/components/Componentes/discount-widget.tsx

@ -0,0 +1,218 @@
"use client";
import { useState } from "react";
import { useI18n } from "@/translations/provider";
import {
type CheckDiscountResult,
useValidateDiscountMutation,
} from "@/hooks/marriage/use-validate-discount";
export interface DiscountWidgetProps {
objectId: number | string;
service?: string;
onDiscountApplied?: (result: CheckDiscountResult) => void;
onDiscountCleared?: () => void;
className?: string;
}
export function DiscountWidget({
objectId,
service = "marriagesubscriptionplan",
onDiscountApplied,
onDiscountCleared,
className = "",
}: DiscountWidgetProps) {
const { dictionary: t } = useI18n();
const [isExpanded, setIsExpanded] = useState(false);
const [code, setCode] = useState("");
const [discountStatus, setDiscountStatus] = useState<
"initial" | "empty" | "loading" | "success" | "failed"
>("initial");
const [discountResult, setDiscountResult] =
useState<CheckDiscountResult | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const validateMutation = useValidateDiscountMutation();
const handleApply = async () => {
const trimmed = code.trim();
if (!trimmed) {
setDiscountStatus("empty");
setErrorMessage(t["This field is required"] || "This field is required");
return;
}
setDiscountStatus("loading");
setErrorMessage(null);
try {
const result = await validateMutation.mutateAsync({
code: trimmed,
objectId,
service,
});
if (result.valid) {
setDiscountStatus("success");
setDiscountResult(result);
setErrorMessage(null);
onDiscountApplied?.(result);
} else {
setDiscountStatus("failed");
setDiscountResult(null);
setErrorMessage(result.message || "Invalid discount code");
}
} catch (err: any) {
setDiscountStatus("failed");
setDiscountResult(null);
setErrorMessage(
err?.response?.data?.detail || err?.message || "Invalid discount code",
);
}
};
const handleClear = () => {
setCode("");
setDiscountStatus("initial");
setDiscountResult(null);
setErrorMessage(null);
onDiscountCleared?.();
};
const getBorderAndRing = () => {
switch (discountStatus) {
case "success":
return "border-[#00AC78] ring-1 ring-[#00AC78]";
case "failed":
case "empty":
return "border-[#F0445B] ring-1 ring-[#F0445B]";
default:
return "border-[#D0D5DD] hover:border-[#98A2B3] focus-within:border-[#F0445B] focus-within:ring-1 focus-within:ring-[#F0445B]";
}
};
return (
<div className={`w-full ${className}`}>
{/* ── Collapsible Header ── */}
<button
type="button"
onClick={() => setIsExpanded((prev) => !prev)}
className="flex w-full items-center justify-between py-3.5 text-left transition-colors cursor-pointer select-none"
>
<div className="flex items-center gap-3">
{/* Coupon icon */}
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`shrink-0 transition-colors ${
isExpanded ? "text-[#747474]" : "text-[#F0445B]"
}`}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.60714 3.21429C1.60714 2.94686 1.82314 2.73214 2.08671 2.73214H15.9133C16.1769 2.73214 16.3929 2.94686 16.3929 3.21429V5.91943C15.8137 6.19949 15.3252 6.6371 14.9834 7.18208C14.6416 7.72706 14.4603 8.35734 14.4603 9.00064C14.4603 9.64395 14.6416 10.2742 14.9834 10.8192C15.3252 11.3642 15.8137 11.8018 16.3929 12.0819V14.7857C16.393 14.8489 16.3808 14.9114 16.3567 14.9698C16.3327 15.0282 16.2975 15.0813 16.2529 15.1261C16.2084 15.1709 16.1555 15.2064 16.0972 15.2308C16.0389 15.2551 15.9764 15.2677 15.9133 15.2679H2.08671C2.02357 15.2677 1.96107 15.2551 1.9028 15.2308C1.84452 15.2064 1.79161 15.1709 1.74707 15.1261C1.70254 15.0813 1.66726 15.0282 1.64325 14.9698C1.61924 14.9114 1.60697 14.8489 1.60714 14.7857V12.0909C2.19152 11.8129 2.68514 11.3751 3.03076 10.828C3.37637 10.2809 3.55982 9.64709 3.55982 9C3.55982 8.3529 3.37637 7.71907 3.03076 7.172C2.68514 6.62493 2.19152 6.18705 1.60714 5.90914V3.21429ZM2.08671 1.125C0.933429 1.125 0 2.06229 0 3.21429V6.46714C0 6.83229 0.246857 7.15114 0.599143 7.24371C0.987519 7.34535 1.33125 7.57285 1.57657 7.89063C1.82189 8.20841 1.95496 8.59855 1.95496 9C1.95496 9.40145 1.82189 9.79159 1.57657 10.1094C1.33125 10.4271 0.987519 10.6546 0.599143 10.7563C0.427554 10.8014 0.275728 10.9021 0.167347 11.0426C0.0589657 11.183 0.000124048 11.3554 0 11.5329L0 14.7857C0 15.939 0.933429 16.875 2.08671 16.875H15.9133C17.0666 16.875 18 15.939 18 14.7857V11.5277C18 11.3514 17.9419 11.1799 17.8348 11.0398C17.7277 10.8997 17.5775 10.7987 17.4073 10.7524C17.0222 10.6483 16.6822 10.4202 16.4398 10.1034C16.1973 9.78667 16.066 9.39889 16.066 9C16.066 8.60111 16.1973 8.21333 16.4398 7.89658C16.6822 7.57983 17.0222 7.35174 17.4073 7.24757C17.5775 7.20129 17.7277 7.10032 17.8348 6.96021C17.9419 6.8201 18 6.64865 18 6.47229V3.21429C18 2.06229 17.0666 1.125 15.9133 1.125H2.08671ZM12.7826 5.21743C12.708 5.14279 12.6194 5.08358 12.5218 5.04318C12.4243 5.00279 12.3198 4.98199 12.2143 4.98199C12.1087 4.98199 12.0042 5.00279 11.9067 5.04318C11.8092 5.08358 11.7206 5.14279 11.646 5.21743L5.21743 11.646C5.1428 11.7206 5.0836 11.8092 5.04321 11.9067C5.00282 12.0042 4.98204 12.1087 4.98204 12.2143C4.98204 12.3198 5.00282 12.4243 5.04321 12.5218C5.0836 12.6193 5.1428 12.7079 5.21743 12.7826C5.29206 12.8572 5.38065 12.9164 5.47816 12.9568C5.57567 12.9972 5.68017 13.018 5.78571 13.018C5.89125 13.018 5.99576 12.9972 6.09327 12.9568C6.19077 12.9164 6.27937 12.8572 6.354 12.7826L12.7826 6.354C12.8572 6.27938 12.9164 6.19078 12.9568 6.09328C12.9972 5.99577 13.018 5.89126 13.018 5.78571C13.018 5.68017 12.9972 5.57566 12.9568 5.47815C12.9164 5.38064 12.8572 5.29205 12.7826 5.21743ZM4.98214 6.42857C4.98214 6.23862 5.01956 6.05054 5.09225 5.87505C5.16494 5.69956 5.27148 5.54011 5.40579 5.40579C5.54011 5.27148 5.69956 5.16494 5.87505 5.09225C6.05054 5.01956 6.23862 4.98214 6.42857 4.98214C6.61852 4.98214 6.80661 5.01956 6.9821 5.09225C7.15758 5.16494 7.31704 5.27148 7.45135 5.40579C7.58566 5.54011 7.69221 5.69956 7.7649 5.87505C7.83759 6.05054 7.875 6.23862 7.875 6.42857C7.875 6.81219 7.72261 7.18009 7.45135 7.45135C7.18009 7.72261 6.81219 7.875 6.42857 7.875C6.04495 7.875 5.67705 7.72261 5.40579 7.45135C5.13453 7.18009 4.98214 6.81219 4.98214 6.42857ZM11.5714 10.125C11.1878 10.125 10.8199 10.2774 10.5486 10.5486C10.2774 10.8199 10.125 11.1878 10.125 11.5714C10.125 11.955 10.2774 12.3229 10.5486 12.5942C10.8199 12.8655 11.1878 13.0179 11.5714 13.0179C11.955 13.0179 12.3229 12.8655 12.5942 12.5942C12.8655 12.3229 13.0179 11.955 13.0179 11.5714C13.0179 11.1878 12.8655 10.8199 12.5942 10.5486C12.3229 10.2774 11.955 10.125 11.5714 10.125Z"
fill="currentColor"
/>
</svg>
<span
className={`text-[14px] font-bold transition-colors ${
isExpanded ? "text-[#747474]" : "text-[#F0445B]"
}`}
>
{t["Do you have a discount code?"] ||
"Do you have a discount code?"}
</span>
</div>
{/* Arrow expand icon */}
<svg
aria-hidden="true"
className={`size-5 transition-transform duration-300 ${
isExpanded ? "rotate-180 text-[#747474]" : "rotate-0 text-[#F0445B]"
}`}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
{/* ── Expanded Form Body ── */}
{isExpanded && (
<div className="pt-1 pb-2 w-full">
<div
className={`flex h-[52px] w-full items-center rounded-[12px] border bg-white px-4 transition-all ${getBorderAndRing()}`}
>
<input
type="text"
dir="ltr"
value={code}
disabled={discountStatus === "loading" || discountResult !== null}
onChange={(e) => {
setCode(e.target.value);
if (discountStatus !== "initial") {
setDiscountStatus("initial");
setErrorMessage(null);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" && discountResult === null) {
e.preventDefault();
handleApply();
}
}}
placeholder={
t["Enter discount code..."] || "Enter discount code..."
}
className="h-full flex-1 bg-transparent text-[15px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3] disabled:opacity-70 min-w-0"
/>
{discountStatus === "loading" ? (
<div className="shrink-0 pl-3">
<div className="size-4.5 animate-spin rounded-full border-2 border-[#F0445B] border-t-transparent" />
</div>
) : discountResult !== null ? (
<button
type="button"
onClick={handleClear}
className="shrink-0 pl-3 text-[15px] font-semibold text-[#F0445B] hover:opacity-80 transition-opacity cursor-pointer"
>
{t["Clear"] || "Clear"}
</button>
) : (
<button
type="button"
onClick={handleApply}
className="shrink-0 pl-3 text-[15px] font-semibold text-[#F0445B] hover:opacity-80 transition-opacity cursor-pointer"
>
{t["Apply"] || "Apply"}
</button>
)}
</div>
{/* Status Message */}
{errorMessage && (
<p className="mt-2 text-start text-[13px] font-medium text-[#D44747] leading-normal">
{errorMessage}
</p>
)}
{discountStatus === "success" && discountResult?.message && (
<p className="mt-2 text-start text-[13px] font-medium text-[#00AC78] leading-normal">
{discountResult.message}
</p>
)}
</div>
)}
</div>
);
}

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

@ -5,6 +5,7 @@ import type { HTMLAttributes, ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import Button from "./button";
import { LoadingSkeleton } from "./loading-skeleton";
import { LoadingThreeDot } from "./loading-three-dot";
import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220;
@ -75,14 +76,14 @@ const ICON_PRESETS: Record<
coin: {
src: "/assets/images/Inner Plugdsain Iframe.svg",
alt: "Coin",
width: 50,
height: 50,
width: 56,
height: 56,
},
"coin.svg": {
src: "/assets/images/Inner Plugdsain Iframe.svg",
alt: "Coin",
width: 50,
height: 50,
width: 56,
height: 56,
},
check: {
src: "/assets/images/Vectofdasr.svg",
@ -206,9 +207,7 @@ export function InformationSheet({
<div
className={[
"fixed inset-0 z-50 flex items-end justify-center transition-all duration-[220ms] animate-in fade-in",
isClosing
? "bg-[#171717]/0 opacity-0"
: "bg-[#171717]/55 opacity-100",
isClosing ? "bg-[#171717]/0 opacity-0" : "bg-[#171717]/55 opacity-100",
]
.filter(Boolean)
.join(" ")}
@ -236,7 +235,7 @@ export function InformationSheet({
<section
{...props}
className={[
"w-full sm:max-w-[375px] 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 animate-in slide-in-from-bottom",
"w-full sm:max-w-[375px] rounded-t-[22px] bg-[#F9F8F8] p-4 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform animate-in slide-in-from-bottom",
isClosing ? "translate-y-full" : "translate-y-0",
className,
]
@ -245,17 +244,9 @@ export function InformationSheet({
>
<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>
</>
<div className="flex h-[280px] min-h-[280px] w-full items-center justify-center">
<LoadingThreeDot className="text-[#FF4E67]" />
</div>
) : (
<>
{resolvedIcon ? (

140
src/components/Componentes/match-profile-overlay.test.tsx

@ -0,0 +1,140 @@
import { act, cleanup, render, renderHook, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nProvider } from "@/translations/provider";
import {
MatchProfileOverlay,
useMatchProfileOverlay,
} from "./match-profile-overlay";
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}),
}));
vi.mock("@/hooks/marriage/use-profile-main", () => ({
useMarriageProfileQuery: () => ({
data: {
status: "matched",
gender: "female",
active_case: { case_id: "test-case-1", status: "introduced" },
match_summary: {
gender: "male",
public_info: [{ key: "full_name", label: "نام", value: "علی احمدی" }],
},
},
isLoading: false,
refetch: vi.fn(),
}),
}));
vi.mock("@/hooks/marriage/use-case-respond", () => ({
useRespondToMarriageCaseMutation: () => ({
mutateAsync: vi.fn(),
isPending: false,
}),
}));
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<I18nProvider locale="fa">{children}</I18nProvider>
</QueryClientProvider>
);
};
}
describe("MatchProfileOverlay & useMatchProfileOverlay", () => {
beforeEach(() => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
return setTimeout(() => cb(Date.now()), 0);
});
vi.stubGlobal("cancelAnimationFrame", (id: number) => {
clearTimeout(id);
});
document.body.className = "";
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
vi.useRealTimers();
document.body.className = "";
});
it("hook manages open and close state with history sync", () => {
const { result } = renderHook(() => useMatchProfileOverlay());
expect(result.current.isProfileOpen).toBe(false);
act(() => {
result.current.openProfile();
});
expect(result.current.isProfileOpen).toBe(true);
act(() => {
result.current.closeProfile();
});
expect(result.current.isProfileOpen).toBe(false);
});
it("renders overlay dialog and profile page when open is true", () => {
const handleClose = vi.fn();
const wrapper = createWrapper();
render(
<MatchProfileOverlay open={true} onClose={handleClose} />,
{ wrapper },
);
act(() => {
vi.advanceTimersByTime(16);
});
const overlay = screen.getByRole("dialog");
expect(overlay).toBeInTheDocument();
expect(overlay).toHaveClass("section-overlay");
expect(document.body.classList.contains("section-overlay-open")).toBe(true);
});
it("handles closing animation and removes body class when open becomes false", () => {
const handleClose = vi.fn();
const wrapper = createWrapper();
const { rerender } = render(
<MatchProfileOverlay open={true} onClose={handleClose} />,
{ wrapper },
);
act(() => {
vi.advanceTimersByTime(16);
});
expect(screen.getByRole("dialog")).toHaveAttribute("data-state", "open");
expect(document.body.classList.contains("section-overlay-open")).toBe(true);
rerender(<MatchProfileOverlay open={false} onClose={handleClose} />);
expect(screen.getByRole("dialog")).toHaveAttribute("data-state", "closing");
expect(document.body.classList.contains("section-overlay-open")).toBe(false);
act(() => {
vi.advanceTimersByTime(210);
});
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
});

77
src/components/Componentes/match-profile-overlay.tsx

@ -0,0 +1,77 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import NewMatchProfilePage from "@/app/new-match/profile/page";
import SectionOverlayHost from "@/components/Componentes/section-overlay-host";
import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
export type MatchProfileOverlayProps = {
open: boolean;
onClose: () => void;
};
/**
* Hook to manage Match Profile ("More detail") slide-in overlay state.
* Syncs with browser history (?profile=open) and intercepts hardware back in Flutter.
*/
export function useMatchProfileOverlay() {
const [isProfileOpen, setIsProfileOpen] = useState(false);
useEffect(() => {
const readProfileFromUrl = () => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
setIsProfileOpen(params.get("profile") === "open");
};
readProfileFromUrl();
window.addEventListener("popstate", readProfileFromUrl);
return () => window.removeEventListener("popstate", readProfileFromUrl);
}, []);
const openProfile = useCallback(() => {
setIsProfileOpen(true);
if (typeof window !== "undefined") {
const url = new URL(window.location.href);
url.searchParams.set("profile", "open");
window.history.pushState({ profile: "open" }, "", url.toString());
}
}, []);
const closeProfile = useCallback(() => {
if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search);
if (params.get("profile") === "open") {
setIsProfileOpen(false);
window.history.back();
return;
}
}
setIsProfileOpen(false);
}, []);
// Intercept hardware back in Flutter WebView when profile overlay is open
useHardwareBackHandler(() => {
closeProfile();
return true; // handled: keep WebView screen open
}, isProfileOpen);
return {
isProfileOpen,
openProfile,
closeProfile,
};
}
export function MatchProfileOverlay({
open,
onClose,
}: MatchProfileOverlayProps) {
return (
<SectionOverlayHost open={open} onClose={onClose}>
<NewMatchProfilePage onClose={onClose} />
</SectionOverlayHost>
);
}
export default MatchProfileOverlay;

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

@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
import { GoArrowLeft } from "react-icons/go";
import { HiEllipsisVertical } from "react-icons/hi2";
import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import { useHabcoinInventoryQuery } from "@/hooks/marriage/use-habcoin-inventory";
import {
extractHabcoinPaymentUrl,
useHabcoinPaymentMutation,
@ -18,6 +19,8 @@ import {
isInFlutterWebView,
} from "@/lib/webview-actions";
import { useI18n } from "@/translations/provider";
import { DiscountWidget } from "./discount-widget";
import type { CheckDiscountResult } from "@/hooks/marriage/use-validate-discount";
import ErrorToast from "./error-toast";
import HelpModal from "./help-modal";
import InformationSheet from "./information-sheet";
@ -47,6 +50,7 @@ export type NavigationButtonProps = Omit<
helpDescription?: ReactNode;
helpButtonText?: ReactNode;
disableHelpModal?: boolean;
profile?: any;
};
export function NavigationButton({
@ -59,6 +63,7 @@ export function NavigationButton({
helpDescription,
helpButtonText,
disableHelpModal = false,
profile: profileProp,
...props
}: NavigationButtonProps) {
const router = useRouter();
@ -70,16 +75,19 @@ export function NavigationButton({
const [isSubscriptionLoading, setIsSubscriptionLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Only fetch profile for icons that actually need profile data.
// Simple icons like back, close, info, document don't need it.
// Only fetch profile for icons that actually need profile data if not provided via prop.
const needsProfile =
icon === "support" ||
icon === "subscription" ||
icon === "more" ||
icon === "consultation";
const { data: profile, refetch } = useMarriageProfileQuery({
enabled: needsProfile,
});
!profileProp &&
(icon === "support" ||
icon === "subscription" ||
icon === "more" ||
icon === "consultation");
const { data: queriedProfile, refetch: refetchQuery } =
useMarriageProfileQuery({
enabled: needsProfile,
});
const profile = profileProp !== undefined ? profileProp : queriedProfile;
const refetch = refetchQuery;
const paymentMutation = useHabcoinPaymentMutation();
const queryClient = useQueryClient();
const [toastMessage, setToastMessage] = useState<string | null>(null);
@ -88,137 +96,214 @@ export function NavigationButton({
);
const [isRenewing, setIsRenewing] = useState(false);
const [isInsufficientCoins, setIsInsufficientCoins] = useState(false);
const [appliedDiscount, setAppliedDiscount] =
useState<CheckDiscountResult | null>(null);
const { data: inventory } = useHabcoinInventoryQuery({
enabled: isSubscriptionInfoOpen,
});
const planPrice = Number(profile?.recommended_plan?.price) || 0;
const finalPrice = appliedDiscount?.valid
? appliedDiscount.discountedPrice
: planPrice;
const coinBalance = inventory?.coin_balance ?? 0;
const hasEnoughCoins =
!isInsufficientCoins &&
(inventory === undefined ? true : coinBalance >= finalPrice);
const handleOpenSubscriptionModal = () => {
setIsSubscriptionInfoOpen(true);
if (!profile) {
setIsSubscriptionLoading(true);
refetch().finally(() => {
setIsSubscriptionLoading(false);
});
}
};
const renderSubscriptionModal = () => {
if (!isSubscriptionInfoOpen) return null;
if (isSubscriptionLoading) {
if (isSubscriptionLoading && !profile) {
return (
<InformationSheet
isLoading={true}
onClose={() => setIsSubscriptionInfoOpen(false)}
onClose={() => {
setIsSubscriptionInfoOpen(false);
setAppliedDiscount(null);
}}
/>
);
}
const sub = profile?.active_subscription;
const planType = sub?.plan?.plan_type || "time_based";
const remainingDays = sub?.remaining_days ?? 0;
const usageLimit = sub?.plan?.usage_limit ?? 0;
const totalUsages = sub?.total_usages ?? 0;
const remainingUsages = Math.max(0, usageLimit - totalUsages);
let subscriptionDescription = "";
if (planType === "count_based") {
subscriptionDescription = (
t["{count} case introductions remaining of your subscription."] ||
"{count} case introductions remaining of your subscription."
).replace("{count}", String(remainingUsages));
} else if (planType === "combined") {
subscriptionDescription = (
t[
"{days} days and {count} case introductions remaining of your subscription."
] ||
"{days} days and {count} case introductions remaining of your subscription."
)
.replace("{days}", String(remainingDays))
.replace("{count}", String(remainingUsages));
} else {
subscriptionDescription = (
t["{days} days remaining of your subscription."] ||
"{days} days remaining of your subscription."
).replace("{days}", String(remainingDays));
}
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)}
description={subscriptionDescription}
onClose={() => {
setIsSubscriptionInfoOpen(false);
setAppliedDiscount(null);
}}
buttons={({ close }) => (
<div className="space-y-3">
{isInsufficientCoins && isInFlutterWebView() && (
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left w-full"
onClick={() => buyHabibCoinPackages()}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[18px] bg-[#00AC78] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(0,172,120,0.28)] transition-opacity active:opacity-90">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={18}
height={18}
className="shrink-0"
/>
<span className="truncate">
{t["Buy Habib Coins"] || "Buy Habib Coins"}
</span>
</div>
</button>
)}
{profile?.recommended_plan?.id ? (
<DiscountWidget
objectId={profile.recommended_plan.id}
onDiscountApplied={(res) => setAppliedDiscount(res)}
onDiscountCleared={() => setAppliedDiscount(null)}
/>
) : null}
<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">
<div className="inline-flex w-full items-center justify-center rounded-[11px] border border-[#747474] bg-transparent px-2 h-[52px] text-[16px] font-semibold text-[#747474] 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");
setIsInsufficientCoins(false);
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 || "";
{!hasEnoughCoins ? (
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={() => buyHabibCoinPackages()}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] 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["Buy Habib Coins"] || "Buy Habib Coins"}
</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");
if (
errMessage.includes("Not enough coins") ||
err?.response?.status === 400
) {
setIsInsufficientCoins(true);
setIsInsufficientCoins(false);
try {
const paymentResponse =
await paymentMutation.mutateAsync({
objectId: planId,
discountCode: appliedDiscount?.valid
? appliedDiscount.code
: undefined,
});
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);
setAppliedDiscount(null);
setToastVariant("success");
setToastMessage(
t[
"Insufficient coin balance. Please recharge your account."
] ||
"Insufficient coin balance. Please recharge your account.",
t["Payment successful"] || "Payment successful",
);
} else {
setToastMessage(errMessage || "Payment failed");
} 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
) {
setIsInsufficientCoins(true);
setToastMessage(
t[
"Insufficient coin balance. Please recharge your account."
] ||
"Insufficient coin balance. Please recharge your account.",
);
} else {
setToastMessage(errMessage || "Payment failed");
}
} finally {
setIsRenewing(false);
}
} 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>
}}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] 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>
)}
</div>
</button>
{!isRenewing && (
<span className="inline-flex items-center gap-1.5 rounded-[12px] bg-black/15 px-[9px] py-[4px] text-xs font-semibold leading-none text-white shrink-0 min-w-0 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={16}
height={16}
className="shrink-0"
/>
{appliedDiscount?.valid ? (
<>
<span className="line-through text-white/50 text-[11px]">
{planPrice}
</span>
<span className="font-bold text-white text-[12px]">
{finalPrice}
</span>
</>
) : (
<span className="truncate">
{planPrice || "100"}
</span>
)}
</span>
)}
</div>
</button>
)}
</div>
</div>
)}
@ -240,7 +325,10 @@ export function NavigationButton({
};
const isFemale = profile?.gender === "female";
const hasActiveSubscription = !!profile?.active_subscription;
const hasActiveSubscription =
!!profile?.active_subscription &&
profile.active_subscription.is_active !== false &&
profile.active_subscription.is_valid !== false;
useEffect(() => {
if (!isDropdownOpen) return;
@ -289,9 +377,10 @@ export function NavigationButton({
src="/assets/images/diamond-color.png"
alt=""
aria-hidden="true"
className="size-6"
className="size-6 object-contain"
width={24}
height={24}
unoptimized
/>
) : (
<UiIcon name="diamond" aria-hidden="true" className="size-6" />
@ -371,11 +460,7 @@ export function NavigationButton({
type="button"
onClick={() => {
setIsDropdownOpen(false);
setIsSubscriptionInfoOpen(true);
setIsSubscriptionLoading(true);
refetch().finally(() => {
setIsSubscriptionLoading(false);
});
handleOpenSubscriptionModal();
}}
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"
>
@ -443,11 +528,7 @@ export function NavigationButton({
} else if (icon === "support") {
setIsSupportOpen(true);
} else if (icon === "subscription") {
setIsSubscriptionInfoOpen(true);
setIsSubscriptionLoading(true);
refetch().finally(() => {
setIsSubscriptionLoading(false);
});
handleOpenSubscriptionModal();
}
}
}

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

@ -21,6 +21,7 @@ type PageHeaderProps = {
* Defaults to true.
*/
enableProfileQuery?: boolean;
profile?: any;
};
export function PageHeader({
@ -28,6 +29,7 @@ export function PageHeader({
leftButton,
rightButton,
enableProfileQuery = true,
profile: profileProp,
}: PageHeaderProps) {
const { dictionary: t } = useI18n();
@ -36,27 +38,21 @@ export function PageHeader({
// for the right button can set enableProfileQuery=false to avoid an
// unnecessary API call (e.g. anonymous Intro).
const iconFromProp = rightButton?.icon;
const isCustomRightButton =
!!rightButton?.onClick ||
(iconFromProp !== undefined &&
iconFromProp !== "subscription" &&
iconFromProp !== "support");
const needsProfile =
enableProfileQuery &&
!rightButton?.onClick &&
(iconFromProp === undefined || iconFromProp === "subscription");
enableProfileQuery && !rightButton?.onClick && !profileProp;
const { data: profile } = useMarriageProfileQuery({
const { data: queriedProfile } = useMarriageProfileQuery({
enabled: needsProfile,
});
const isDefaultOrSubscription =
iconFromProp === undefined || iconFromProp === "subscription";
let finalIcon: NavigationButtonProps["icon"] = iconFromProp || "support";
if (isDefaultOrSubscription && needsProfile) {
if (hasSupportAccess(profile)) {
finalIcon = "support";
} else if (profile?.gender === "male") {
finalIcon = "subscription";
}
}
const profile = profileProp !== undefined ? profileProp : queriedProfile;
const isMale = profile?.gender === "male";
const { icon: rightButtonIcon, ...rightButtonRest } = rightButton || {};
return (
@ -68,16 +64,41 @@ export function PageHeader({
{leftButton?.className?.includes("hidden") ? (
<div className="size-10 shrink-0" />
) : (
<NavigationButton icon="back" {...leftButton} />
<NavigationButton icon="back" profile={profile} {...leftButton} />
)}
<h1 className="font-faminela text-[20px] font-normal text-foreground">
{t["Habib Marriage"]}
</h1>
<NavigationButton
icon={finalIcon}
{...(finalIcon === "support" ? { iconLabel: t["Support"] } : {})}
{...rightButtonRest}
/>
{isCustomRightButton ? (
<NavigationButton
icon={iconFromProp || "support"}
profile={profile}
{...(iconFromProp === "support" ? { iconLabel: t["Support"] } : {})}
{...rightButtonRest}
/>
) : isMale ? (
<div className="flex items-center gap-2">
<NavigationButton
icon="subscription"
profile={profile}
iconLabel={t["Subscription Status"] || "Subscription"}
/>
<NavigationButton
icon="support"
profile={profile}
iconLabel={t["Support"]}
{...rightButtonRest}
/>
</div>
) : (
<NavigationButton
icon="support"
profile={profile}
iconLabel={t["Support"]}
{...rightButtonRest}
/>
)}
</header>
);
}

4
src/components/Componentes/sticky-header.tsx

@ -26,11 +26,11 @@ export default function StickyHeader({
<header
style={
sticky
? { top: `-${top}px`, paddingTop: `${paddingTop}px` }
? { top: "0px", paddingTop: `${paddingTop}px` }
: { paddingTop: `${paddingTop}px` }
}
className={[
sticky ? "sticky z-30" : "relative z-30",
sticky ? "sticky top-0 z-30" : "relative z-30",
"rounded-b-[15px] bg-[linear-gradient(135deg,#E03950_0%,#FE6F82_100%)] px-[17px] pb-3",
className,
]

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

@ -1,6 +1,8 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { useHabcoinInventoryQuery } from "@/hooks/marriage/use-habcoin-inventory";
import { DiscountWidget } from "./discount-widget";
import type { CheckDiscountResult } from "@/hooks/marriage/use-validate-discount";
import {
buyHabibCoinPackages,
isInFlutterWebView,
@ -11,12 +13,14 @@ import { LoadingThreeDot } from "./loading-three-dot";
type SubscriptionRequiredSheetProps = {
onClose: () => void;
onPayment: () => void;
onPayment: (discountCode?: string) => void;
isPaymentPending?: boolean;
/** Payment error to display above the buttons (e.g. "Not enough coins"). */
errorMessage?: string | null;
/** Show the "Buy Habib Coins" button (Flutter opens its packages page). */
showBuyCoins?: boolean;
price?: number;
planId?: number | string;
};
export function SubscriptionRequiredSheet({
@ -25,111 +29,151 @@ export function SubscriptionRequiredSheet({
isPaymentPending = false,
errorMessage = null,
showBuyCoins = false,
price = 50,
planId,
}: SubscriptionRequiredSheetProps) {
const { dictionary: t } = useI18n();
const [appliedDiscount, setAppliedDiscount] =
useState<CheckDiscountResult | null>(null);
const { data: inventory, isLoading: isInventoryLoading } =
useHabcoinInventoryQuery();
const finalPrice = appliedDiscount?.valid
? appliedDiscount.discountedPrice
: price;
const balance = inventory?.coin_balance ?? 0;
const hasEnoughCoins =
!showBuyCoins && (inventory === undefined ? true : balance >= finalPrice);
if (isInventoryLoading) {
return <InformationSheet isLoading={true} onClose={onClose} />;
}
return (
<InformationSheet
icon="coin"
title={t["Subscription required"] || "Subscription required"}
title={
!hasEnoughCoins ? (
<span className="text-[#BD3F3F]">
{t["You do not have enough Habib Coins"] ||
"You do not have enough Habib Coins"}
</span>
) : (
t["Subscription required"] || "Subscription required"
)
}
description={
<div className="space-y-4 text-left">
<p className="text-sm text-center leading-[1.45] font-medium text-[#2B2B2B]">
!hasEnoughCoins ? (
<p className="text-[14px] text-[#4D4D4D] leading-[1.3] font-normal text-center">
{t[
"To view profiles, you need a subscription. Each subscription includes up to {count} matches."
]?.replace("{count}", "3") || (
<>
To view profiles, you need a subscription. Each subscription
includes up to{" "}
<span className="font-bold underline decoration-[1.5px] underline-offset-2">
3
</span>{" "}
matches.
</>
)}
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content"
] ||
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content"}
</p>
<div className="rounded-[12px] border border-[#FF4F67] bg-[#FFECEF] px-3.5 py-2.5 text-center">
<p className="text-xs leading-[1.45] font-semibold text-[#FF4F67]">
) : (
<div className="space-y-4 text-left">
<p className="text-sm text-center leading-[1.45] font-medium text-[#2B2B2B]">
{t[
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users"
] ||
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users"}
"To view profiles, you need a subscription. Each subscription includes up to {count} matches."
]?.replace("{count}", "3") || (
<>
To view profiles, you need a subscription. Each subscription
includes up to{" "}
<span className="font-bold underline decoration-[1.5px] underline-offset-2">
3
</span>{" "}
matches.
</>
)}
</p>
<div className="rounded-[12px] border border-[#FF4F67] bg-[#FFECEF] px-3.5 py-2.5 text-center">
<p className="text-xs leading-[1.45] font-semibold text-[#FF4F67]">
{t[
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users"
] ||
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users"}
</p>
</div>
</div>
</div>
)
}
onClose={onClose}
buttons={({ close }) => (
<div className="space-y-3">
{planId ? (
<DiscountWidget
objectId={planId}
onDiscountApplied={(res) => setAppliedDiscount(res)}
onDiscountCleared={() => setAppliedDiscount(null)}
/>
) : null}
{errorMessage && (
<div className="w-full bg-[#FEF2F2] text-[#B91C1C] text-xs p-3 rounded-xl text-center font-medium">
<div className="w-full bg-[#FEF2F2] text-[#B91C1C] text-xs p-3 rounded-[11px] text-center font-medium">
{errorMessage}
</div>
)}
{showBuyCoins && isInFlutterWebView() && (
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left w-full"
onClick={() => buyHabibCoinPackages()}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[18px] bg-[#00AC78] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(0,172,120,0.28)] transition-opacity active:opacity-90">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={18}
height={18}
className="shrink-0"
/>
<span className="truncate">
{t["Buy Habib Coins"] || "Buy Habib Coins"}
</span>
</div>
</button>
)}
<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">
<div className="inline-flex w-full items-center justify-center rounded-[11px] border border-[#747474] bg-transparent px-2 h-[52px] text-[16px] font-semibold text-[#747474] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t["Back"] || "Back"}</span>
</div>
</button>
<button
type="button"
disabled={isPaymentPending}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={onPayment}
>
<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">
{isPaymentPending ? (
<LoadingThreeDot />
) : (
<>
<span className="truncate">
{t["Payment"] || "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)] 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">50</span>
</span>
</>
)}
</div>
</button>
{!hasEnoughCoins ? (
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={() => buyHabibCoinPackages()}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] 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["Buy Habib Coins"] || "Buy Habib Coins"}
</span>
</div>
</button>
) : (
<button
type="button"
disabled={isPaymentPending}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={() =>
onPayment(
appliedDiscount?.valid ? appliedDiscount.code : undefined,
)
}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] 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">
{isPaymentPending ? (
<LoadingThreeDot />
) : (
<>
<span className="truncate">
{t["Payment"] || "Payment"}
</span>
<span className="inline-flex items-center gap-1 rounded-[12px] bg-black/15 px-[9px] py-[4px] text-xs font-semibold leading-none text-white shrink-0 min-w-0 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={16}
height={16}
className="shrink-0"
/>
<span className="truncate">{finalPrice}</span>
</span>
</>
)}
</div>
</button>
)}
</div>
</div>
)}

19
src/components/Componentes/support-access.ts

@ -1,23 +1,8 @@
import type { MarriageProfile } from "@/hooks/marriage/types";
/**
* Support is unavailable until a user has finished their profile. Women gain
* access on the waiting screen; men additionally need a currently valid
* subscription.
* Support is accessible for users across all main screens.
*/
export function hasSupportAccess(profile?: MarriageProfile): boolean {
if (!profile || !["waiting", "in_case", "matched"].includes(profile.status)) {
return false;
}
if (profile.gender === "female") {
return true;
}
const subscription = profile.active_subscription;
return (
profile.gender === "male" &&
subscription?.is_active === true &&
subscription.is_valid === true
);
return true;
}

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

@ -6,6 +6,9 @@ import { MdOutlineSwitchAccount } from "react-icons/md";
import { getClientCookie, setClientCookie } from "@/lib/cookies";
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache";
// ─────────────────────────────────────────────────────────────────────────────
// ۱. توکن‌های ثابت سیستم (System Preset Tokens)
// ─────────────────────────────────────────────────────────────────────────────
export const FEMALE_TOKEN = "545f61bb3e061ccb9b19f84715eb1b1ed4740331";
export const MALE_TOKEN = "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308";
export const MALE_2_TOKEN = "56cd0fceb93f7cecf7ffa84c590135026cf54a1d";
@ -15,32 +18,130 @@ export const MALE_EMAIL = "muhammadamin.ghorbani@gmail.com";
export const MALE_2_EMAIL = "habibwabackup@gmail.com";
const TOKEN_COOKIE_NAME = "HABIB_TOKEN";
const DEV_CUSTOM_TOKENS_STORAGE_KEY = "HABIB_DEV_CUSTOM_TOKENS_V1";
const DEV_ACTIVE_NAME_STORAGE_KEY = "HABIB_DEV_ACTIVE_NAME_V1";
const DEV_ACTIVE_EMOJI_STORAGE_KEY = "HABIB_DEV_ACTIVE_EMOJI_V1";
type WindowWithHabibToken = Window & {
HABIB_TOKEN?: string;
};
export interface DevAccountItem {
id?: number | null;
token: string;
title: string;
subtitle?: string;
emoji?: string;
isCustom?: boolean;
}
// ─────────────────────────────────────────────────────────────────────────────
// ۲. لیست حساب‌های پیش‌فرض و Inline درون کد:
// هر توسعه‌دهنده می‌تواند توکن‌های تستی جدید خود را همراه با نام دلخواه به این آرایه اضافه کند.
// ─────────────────────────────────────────────────────────────────────────────
export const PRESET_DEV_ACCOUNTS: DevAccountItem[] = [
{
id: 17119,
token: MALE_TOKEN,
title: "حساب آقا",
subtitle: MALE_EMAIL,
emoji: "👨",
},
{
id: 146024,
token: MALE_2_TOKEN,
title: "حساب آقا ۲",
subtitle: MALE_2_EMAIL,
emoji: "👨",
},
{
id: 147714,
token: FEMALE_TOKEN,
title: "حساب خانم",
subtitle: FEMALE_EMAIL,
emoji: "👩",
},
// نمونه افزودن توکن جدید Inline در کد:
// {
// token: "YOUR_DEV_TOKEN_HERE",
// title: "حساب تست طلاق",
// subtitle: "تست مرحله ۳",
// emoji: "🧪",
// },
];
const AVAILABLE_EMOJIS = ["🔑", "👨", "👩", "👤", "🧪", "⚡", "🎯", "🚀"];
function normalizeToken(token?: string | null): string {
if (!token) return "";
let clean = decodeURIComponent(token).trim();
if (clean.startsWith('"') && clean.endsWith('"')) {
clean = clean.slice(1, -1).trim();
}
if (clean.toLowerCase().startsWith("token ")) {
clean = clean.slice(6).trim();
}
return clean;
}
type TokenSwitcherProps = {
variant?: "default" | "transparent";
className?: string;
inlineAccounts?: DevAccountItem[];
};
export function TokenSwitcher({
variant = "default",
className,
inlineAccounts = [],
}: TokenSwitcherProps) {
const [mounted, setMounted] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [currentToken, setCurrentToken] = useState<string>("");
const [activeStoredName, setActiveStoredName] = useState<string>("");
const [activeStoredEmoji, setActiveStoredEmoji] = useState<string>("");
const [savedCustomAccounts, setSavedCustomAccounts] = useState<
DevAccountItem[]
>([]);
const [customTitleInput, setCustomTitleInput] = useState<string>("");
const [customTokenInput, setCustomTokenInput] = useState<string>("");
const [isCustomInputOpen, setIsCustomInputOpen] = useState(false);
const [selectedEmoji, setSelectedEmoji] = useState<string>("🔑");
useEffect(() => {
setMounted(true);
if (typeof window !== "undefined") {
const token =
(window as any).HABIB_TOKEN ??
const win = window as WindowWithHabibToken;
const rawToken =
win.HABIB_TOKEN ??
getClientCookie(TOKEN_COOKIE_NAME) ??
getClientCookie("habib_token") ??
sessionStorage.getItem(TOKEN_COOKIE_NAME) ??
"";
setCurrentToken(token);
const norm = normalizeToken(rawToken);
setCurrentToken(norm);
try {
const savedName = window.localStorage.getItem(
DEV_ACTIVE_NAME_STORAGE_KEY,
);
const savedEmoji = window.localStorage.getItem(
DEV_ACTIVE_EMOJI_STORAGE_KEY,
);
if (savedName) setActiveStoredName(savedName);
if (savedEmoji) setActiveStoredEmoji(savedEmoji);
const saved = window.localStorage.getItem(
DEV_CUSTOM_TOKENS_STORAGE_KEY,
);
if (saved) {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed)) {
setSavedCustomAccounts(parsed);
}
}
} catch (e) {
console.error("Failed to load dev saved custom accounts:", e);
}
}
}, []);
@ -48,60 +149,169 @@ export function TokenSwitcher({
return null;
}
const isMale = currentToken === MALE_TOKEN;
const isMale2 = currentToken === MALE_2_TOKEN;
const isFemale = currentToken === FEMALE_TOKEN;
const isNoToken = currentToken === "NO_TOKEN" || !currentToken;
const isCustomToken =
!isMale && !isMale2 && !isFemale && !isNoToken;
const handleSelectToken = (newToken: string) => {
const trimmed = newToken.trim();
if (!trimmed) return;
const persistCustomAccounts = (accounts: DevAccountItem[]) => {
setSavedCustomAccounts(accounts);
if (typeof window !== "undefined") {
try {
window.localStorage.clear();
window.sessionStorage.clear();
window.localStorage.setItem(
DEV_CUSTOM_TOKENS_STORAGE_KEY,
JSON.stringify(accounts),
);
} catch (e) {
console.error("Failed to clear storage", e);
console.warn("Failed to persist custom accounts:", e);
}
}
};
const safeClearStorage = (keepName?: string, keepEmoji?: string) => {
if (typeof window === "undefined") return;
try {
const savedTokens = window.localStorage.getItem(
DEV_CUSTOM_TOKENS_STORAGE_KEY,
);
window.localStorage.clear();
window.sessionStorage.clear();
if (savedTokens) {
window.localStorage.setItem(DEV_CUSTOM_TOKENS_STORAGE_KEY, savedTokens);
}
if (keepName) {
window.localStorage.setItem(DEV_ACTIVE_NAME_STORAGE_KEY, keepName);
}
if (keepEmoji) {
window.localStorage.setItem(DEV_ACTIVE_EMOJI_STORAGE_KEY, keepEmoji);
}
} catch (e) {
console.error("Failed to clear storage safely:", e);
}
};
const allAccounts: DevAccountItem[] = [
...PRESET_DEV_ACCOUNTS,
...inlineAccounts,
...savedCustomAccounts,
];
const normCurrent = normalizeToken(currentToken);
const activeAccount = allAccounts.find(
(acc) => normalizeToken(acc.token) === normCurrent,
);
const isNoToken =
normCurrent === "NO_TOKEN" || !normCurrent || normCurrent === "";
const handleSelectToken = (
newToken: string,
accountTitle?: string,
accountEmoji?: string,
) => {
const cleanToken = normalizeToken(newToken);
if (!cleanToken && cleanToken !== "NO_TOKEN") return;
// Find account info if title was not explicitly passed
const targetAccount = allAccounts.find(
(acc) => normalizeToken(acc.token) === cleanToken,
);
const resolvedTitle =
accountTitle ||
(cleanToken === "NO_TOKEN"
? "کاربر بدون توکن"
: targetAccount?.title || "حساب سفارشی");
const resolvedEmoji =
accountEmoji ||
(cleanToken === "NO_TOKEN" ? "👤" : targetAccount?.emoji || "🔑");
if (typeof window !== "undefined") {
safeClearStorage(resolvedTitle, resolvedEmoji);
setCachedMarriageEntryPath(null);
setClientCookie(TOKEN_COOKIE_NAME, trimmed);
setClientCookie("habib_token", trimmed);
setClientCookie(TOKEN_COOKIE_NAME, cleanToken);
setClientCookie("habib_token", cleanToken);
try {
sessionStorage.setItem(TOKEN_COOKIE_NAME, trimmed);
sessionStorage.setItem(TOKEN_COOKIE_NAME, cleanToken);
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
(window as any).HABIB_TOKEN = trimmed;
setCurrentToken(trimmed);
(window as WindowWithHabibToken).HABIB_TOKEN = cleanToken;
setCurrentToken(cleanToken);
setActiveStoredName(resolvedTitle);
setActiveStoredEmoji(resolvedEmoji);
setIsOpen(false);
window.location.replace("/");
}
};
const handleAddAndSelectAccount = (e?: React.FormEvent) => {
if (e) e.preventDefault();
const cleanToken = normalizeToken(customTokenInput);
if (!cleanToken) return;
const cleanTitle =
customTitleInput.trim() || `حساب سفارشی (${cleanToken.slice(0, 6)}...)`;
const newAccount: DevAccountItem = {
token: cleanToken,
title: cleanTitle,
emoji: selectedEmoji,
isCustom: true,
};
const updated = [
newAccount,
...savedCustomAccounts.filter(
(a) => normalizeToken(a.token) !== cleanToken,
),
];
persistCustomAccounts(updated);
setCustomTitleInput("");
setCustomTokenInput("");
handleSelectToken(cleanToken, cleanTitle, selectedEmoji);
};
const handleSaveAccountOnly = () => {
const cleanToken = normalizeToken(customTokenInput);
if (!cleanToken) return;
const cleanTitle =
customTitleInput.trim() || `حساب سفارشی (${cleanToken.slice(0, 6)}...)`;
const newAccount: DevAccountItem = {
token: cleanToken,
title: cleanTitle,
emoji: selectedEmoji,
isCustom: true,
};
const updated = [
newAccount,
...savedCustomAccounts.filter(
(a) => normalizeToken(a.token) !== cleanToken,
),
];
persistCustomAccounts(updated);
setCustomTitleInput("");
setCustomTokenInput("");
};
const handleDeleteCustomAccount = (tokenToDelete: string) => {
const cleanDelete = normalizeToken(tokenToDelete);
const updated = savedCustomAccounts.filter(
(acc) => normalizeToken(acc.token) !== cleanDelete,
);
persistCustomAccounts(updated);
};
const handleResetUser = async (userId: number | null, label: string) => {
const message =
userId !== null
? `آیا از پاک کردن تمامی اطلاعات هویتی، پاسخ‌ها و پیشرفت این کاربر (${label}) و شروع مجدد از مرحله اول آنبوردینگ مطمئن هستید؟`
: `آیا از پاک کردن تمامی اطلاعات محلی و کوکی‌های مربوط به این حالت (بدون توکن) و شروع مجدد مطمئن هستید؟`;
: `آیا از پاک کردن تمامی اطلاعات محلی و کوکی‌های مربوط به این حالت (${label}) و شروع مجدد مطمئن هستید؟`;
if (!window.confirm(message)) {
return;
}
try {
if (typeof window !== "undefined") {
try {
window.localStorage.clear();
window.sessionStorage.clear();
} catch (e) {
console.error("Failed to clear storage", e);
}
}
safeClearStorage();
if (userId !== null) {
const response = await fetch("/api/dev-reset-profile", {
@ -128,63 +338,45 @@ export function TokenSwitcher({
? MALE_2_TOKEN
: "NO_TOKEN";
setCachedMarriageEntryPath(null);
setClientCookie(TOKEN_COOKIE_NAME, targetToken);
setClientCookie("habib_token", targetToken);
if (typeof window !== "undefined") {
try {
sessionStorage.setItem(TOKEN_COOKIE_NAME, targetToken);
} catch (e) {
console.warn("sessionStorage is not accessible:", e);
}
(window as any).HABIB_TOKEN = targetToken;
setCurrentToken(targetToken);
}
const targetAccount = allAccounts.find(
(a) => normalizeToken(a.token) === normalizeToken(targetToken),
);
const targetTitle = targetAccount?.title || label;
const targetEmoji = targetAccount?.emoji || "🔑";
setIsOpen(false);
window.location.replace("/");
handleSelectToken(targetToken, targetTitle, targetEmoji);
} catch (error) {
console.error("Failed to reset user:", error);
alert("خطایی در انجام عملیات رخ داد.");
}
};
const mainAccounts = [
{
id: 17119,
token: MALE_TOKEN,
title: "حساب آقا",
subtitle: MALE_EMAIL,
emoji: "👨",
isActive: isMale,
},
{
id: 146024,
token: MALE_2_TOKEN,
title: "حساب آقا ۲",
subtitle: MALE_2_EMAIL,
emoji: "👨",
isActive: isMale2,
},
{
id: 147714,
token: FEMALE_TOKEN,
title: "حساب خانم",
subtitle: FEMALE_EMAIL,
emoji: "👩",
isActive: isFemale,
},
];
// ─────────────────────────────────────────────────────────────────────────
// عنوان و نام نمایشی روی Badge بسته شده (Floating Closed Badge Label)
// ─────────────────────────────────────────────────────────────────────────
const getFloatingButtonLabel = () => {
if (isNoToken) {
return "بدون توکن 👤";
}
if (activeAccount) {
return `${activeAccount.title} ${activeAccount.emoji || "🔑"}`;
}
if (activeStoredName) {
return `${activeStoredName} ${activeStoredEmoji || "🔑"}`;
}
return "حساب سفارشی 🔑";
};
return (
<>
{/* Floating Badge (وقتی بسته است فقط نام حساب و ایموجی را نمایش می‌دهد) */}
<button
type="button"
onClick={() => setIsOpen(true)}
aria-label="تغییر توکن کاربر (تست)"
title="تغییر توکن (آقا / خانم)"
title="تغییر توکن کاربر (محیط توسعه)"
className={[
"fixed top-4 left-4 z-[99999] inline-flex items-center gap-1.5 rounded-[15px] px-3 py-2 text-xs font-bold transition-all active:scale-95 shadow-md border border-slate-200/80 dark:border-slate-700/80 backdrop-blur-md",
"fixed top-4 left-4 z-[99999] inline-flex items-center gap-2 rounded-[15px] px-3.5 py-2 text-xs font-extrabold transition-all active:scale-95 shadow-md border border-slate-200/80 dark:border-slate-700/80 backdrop-blur-md max-w-[260px] cursor-pointer",
variant === "transparent"
? "bg-black/40 text-white hover:bg-black/50"
: "bg-white/95 text-slate-800 hover:bg-white dark:bg-slate-900/95 dark:text-slate-100",
@ -193,33 +385,31 @@ export function TokenSwitcher({
.filter(Boolean)
.join(" ")}
>
<MdOutlineSwitchAccount className="size-5 text-rose-500" />
<span>
{isMale
? "آقا 👨"
: isMale2
? "آقا ۲ 👨"
: isFemale
? "خانم 👩"
: isNoToken
? "بدون توکن 👤"
: "سفارشی 🔑"}
</span>
<MdOutlineSwitchAccount className="size-5 text-rose-500 shrink-0" />
<span className="truncate">{getFloatingButtonLabel()}</span>
</button>
{isOpen && (
<div
className="fixed inset-0 z-[100000] flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs animate-in fade-in duration-200"
onClick={() => setIsOpen(false)}
>
<div className="fixed inset-0 z-[100000] flex items-center justify-center p-4 animate-in fade-in duration-200">
{/* Backdrop Button */}
<button
type="button"
aria-label="بستن پنجره"
onClick={() => setIsOpen(false)}
className="fixed inset-0 bg-black/60 backdrop-blur-xs cursor-default"
/>
{/* Modal Container */}
<div
className="w-full max-w-sm rounded-2xl bg-white p-5 shadow-2xl dark:bg-slate-900 border border-slate-100 dark:border-slate-800 max-h-[90vh] flex flex-col"
onClick={(e) => e.stopPropagation()}
className="relative z-10 w-full max-w-md rounded-2xl bg-white shadow-2xl dark:bg-slate-900 border border-slate-100 dark:border-slate-800 max-h-[90vh] flex flex-col overflow-hidden text-right"
dir="rtl"
role="dialog"
aria-modal="true"
>
{/* Modal Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100 dark:border-slate-800 shrink-0 bg-slate-50/50 dark:bg-slate-900/50">
<div className="flex items-center gap-3">
<div className="size-10 rounded-xl bg-rose-500/10 text-rose-500 flex items-center justify-center">
<div className="size-10 rounded-xl bg-rose-500/10 text-rose-500 flex items-center justify-center shrink-0">
<MdOutlineSwitchAccount className="size-5" />
</div>
<div>
@ -227,67 +417,300 @@ export function TokenSwitcher({
انتخاب حساب کاربری (تست)
</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">
تغییر سریع پروفایل و توکن جهت تست
تغییر سریع پروفایل، نامگذاری و مدیریت توکنها
</p>
</div>
</div>
<button
type="button"
onClick={() => setIsOpen(false)}
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-sm p-1"
className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 text-sm p-1 cursor-pointer"
>
<IoClose className="size-5" />
</button>
</div>
{/* Scrollable Account Cards */}
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-2.5 min-h-0">
{/* Main Predefined Accounts */}
{mainAccounts.map((acc) => (
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-4 min-h-0">
{/* Predefined System Accounts */}
<div className="space-y-2">
<div className="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-1">
حسابهای پیشفرض سیستم
</div>
<div className="space-y-2">
{PRESET_DEV_ACCOUNTS.map((acc) => {
const isActive = normalizeToken(acc.token) === normCurrent;
return (
<div
key={acc.token}
className={[
"flex items-center justify-between gap-3 p-3 rounded-2xl border transition-all select-none",
isActive
? "border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30"
: "border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 bg-white dark:bg-slate-800/80 hover:bg-slate-50/60 dark:hover:bg-slate-800",
].join(" ")}
>
<button
type="button"
onClick={() =>
handleSelectToken(acc.token, acc.title, acc.emoji)
}
className="flex-1 min-w-0 flex items-center gap-3 text-right cursor-pointer bg-transparent border-0 p-0 text-inherit"
>
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
{acc.emoji || "👨"}
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
{acc.title}
</span>
{acc.subtitle && (
<span
className="text-xs text-slate-500 dark:text-slate-400 font-medium truncate block leading-tight text-right"
dir="ltr"
>
{acc.subtitle}
</span>
)}
<span
className="text-[10px] font-mono text-slate-400 dark:text-slate-500 truncate block text-left leading-tight"
dir="ltr"
>
{acc.token}
</span>
</div>
</button>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
{isActive && (
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
)}
<button
type="button"
onClick={() =>
handleResetUser(acc.id ?? null, acc.title)
}
title="پاک کردن اطلاعات و شروع مجدد"
className="size-8 rounded-lg flex items-center justify-center text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition-colors cursor-pointer"
>
<IoTrashOutline className="size-4" />
</button>
</div>
</div>
);
})}
</div>
</div>
{/* Inline Passed Accounts (if any) */}
{inlineAccounts.length > 0 && (
<div className="space-y-2">
<div className="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-1">
حسابهای کد (Inline)
</div>
<div className="space-y-2">
{inlineAccounts.map((acc) => {
const isActive =
normalizeToken(acc.token) === normCurrent;
return (
<div
key={acc.token}
className={[
"flex items-center justify-between gap-3 p-3 rounded-2xl border transition-all select-none",
isActive
? "border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30"
: "border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 bg-white dark:bg-slate-800/80 hover:bg-slate-50/60 dark:hover:bg-slate-800",
].join(" ")}
>
<button
type="button"
onClick={() =>
handleSelectToken(acc.token, acc.title, acc.emoji)
}
className="flex-1 min-w-0 flex items-center gap-3 text-right cursor-pointer bg-transparent border-0 p-0 text-inherit"
>
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
{acc.emoji || "🔑"}
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
{acc.title}
</span>
<span
className="text-[10px] font-mono text-slate-400 dark:text-slate-500 truncate block text-left leading-tight"
dir="ltr"
>
{acc.token}
</span>
</div>
</button>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
{isActive && (
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
)}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Saved Custom Named Accounts */}
{savedCustomAccounts.length > 0 && (
<div className="space-y-2">
<div className="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-1">
حسابهای سفارشی شما
</div>
<div className="space-y-2">
{savedCustomAccounts.map((acc) => {
const isActive =
normalizeToken(acc.token) === normCurrent;
return (
<div
key={acc.token}
className={[
"flex items-center justify-between gap-3 p-3 rounded-2xl border transition-all select-none",
isActive
? "border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30"
: "border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 bg-white dark:bg-slate-800/80 hover:bg-slate-50/60 dark:hover:bg-slate-800",
].join(" ")}
>
<button
type="button"
onClick={() =>
handleSelectToken(acc.token, acc.title, acc.emoji)
}
className="flex-1 min-w-0 flex items-center gap-3 text-right cursor-pointer bg-transparent border-0 p-0 text-inherit"
>
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
{acc.emoji || "🔑"}
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
{acc.title}
</span>
<span
className="text-[10px] font-mono text-slate-400 dark:text-slate-500 truncate block text-left leading-tight"
dir="ltr"
>
{acc.token}
</span>
</div>
</button>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
{isActive && (
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
)}
<button
type="button"
onClick={() =>
handleDeleteCustomAccount(acc.token)
}
title="حذف از لیست حساب‌های سفارشی"
className="size-8 rounded-lg flex items-center justify-center text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-950/40 transition-colors cursor-pointer"
>
<IoTrashOutline className="size-4" />
</button>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Unnamed Active Custom Token (if any active token not in list) */}
{!activeAccount && !isNoToken && (
<div className="space-y-2">
<div className="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-1">
توکن فعال فعلی
</div>
<div className="flex items-center justify-between gap-3 p-3 rounded-2xl border border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30 select-none">
<div className="flex-1 min-w-0 flex items-center gap-3 text-right">
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
{activeStoredEmoji || "🔑"}
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
{activeStoredName || "توکن سفارشی فعال"}
</span>
<span
className="text-[10px] font-mono text-slate-500 dark:text-slate-400 truncate block text-left leading-tight"
dir="ltr"
>
{currentToken}
</span>
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
<button
type="button"
onClick={() => handleResetUser(null, "توکن سفارشی")}
title="پاک کردن اطلاعات و شروع مجدد"
className="size-8 rounded-lg flex items-center justify-center text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition-colors cursor-pointer"
>
<IoTrashOutline className="size-4" />
</button>
</div>
</div>
</div>
)}
{/* No Token Option Card */}
<div className="space-y-2">
<div className="text-[11px] font-bold text-slate-400 dark:text-slate-500 px-1">
حالت بدون احراز هویت
</div>
<div
key={acc.token}
className={[
"flex items-center justify-between gap-3 p-3 rounded-2xl border transition-all select-none",
acc.isActive
isNoToken
? "border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30"
: "border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 bg-white dark:bg-slate-800/80 hover:bg-slate-50/60 dark:hover:bg-slate-800",
].join(" ")}
>
<div
onClick={() => handleSelectToken(acc.token)}
className="flex-1 min-w-0 flex items-center gap-3 text-right cursor-pointer"
<button
type="button"
onClick={() =>
handleSelectToken("NO_TOKEN", "کاربر بدون توکن", "👤")
}
className="flex-1 min-w-0 flex items-center gap-3 text-right cursor-pointer bg-transparent border-0 p-0 text-inherit"
>
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
{acc.emoji}
👤
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
{acc.title}
</span>
<span
className="text-xs text-slate-500 dark:text-slate-400 font-medium truncate block leading-tight text-right"
dir="ltr"
>
{acc.subtitle}
کاربر بدون توکن
</span>
<span
className="text-[10px] font-mono text-slate-400 dark:text-slate-500 truncate block text-left leading-tight"
dir="ltr"
>
{acc.token}
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium truncate block leading-tight">
حالت مهمان / کاربر جدید بدون لاگین
</span>
</div>
</div>
</button>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
{acc.isActive && (
{isNoToken && (
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
)}
<button
type="button"
onClick={() => handleResetUser(acc.id, acc.title)}
onClick={() => handleResetUser(null, "کاربر بدون توکن")}
title="پاک کردن اطلاعات و شروع مجدد"
className="size-8 rounded-lg flex items-center justify-center text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition-colors cursor-pointer"
>
@ -295,117 +718,84 @@ export function TokenSwitcher({
</button>
</div>
</div>
))}
</div>
{/* Custom Active Token Card (if any custom token is set) */}
{isCustomToken && (
<div className="flex items-center justify-between gap-3 p-3 rounded-2xl border border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30 select-none">
<div className="flex-1 min-w-0 flex items-center gap-3 text-right">
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
🔑
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
توکن سفارشی
</span>
<span
className="text-[10px] font-mono text-slate-500 dark:text-slate-400 truncate block text-left leading-tight"
dir="ltr"
>
{currentToken}
</span>
</div>
{/* Add New Custom Named Token Form */}
<form
onSubmit={handleAddAndSelectAccount}
className="rounded-2xl border border-dashed border-slate-300 dark:border-slate-700 p-4 bg-slate-50/70 dark:bg-slate-800/40 space-y-3"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xs font-bold text-slate-700 dark:text-slate-300">
<IoKeyOutline className="size-4 text-amber-500" />
<span>افزودن حساب تستی با نام دلخواه:</span>
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
{/* Emoji Selector */}
<div className="flex items-center gap-1.5 overflow-x-auto pb-1">
<span className="text-[11px] text-slate-500 dark:text-slate-400 shrink-0 ml-1">
آیکون:
</span>
{AVAILABLE_EMOJIS.map((emoji) => (
<button
key={emoji}
type="button"
onClick={() => handleResetUser(null, "توکن سفارشی")}
title="پاک کردن اطلاعات و شروع مجدد"
className="size-8 rounded-lg flex items-center justify-center text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition-colors cursor-pointer"
onClick={() => setSelectedEmoji(emoji)}
className={[
"size-7 rounded-lg text-sm flex items-center justify-center transition-all cursor-pointer",
selectedEmoji === emoji
? "bg-rose-500/20 ring-1 ring-rose-500 scale-110 shadow-3xs"
: "bg-white dark:bg-slate-900 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200 dark:border-slate-700",
].join(" ")}
>
<IoTrashOutline className="size-4" />
{emoji}
</button>
</div>
</div>
)}
{/* No Token Option Card */}
<div
className={[
"flex items-center justify-between gap-3 p-3 rounded-2xl border transition-all select-none",
isNoToken
? "border-rose-500 bg-rose-50/70 dark:bg-rose-950/40 shadow-xs ring-1 ring-rose-500/30"
: "border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 bg-white dark:bg-slate-800/80 hover:bg-slate-50/60 dark:hover:bg-slate-800",
].join(" ")}
>
<div
onClick={() => handleSelectToken("NO_TOKEN")}
className="flex-1 min-w-0 flex items-center gap-3 text-right cursor-pointer"
>
<div className="size-11 rounded-xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-2xl shrink-0 border border-slate-200/60 dark:border-slate-700/60 shadow-3xs">
👤
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center gap-0.5 text-right">
<span className="text-sm font-bold text-slate-900 dark:text-white leading-tight">
کاربر بدون توکن
</span>
<span className="text-xs text-slate-500 dark:text-slate-400 font-medium truncate block leading-tight">
حالت مهمان / بدون احراز هویت
</span>
</div>
))}
</div>
<div className="flex items-center gap-1.5 shrink-0 pr-1">
{isNoToken && (
<span className="text-[10px] font-bold px-2 py-0.5 rounded-full bg-rose-500 text-white shadow-xs">
فعال
</span>
)}
<button
type="button"
onClick={() => handleResetUser(null, "کاربر بدون توکن")}
title="پاک کردن اطلاعات و شروع مجدد"
className="size-8 rounded-lg flex items-center justify-center text-slate-400 hover:text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 transition-colors cursor-pointer"
>
<IoTrashOutline className="size-4" />
</button>
{/* Title Input */}
<div>
<input
type="text"
value={customTitleInput}
onChange={(e) => setCustomTitleInput(e.target.value)}
placeholder="نام یا عنوان حساب (مثلاً: تست طلاق، کاربر سناریو ۲)"
className="w-full rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 py-2 text-xs text-slate-800 dark:text-slate-200 outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-500/20 transition-all placeholder:text-slate-400 placeholder:text-[11px]"
/>
</div>
</div>
{/* Custom Token Input Section */}
<div className="rounded-2xl border border-dashed border-slate-300 dark:border-slate-700 p-3.5 bg-slate-50/70 dark:bg-slate-800/40 space-y-2.5 mt-3">
<div className="flex items-center gap-2 text-xs font-bold text-slate-700 dark:text-slate-300">
<IoKeyOutline className="size-4 text-amber-500" />
<span>افزودن یا تنظیم توکن دلخواه:</span>
</div>
<div className="flex items-center gap-2">
{/* Token Input */}
<div>
<input
type="text"
value={customTokenInput}
onChange={(e) => setCustomTokenInput(e.target.value)}
placeholder="توکن ۳۲/۴۰ کاراکتری..."
placeholder="رشته توکن (۳۲ یا ۴۰ کاراکتری)..."
dir="ltr"
className="flex-1 min-w-0 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 py-2 text-xs font-mono text-slate-800 dark:text-slate-200 outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-500/20 transition-all placeholder:text-slate-400 placeholder:text-[11px]"
onKeyDown={(e) => {
if (e.key === "Enter" && customTokenInput.trim()) {
handleSelectToken(customTokenInput);
}
}}
className="w-full rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-3 py-2 text-xs font-mono text-slate-800 dark:text-slate-200 outline-none focus:border-rose-500 focus:ring-1 focus:ring-rose-500/20 transition-all placeholder:text-slate-400 placeholder:text-[11px]"
/>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2 pt-1">
<button
type="submit"
disabled={!customTokenInput.trim()}
className="flex-1 rounded-xl bg-rose-500 hover:bg-rose-600 active:scale-95 py-2 text-xs font-bold text-white shadow-xs disabled:opacity-40 disabled:cursor-not-allowed transition-all cursor-pointer text-center"
>
ذخیره و ورود به حساب
</button>
<button
type="button"
disabled={!customTokenInput.trim()}
onClick={() => handleSelectToken(customTokenInput)}
className="rounded-xl bg-rose-500 hover:bg-rose-600 active:scale-95 px-4 py-2 text-xs font-bold text-white shadow-xs disabled:opacity-40 disabled:cursor-not-allowed transition-all shrink-0 cursor-pointer"
onClick={handleSaveAccountOnly}
className="rounded-xl border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700 active:scale-95 px-3 py-2 text-xs font-medium text-slate-700 dark:text-slate-300 disabled:opacity-40 disabled:cursor-not-allowed transition-all cursor-pointer shrink-0"
>
اعمال
فقط ذخیره در لیست
</button>
</div>
</div>
</form>
</div>
{/* Modal Footer */}

27
src/hooks/marriage/use-habcoin-inventory.ts

@ -0,0 +1,27 @@
"use client";
import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
import { http } from "@/lib/http";
export type HabcoinInventory = {
coin_balance: number;
};
export async function getHabcoinInventory(): Promise<HabcoinInventory> {
const { data } = await http.get<HabcoinInventory>("/habcoin/inventory/");
return data;
}
export function useHabcoinInventoryQuery(
options?: Omit<
UseQueryOptions<HabcoinInventory, Error>,
"queryKey" | "queryFn"
>,
) {
return useQuery({
queryKey: ["habcoin", "inventory"],
queryFn: getHabcoinInventory,
staleTime: 15 * 1000,
...options,
});
}

32
src/hooks/marriage/use-habcoin-payment.ts

@ -54,20 +54,38 @@ export function extractHabcoinPaymentUrl(response: HabcoinPaymentResponse) {
return null;
}
export async function getHabcoinPayment(objectId: number) {
export type HabcoinPaymentParams =
| number
| {
objectId: number;
discountCode?: string;
};
export async function getHabcoinPayment(params: HabcoinPaymentParams) {
const objectId = typeof params === "number" ? params : params.objectId;
const discountCode =
typeof params === "number" ? undefined : params.discountCode;
const queryParams: Record<string, unknown> = {
token: getSecurityToken(),
service: HABCOIN_PAY_SERVICE,
app_name: HABCOIN_PAY_SERVICE,
object_id: objectId,
};
if (discountCode) {
queryParams.discount_code = discountCode;
}
const { data } = await http.get<HabcoinPaymentResponse>("/habcoin/pay/", {
params: {
token: getSecurityToken(),
service: HABCOIN_PAY_SERVICE,
object_id: objectId,
},
params: queryParams,
});
return data;
}
export function useHabcoinPaymentMutation(
options?: MutationOptions<HabcoinPaymentResponse, number>,
options?: MutationOptions<HabcoinPaymentResponse, HabcoinPaymentParams>,
) {
const queryClient = useQueryClient();

36
src/hooks/marriage/use-profile-main.ts

@ -12,25 +12,45 @@ import { marriageQueryKeys } from "./query-keys";
import type { MarriageProfileResponse } from "./types";
export async function getMarriageProfile() {
const { data } = await http.get<MarriageProfileResponse>(
"/api/marriage/profile/main/",
);
console.log("🌐 [Profile API] Sending GET /api/marriage/profile/main/ ...");
try {
const { data } = await http.get<MarriageProfileResponse>(
"/api/marriage/profile/main/",
);
setCachedMarriageEntryPath(
hasCompletedMarriageProfileBasics(data) ? getSubmitPath(data) : null,
);
console.log("🌐 [Profile API] Successfully received data:", {
id: data?.id,
status: data?.status,
gender: data?.gender,
is_ready_for_match: data?.is_ready_for_match,
active_case: data?.active_case,
has_match_summary: Boolean(data?.match_summary),
match_summary_id: data?.match_summary?.id,
public_info_count: data?.match_summary?.public_info?.length,
raw_match_summary: data?.match_summary,
});
return data;
setCachedMarriageEntryPath(
hasCompletedMarriageProfileBasics(data) ? getSubmitPath(data) : null,
);
return data;
} catch (error) {
console.error("❌ [Profile API] Failed to fetch profile:", error);
throw error;
}
}
export function useMarriageProfileQuery<TData = MarriageProfileResponse>(
options?: QueryOptions<MarriageProfileResponse, TData>,
) {
return useQuery({
const query = useQuery({
staleTime: 10 * 1000,
refetchOnWindowFocus: false,
...options,
queryFn: getMarriageProfile,
queryKey: marriageQueryKeys.profile(),
});
return query;
}

77
src/hooks/marriage/use-validate-discount.ts

@ -0,0 +1,77 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { http } from "@/lib/http";
import type { MutationOptions } from "./options";
const HABCOIN_SERVICE = "marriagesubscriptionplan" as const;
export interface CheckDiscountResult {
valid: boolean;
code: string;
originalPrice: number;
discountedPrice: number;
discountAmount: number;
discountPercentage: number;
message?: string;
}
export interface ValidateDiscountParams {
code: string;
objectId: number | string;
service?: string;
}
export async function validateDiscountCode({
code,
objectId,
service = HABCOIN_SERVICE,
}: ValidateDiscountParams): Promise<CheckDiscountResult> {
try {
const { data } = await http.post<any>("/habcoin/validate-discount/", {
code,
service,
object_id: objectId,
});
return {
valid: Boolean(data?.valid ?? true),
code,
originalPrice: Number(data?.original_price ?? 0),
discountedPrice: Number(data?.discounted_price ?? 0),
discountAmount: Number(data?.discount_amount ?? 0),
discountPercentage: Number(data?.discount_percentage ?? 0),
message: data?.message || undefined,
};
} catch (err: any) {
const responseData = err?.response?.data;
const errors = responseData?.errors;
const message =
errors?.code?.[0] ||
errors?.service?.[0] ||
errors?.object_id?.[0] ||
responseData?.detail ||
responseData?.message ||
err?.message ||
"Invalid discount code";
return {
valid: false,
code,
originalPrice: 0,
discountedPrice: 0,
discountAmount: 0,
discountPercentage: 0,
message,
};
}
}
export function useValidateDiscountMutation(
options?: MutationOptions<CheckDiscountResult, ValidateDiscountParams>,
) {
return useMutation({
...options,
mutationFn: validateDiscountCode,
});
}

26
src/lib/webview-actions.test.ts

@ -4,6 +4,7 @@ import {
copyToClipboard,
downloadFile,
isInFlutterWebView,
openConsultantPage,
openExternalUrl,
postActionToFlutter,
uploadFile,
@ -188,4 +189,29 @@ describe("webview-actions", () => {
);
});
});
describe("openConsultantPage", () => {
it("dispatches open_consultant_page action with username", () => {
const mockPostMessage = vi.fn();
window.HabibApp = { postMessage: mockPostMessage };
const result = openConsultantPage("sara_marriage_advisor");
expect(result).toBe(true);
expect(mockPostMessage).toHaveBeenCalledWith(
JSON.stringify({
action: "open_consultant_page",
data: {
consultant: "sara_marriage_advisor",
},
}),
);
});
it("returns false when not in flutter webview", () => {
delete (window as any).HabibApp;
const result = openConsultantPage("sara_marriage_advisor");
expect(result).toBe(false);
});
});
});

14
src/lib/webview-actions.ts

@ -166,3 +166,17 @@ export function uploadFile(options: UploadFileOptions): boolean {
export function buyHabibCoinPackages(): boolean {
return postActionToFlutter("buy_coin");
}
// ─── open_consultant_page ────────────────────────────────
/**
* Ask Flutter to open the native Talk consultant single page.
*
* ```ts
* openConsultantPage("consultant_username");
* ```
*/
export function openConsultantPage(username: string): boolean {
return postActionToFlutter("open_consultant_page", { consultant: username });
}

18
src/translations/locales/ar.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### خيارات الجو الديني العائلي * **الدين والالتزام الصارم:** يحدد هذا عائلة مكرسة للغاية لأداء جميع **الواجبات الإلزامية**، والحفاظ بشكل صارم على **الحدود الدينية** (مثل قواعد المحارم)، ودعم **الطقوس والتعاليم الدينية** في جميع جوانب الحياة. * **متدين (ملتزم بالالتزامات):** يشير هذا إلى عائلة ملتزمة بأساسيات **الواجبات الدينية** (مثل الصلاة والصيام) و **الأخلاق الإسلامية**، التي تعيش ضمن الأطر القياسية لمجتمع ديني. * **التقليدية (التي تحترم القيم الدينية):** تصف هذه العائلة التي تلتزم بالقيم الأخلاقية و**تحترم الدين**، ولكن لا يجوز لها أن تنفذ بدقة كل **قانون أو التزام ديني** محدد. * **غير دينية / علمانية:** يمثل هذا عائلة لا تؤثر فيها **الطقوس والأطر الدينية** بشكل كبير على **نمط حياتهم اليومي أو علاقاتهم أو قراراتهم**، على الرغم من الاحترام العام للدين.",
"(Complete Required Forms)": "(إكمال النماذج المطلوبة)",
"(after 2 days)": "(بعد يومين)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. شروط الأهلية والعضوية",
"160 to 170": "160 إلى 170",
"170 to 180": "170 إلى 180",
"175": "175",
"180 to 190": "180 إلى 190",
"2": "2",
"2 minutes": "2 دقيقة",
"2. Privacy and Data Management": "2. الخصوصية وإدارة البيانات",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 دقائق",
"50 Coins": "50 قطعة نقدية",
"6 minutes": "6 دقائق",
"70": "70",
"8 minutes": "8 دقائق",
"A Path to Heavenly Marriage": "الطريق إلى الزواج السماوي",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "ليس هناك حاجة إلى عنوان دقيق. يكفي فقط المنطقة العامة للمكان الذي تعيش فيه، مثل المدينة أو المنطقة أو الحي أو أقرب مدينة رئيسية.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "شراء عملات حبيب",
"Subscription required": "الاشتراك مطلوب",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "لعرض الملفات الشخصية، تحتاج إلى اشتراك. يتضمن كل اشتراك ما يصل إلى {count} تطابقات.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "لعرض الملفات الشخصية، يلزم الاشتراك. يساعد ذلك في تغطية خدمات الدعم والتوفيق ويسري فقط على المستخدمين الذكور"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "لعرض الملفات الشخصية، يلزم الاشتراك. يساعد ذلك في تغطية خدمات الدعم والتوفيق ويسري فقط على المستخدمين الذكور",
"More detail": "مزيد من التفاصيل",
"{count} case introductions remaining of your subscription.": "{count} من حالات التعارف المتبقية من اشتراكك.",
"{days} days and {count} case introductions remaining of your subscription.": "{days} يوم و {count} من حالات التعارف المتبقية من اشتراكك.",
"You do not have enough Habib Coins": "ليس لديك عدد كافٍ من حبيب كوين",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "يرجى إضافة المزيد من حبيب كوين إلى رصيدك (أو استخدم رمز الخصم) لتتمكن من الوصول إلى هذا المحتوى",
"Do you have a discount code?": "هل لديك رمز خصم؟",
"Enter discount code...": "أدخل رمز الخصم...",
"Apply": "تطبيق",
"Clear": "مسح",
"This field is required": "هذه الخانة مطلوبة"
}

18
src/translations/locales/az.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Ailə Dini Atmosfer Seçimləri * **Dini və Ciddi şəkildə əməl edən:** Bu, bütün **məcburi vəzifələri** yerinə yetirməyə, **dini sərhədləri** (məs., Məhrəm qaydaları kimi) ciddi şəkildə qorumağa və bütün aspektlərdə **dini ayin və təlimlərə** riayət etməyə yüksək dərəcədə bağlı olan ailəni göstərir. * **Dini (Vəziyyətlərə əməl edən):** Bu, əsas **dini vəzifələrə** (namaz və oruc kimi) və **İslam etikasına** sadiq olan, dini cəmiyyətin standart çərçivələri daxilində yaşayan ailəni göstərir. * **Ənənəvi (Dini Dəyərlərə Hörmətli):** Bu, əxlaqi dəyərlərə sadiq qalan və **dinə hörmət edən** ailəni təsvir edir, lakin hər bir xüsusi **dini qanunu** və ya öhdəliyi ciddi şəkildə yerinə yetirməyə bilməz. * **Dini olmayan / Dünyəvi:** Bu, **dini ayinlər və çərçivələrin** dinə ümumi hörmət bəsləməsinə baxmayaraq, onların gündəlik **həyat tərzinə, münasibətlərinə və ya qərarlarına** əhəmiyyətli dərəcədə təsir göstərməyən ailəni təmsil edir.",
"(Complete Required Forms)": "(Tələb olunan formaları doldurun)",
"(after 2 days)": "(2 gündən sonra)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Uyğunluq və Üzvlük Tələbləri",
"160 to 170": "160-170",
"170 to 180": "170-180",
"175": "175",
"180 to 190": "180-190",
"2": "2",
"2 minutes": "2 dəqiqə",
"2. Privacy and Data Management": "2. Məxfilik və Məlumatların İdarə Edilməsi",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 dəqiqə",
"50 Coins": "50 qəpik",
"6 minutes": "6 dəqiqə",
"70": "70",
"8 minutes": "8 dəqiqə",
"A Path to Heavenly Marriage": "Səmavi Evliliyə gedən yol",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "Dəqiq ünvan tələb olunmur. Yaşadığınız yerin ümumi ərazisi kifayətdir, məsələn, şəhər, rayon, məhəllə və ya ən yaxın böyük şəhər.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "Həbib Sikkələri Al",
"Subscription required": "Abunə tələb olunur",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Profilləri görmək üçün abunəyə ehtiyacınız var. Hər abunə {count} uyğunlaşmanı əhatə edir.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Profilləri görmək üçün abunə tələb olunur. Bu, dəstək və uyğunlaşma xidmətlərimizi əhatə edir və yalnız kişi istifadəçilərə aiddir"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Profilləri görmək üçün abunə tələb olunur. Bu, dəstək və uyğunlaşma xidmətlərimizi əhatə edir və yalnız kişi istifadəçilərə aiddir",
"More detail": "Daha ətraflı",
"{count} case introductions remaining of your subscription.": "Abunəliyinizdən {count} təqdimat qaldı.",
"{days} days and {count} case introductions remaining of your subscription.": "Abunəliyinizdən {days} gün və {count} təqdimat qaldı.",
"You do not have enough Habib Coins": "Kifayət qədər Həbib Koininiz yoxdur",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Bu məzmuna daxil olmaq üçün balansınıza daha çox Həbib Coin əlavə edin (və ya endirim kodu istifadə edin)",
"Do you have a discount code?": "Endirim kodunuz var?",
"Enter discount code...": "Endirim kodunu daxil edin...",
"Apply": "Tətbiq et",
"Clear": "Təmizlə",
"This field is required": "Bu sahə zəruridir"
}

18
src/translations/locales/bn.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### পারিবারিক ধর্মীয় পরিবেশের বিকল্পগুলি * **ধর্মীয় এবং কঠোরভাবে পর্যবেক্ষক:** এটি এমন একটি পরিবারকে নির্দিষ্ট করে যা সমস্ত **বাধ্যতামূলক দায়িত্ব** পালনের জন্য অত্যন্ত নিবেদিত, কঠোরভাবে **ধর্মীয় সীমানা** (যেমন মাহরাম নিয়ম) বজায় রাখতে এবং **জীবনের **ধর্মীয় আচার ও শিক্ষা**কে সমুন্নত রাখে। * **ধর্মীয় (দায়বদ্ধতা পালনকারী):** এটি একটি পরিবারকে নির্দেশ করে যে মূল **ধর্মীয় কর্তব্য** (যেমন প্রার্থনা এবং উপবাস) এবং **ইসলামিক নীতি**, একটি ধর্মীয় সমাজের মানক কাঠামোর মধ্যে বসবাস করে। * **ঐতিহ্যগত (ধর্মীয় মূল্যবোধের প্রতি শ্রদ্ধাশীল):** এটি এমন একটি পরিবারকে বর্ণনা করে যেটি নৈতিক মূল্যবোধের প্রতি ভক্তি রাখে এবং **ধর্মকে সম্মান করে**, কিন্তু প্রতিটি নির্দিষ্ট **ধর্মীয় আইন** বা বাধ্যবাধকতা কঠোরভাবে পালন নাও করতে পারে। * **অধর্মীয় / ধর্মনিরপেক্ষ:** এটি এমন একটি পরিবারের প্রতিনিধিত্ব করে যেখানে **ধর্মীয় আচার-অনুষ্ঠান এবং কাঠামো** তাদের দৈনন্দিন **জীবনধারা, সম্পর্ক বা সিদ্ধান্ত**কে উল্লেখযোগ্যভাবে প্রভাবিত করে না, যদিও ধর্মের প্রতি সাধারণ শ্রদ্ধা রয়েছে।",
"(Complete Required Forms)": "(প্রয়োজনীয় ফর্ম সম্পূর্ণ করুন)",
"(after 2 days)": "(২ দিন পর)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. যোগ্যতা এবং সদস্যতার প্রয়োজনীয়তা",
"160 to 170": "160 থেকে 170",
"170 to 180": "170 থেকে 180",
"175": "175",
"180 to 190": "180 থেকে 190",
"2": "2",
"2 minutes": "2 মিনিট",
"2. Privacy and Data Management": "2. গোপনীয়তা এবং ডেটা ব্যবস্থাপনা",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 মিনিট",
"50 Coins": "50 কয়েন",
"6 minutes": "6 মিনিট",
"70": "70",
"8 minutes": "8 মিনিট",
"A Path to Heavenly Marriage": "স্বর্গীয় বিবাহের পথ",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "একটি সুনির্দিষ্ট ঠিকানা প্রয়োজন হয় না. আপনি যেখানে বাস করেন তার সাধারণ এলাকাটিই যথেষ্ট, যেমন শহর, অঞ্চল, পাড়া বা নিকটতম প্রধান শহর৷",
@ -767,5 +767,15 @@
"Buy Habib Coins": "হাবিব কয়েন কিনুন",
"Subscription required": "সাবস্ক্রিপশন প্রয়োজন",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "প্রোফাইল দেখতে আপনার সাবস্ক্রিপশন প্রয়োজন। প্রতিটি সাবস্ক্রিপশনে {count}টি ম্যাচ অন্তর্ভুক্ত।",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "প্রোফাইল দেখতে সাবস্ক্রিপশন প্রয়োজন। এটি আমাদের সহায়তা এবং ম্যাচিং পরিষেবার খরচ বহন করতে সাহায্য করে এবং শুধুমাত্র পুরুষ ব্যবহারকারীদের জন্য প্রযোজ্য"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "প্রোফাইল দেখতে সাবস্ক্রিপশন প্রয়োজন। এটি আমাদের সহায়তা এবং ম্যাচিং পরিষেবার খরচ বহন করতে সাহায্য করে এবং শুধুমাত্র পুরুষ ব্যবহারকারীদের জন্য প্রযোজ্য",
"More detail": "আরও বিস্তারিত",
"{count} case introductions remaining of your subscription.": "আপনার সাবস্ক্রিপশনের {count}টি পরিচিতি বাকি আছে।",
"{days} days and {count} case introductions remaining of your subscription.": "আপনার সাবস্ক্রিপশনের {days} দিন এবং {count}টি পরিচিতি বাকি আছে।",
"You do not have enough Habib Coins": "আপনার পর্যাপ্ত হাবিব কয়েন নেই",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "অনুগ্রহ করে আপনার ব্যালেন্সে আরও হাবিব কয়েন যোগ করুন (অথবা ডিসকাউন্ট কোড ব্যবহার করুন) যাতে আপনি এই বিষয়বস্তু অ্যাক্সেস করতে পারেন",
"Do you have a discount code?": "আপনার কি ডিসকাউন্ট কোড আছে?",
"Enter discount code...": "ডিসকাউন্ট কোড লিখুন...",
"Apply": "প্রয়োগ",
"Clear": "মুছুন",
"This field is required": "এই ক্ষেত্রটি আবশ্যক"
}

18
src/translations/locales/da.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Valgmuligheder for familiereligiøs atmosfære * **Religiøs og strengt observant:** Dette specificerer en familie, der er meget dedikeret til at udføre alle **obligatoriske pligter**, strengt opretholde **religiøse grænser** (såsom Mahram-regler) og opretholde **religiøse ritualer og lære** på tværs af alle aspekter af livet. * **Religiøs (Observant of Obligations):** Dette indikerer en familie, der er forpligtet til kerne **religiøse pligter** (såsom bøn og faste) og **islamisk etik**, der lever inden for et religiøst samfunds standardrammer. * **Traditionelt (respekterer religiøse værdier):** Dette beskriver en familie, der holder hengivenhed til moralske værdier og **respekterer religion**, men som måske ikke strengt udfører enhver specifik **religiøs lov** eller forpligtelse. * **Ikke-religiøs/sekulær:** Dette repræsenterer en familie, hvor **religiøse ritualer og rammer** ikke har væsentlig indflydelse på deres daglige **livsstil, forhold eller beslutninger**, på trods af at de har en generel respekt for religion.",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 til 170",
"170 to 180": "170 til 180",
"175": "175",
"180 to 190": "180 til 190",
"2": "2",
"2 minutes": "2 minutter",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 minutter",
"50 Coins": "50 Coins",
"6 minutes": "6 minutter",
"70": "70",
"8 minutes": "8 minutter",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "En præcis adresse er ikke påkrævet. Bare det generelle område, hvor du bor, er tilstrækkeligt, såsom byen, regionen, kvarteret eller den nærmeste større by.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "Køb Habib-mønter",
"Subscription required": "Abonnement påkrævet",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "For at se profiler har du brug for et abonnement. Hvert abonnement inkluderer op til {count} matches.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "For at se profiler kræves et abonnement. Dette hjælper med at dække vores support- og matchningstjenester og gælder kun for mandlige brugere"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "For at se profiler kræves et abonnement. Dette hjælper med at dække vores support- og matchningstjenester og gælder kun for mandlige brugere",
"More detail": "Flere detaljer",
"{count} case introductions remaining of your subscription.": "{count} match-introduktioner tilbage af dit abonnement.",
"{days} days and {count} case introductions remaining of your subscription.": "{days} dage og {count} match-introduktioner tilbage af dit abonnement.",
"You do not have enough Habib Coins": "Du har ikke nok Habib-mønter",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Tilføj venligst flere Habib-mønt til din saldo (eller brug en rabatkode) for at få adgang til dette indhold",
"Do you have a discount code?": "Har du en rabatkode?",
"Enter discount code...": "Indtast rabatkode...",
"Apply": "Anvend",
"Clear": "Ryd",
"This field is required": "Dette felt er påkrævet"
}

18
src/translations/locales/de.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Optionen für die religiöse Atmosphäre in der Familie * **Religiös und strikt befolgend:** Dies bezeichnet eine Familie, die sich in hohem Maße der Erfüllung aller **obligatorischen Pflichten** widmet, die **religiösen Grenzen** (z. B. die Mahram-Regeln) strikt einhält und **religiöse Rituale und Lehren** in allen Aspekten des Lebens aufrechterhält. * **Religiös (observant of Obligations):** Dies weist auf eine Familie hin, die sich den grundlegenden **religiösen Pflichten** (wie Gebet und Fasten) und der **islamischen Ethik** verpflichtet und innerhalb der Standardrahmen einer religiösen Gesellschaft lebt. * **Traditionell (Respekt gegenüber religiösen Werten):** Dies beschreibt eine Familie, die moralischen Werten treu bleibt und **die Religion respektiert**, aber möglicherweise nicht jedes bestimmte **religiöse Gesetz** oder jede spezifische Verpflichtung strikt einhält. * **Nicht-religiös/säkular:** Dies stellt eine Familie dar, in der **religiöse Rituale und Rahmenbedingungen** ihren täglichen **Lebensstil, ihre Beziehungen oder Entscheidungen** nicht wesentlich beeinflussen, obwohl sie allgemein Respekt vor der Religion haben.",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 bis 170",
"170 to 180": "170 bis 180",
"175": "175",
"180 to 190": "180 bis 190",
"2": "2",
"2 minutes": "2 Minuten",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 Minuten",
"50 Coins": "50 Coins",
"6 minutes": "6 Minuten",
"70": "70",
"8 minutes": "8 Minuten",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "Eine genaue Adresse ist nicht erforderlich. Es reicht lediglich der allgemeine Bereich Ihres Wohnortes aus, beispielsweise die Stadt, die Region, das Viertel oder die nächstgelegene größere Stadt.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "Habib-Münzen kaufen",
"Subscription required": "Abonnement erforderlich",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Um Profile anzusehen, benötigen Sie ein Abonnement. Jedes Abonnement beinhaltet bis zu {count} Übereinstimmungen.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Zum Anzeigen von Profilen ist ein Abonnement erforderlich. Dies hilft, unsere Support- und Matching-Dienste zu finanzieren und gilt nur für männliche Nutzer"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Zum Anzeigen von Profilen ist ein Abonnement erforderlich. Dies hilft, unsere Support- und Matching-Dienste zu finanzieren und gilt nur für männliche Nutzer",
"More detail": "Mehr Details",
"{count} case introductions remaining of your subscription.": "{count} Vorstellungen verbleiben von Ihrem Abonnement.",
"{days} days and {count} case introductions remaining of your subscription.": "{days} Tage und {count} Vorstellungen verbleiben von Ihrem Abonnement.",
"You do not have enough Habib Coins": "Sie haben nicht genügend Habib-Münzen",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Bitte fügen Sie Ihrem Guthaben mehr Habib-Münze hinzu (oder verwenden Sie einen Rabattcode), um auf diesen Inhalt zugreifen zu können",
"Do you have a discount code?": "Haben Sie einen Rabattcode?",
"Enter discount code...": "Rabattcode eingeben...",
"Apply": "Anwenden",
"Clear": "Löschen",
"This field is required": "Dieses Feld ist erforderlich"
}

17
src/translations/locales/en.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options * **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life. * **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society. * **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation. * **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Family Religious Atmosphere Options * **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life. * **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society. * **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation. * **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 to 170",
"170 to 180": "170 to 180",
"175": "175",
"180 to 190": "180 to 190",
"2": "2",
"2 minutes": "2 minutes",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 minutes",
"50 Coins": "50 Coins",
"6 minutes": "6 minutes",
"70": "70",
"8 minutes": "8 minutes",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.",
@ -817,5 +817,14 @@
"Buy Habib Coins": "Buy Habib Coins",
"Subscription required": "Subscription required",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "To view profiles, you need a subscription. Each subscription includes up to {count} matches.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users",
"{count} case introductions remaining of your subscription.": "{count} case introductions remaining of your subscription.",
"{days} days and {count} case introductions remaining of your subscription.": "{days} days and {count} case introductions remaining of your subscription.",
"You do not have enough Habib Coins": "You do not have enough Habib Coins",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content",
"Do you have a discount code?": "Do you have a discount code?",
"Enter discount code...": "Enter discount code...",
"Apply": "Apply",
"Clear": "Clear",
"This field is required": "This field is required"
}

18
src/translations/locales/es.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Opciones de ambiente religioso familiar * **Religioso y estrictamente observante:** Esto especifica una familia altamente dedicada a realizar todos los **deberes obligatorios**, mantener estrictamente **límites religiosos** (como las reglas de Mahram) y defender **rituales y enseñanzas religiosas** en todos los aspectos de la vida. * **Religioso (observante de las obligaciones):** Esto indica una familia comprometida con los **deberes religiosos** básicos (como la oración y el ayuno) y la **ética islámica**, que vive dentro de los marcos estándar de una sociedad religiosa. * **Tradicional (Respetuoso de los Valores Religiosos):** Esto describe una familia que tiene devoción a los valores morales y **respeta la religión**, pero no puede ejecutar estrictamente cada **ley u obligación religiosa** específica. * **No religioso/Secular:** Esto representa una familia donde **los rituales y marcos religiosos** no influyen significativamente en su **estilo de vida, relaciones o decisiones** diarias, a pesar de tener un respeto general por la religión.",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 a 170",
"170 to 180": "170 a 180",
"175": "175",
"180 to 190": "180 a 190",
"2": "2",
"2 minutes": "2 minutos",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 minutos",
"50 Coins": "50 Coins",
"6 minutes": "6 minutos",
"70": "70",
"8 minutes": "8 minutos",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "No se requiere una dirección precisa. Sólo el área general donde vive es suficiente, como la ciudad, región, vecindario o ciudad importante más cercana.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "Comprar monedas Habib",
"Subscription required": "Suscripción requerida",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Para ver perfiles, necesitas una suscripción. Cada suscripción incluye hasta {count} coincidencias.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Para ver perfiles, se requiere una suscripción. Esto ayuda a cubrir nuestros servicios de soporte y emparejamiento y aplica solo a usuarios masculinos"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Para ver perfiles, se requiere una suscripción. Esto ayuda a cubrir nuestros servicios de soporte y emparejamiento y aplica solo a usuarios masculinos",
"More detail": "Más detalles",
"{count} case introductions remaining of your subscription.": "Quedan {count} presentaciones de casos de tu suscripción.",
"{days} days and {count} case introductions remaining of your subscription.": "Quedan {days} días y {count} presentaciones de casos de tu suscripción.",
"You do not have enough Habib Coins": "No tienes suficientes Monedas Habib",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Agrega más Moneda Habib a tu saldo (o usa un código de descuento) para poder acceder a este contenido",
"Do you have a discount code?": "¿Tienes un código de descuento?",
"Enter discount code...": "Introduce el código de descuento...",
"Apply": "Aplicar",
"Clear": "Borrar",
"This field is required": "Este campo es obligatorio"
}

17
src/translations/locales/fa.json

@ -1,4 +1,7 @@
{
"2": "۲",
"70": "۷۰",
"175": "۱۷۵",
"### Family Religious Atmosphere Options * **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life. * **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society. * **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation. * **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### راهنمای گزینه‌های فضای مذهبی خانواده * **مذهبی و کاملاً مقید:** خانواده‌ای که تقید بسیار بالایی به انجام تمام واجبات دارد، حدود شرعی (مانند محرم و نامحرم) را به شدت رعایت می‌کند و آداب و مناسک مذهبی در تمام شئون زندگی آن‌ها جریان دارد. * **مذهبی (مقید به واجبات):** خانواده‌ای که متعهد به واجبات اصلی مذهبی (مانند نماز و روزه) و اخلاق اسلامی است و در چارچوب‌های متعارف یک جامعه متدین زندگی می‌کند. * **سنتی (محترم به ارزش‌های دینی):** خانواده‌ای که به ارزش‌های اخلاقی پایبند است و به دین احترام می‌گذارد، اما ممکن است تمام احکام و واجبات مذهبی را به طور دقیق و کامل اجرا نکند. * **غیرمذهبی / عرفی:** خانواده‌ای که مناسک و چارچوب‌های مذهبی تاثیر تعیین‌کننده‌ای بر سبک زندگی، ارتباطات یا تصمیم‌گیری‌های روزمره‌شان ندارد، هرچند ممکن است احترامی کلی برای مذهب قائل باشند.",
"(Complete Required Forms)": "(تکمیل فرم‌های ضروری)",
"(after 2 days)": "(بعد از ۲ روز)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "۱. شرایط عضویت و صلاحیت",
"160 to 170": "۱۶۰ تا ۱۷۰",
"170 to 180": "۱۷۰ تا ۱۸۰",
"175": "۱۷۵",
"180 to 190": "۱۸۰ تا ۱۹۰",
"2": "۲",
"2 minutes": "۲ دقیقه",
"2. Privacy and Data Management": "۲. حریم خصوصی و مدیریت داده‌ها",
"25-30": "۲۵-۳۰",
@ -24,7 +25,6 @@
"5 minutes": "۵ دقیقه",
"50 Coins": "۵۰ سکه",
"6 minutes": "۶ دقیقه",
"70": "۷۰",
"8 minutes": "۸ دقیقه",
"A Path to Heavenly Marriage": "مسیری برای ازدواج آسمانی",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "نیازی به آدرس دقیق نیست. فقط محدوده کلی محل زندگی کافی است؛ مثلاً نام شهر، منطقه، ناحیه یا نزدیکترین شهر بزرگ.",
@ -817,5 +817,14 @@
"Buy Habib Coins": "خرید سکه حبیب",
"Subscription required": "اشتراک الزامی است",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "برای مشاهده پروفایل‌ها، نیاز به اشتراک دارید. هر اشتراک شامل حداکثر {count} همسان‌یابی است.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "برای مشاهده پروفایل‌ها، اشتراک الزامی است. این هزینه صرف خدمات پشتیبانی و همسان‌یابی می‌شود و فقط شامل کاربران مرد است"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "برای مشاهده پروفایل‌ها، اشتراک الزامی است. این هزینه صرف خدمات پشتیبانی و همسان‌یابی می‌شود و فقط شامل کاربران مرد است",
"{count} case introductions remaining of your subscription.": "{count} معرفی از اعتبار اشتراک شما باقی مانده است.",
"{days} days and {count} case introductions remaining of your subscription.": "{days} روز و {count} معرفی از اعتبار اشتراک شما باقی مانده است.",
"You do not have enough Habib Coins": "شما حبیب کوین کافی ندارید",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "لطفاً حبیب کوین بیشتری به موجودی خود اضافه کنید (یا از کد تخفیف استفاده کنید) تا بتوانید به این محتوا دسترسی پیدا کنید",
"Do you have a discount code?": "آیا کد تخفیف دارید؟",
"Enter discount code...": "کد تخفیف را وارد کنید...",
"Apply": "اعمال",
"Clear": "پاک کردن",
"This field is required": "این فیلد ضروری است"
}

18
src/translations/locales/fr.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Options d'ambiance religieuse familiale * **Religieux et strictement observant :** Ceci spécifie une famille hautement dévouée à l'accomplissement de toutes les **tâches obligatoires**, au maintien strict des **limites religieuses** (telles que les règles du Mahram) et au respect des **rituels et enseignements religieux** dans tous les aspects de la vie. * **Religieux (observateur des obligations) :** Cela indique une famille engagée dans les **devoirs religieux** fondamentaux (tels que la prière et le jeûne) et dans l'**éthique islamique**, vivant dans les cadres standard d'une société religieuse. * **Traditionnel (respectueux des valeurs religieuses) :** Ceci décrit une famille qui est attachée aux valeurs morales et **respecte la religion**, mais ne peut pas exécuter strictement chaque **loi religieuse** ou obligation spécifique. * **Non religieux/laïc :** Cela représente une famille dans laquelle les **rituels et cadres religieux** n'influencent pas de manière significative leur **mode de vie, leurs relations ou leurs décisions** quotidiennes, malgré un respect général pour la religion.",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 à 170",
"170 to 180": "170 à 180",
"175": "175",
"180 to 190": "180 à 190",
"2": "2",
"2 minutes": "2 minutes",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 minutes",
"50 Coins": "50 Coins",
"6 minutes": "6 minutes",
"70": "70",
"8 minutes": "8 minutes",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "Une adresse précise n’est pas requise. Seule la zone générale où vous habitez est suffisante, comme la ville, la région, le quartier ou la grande ville la plus proche.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "Acheter des pièces Habib",
"Subscription required": "Abonnement requis",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Pour voir les profils, vous avez besoin d'un abonnement. Chaque abonnement comprend jusqu'à {count} correspondances.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Pour voir les profils, un abonnement est requis. Cela aide à couvrir nos services de support et de mise en relation et s'applique uniquement aux utilisateurs masculins"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Pour voir les profils, un abonnement est requis. Cela aide à couvrir nos services de support et de mise en relation et s'applique uniquement aux utilisateurs masculins",
"More detail": "Plus de détails",
"{count} case introductions remaining of your subscription.": "Il reste {count} présentations dans votre abonnement.",
"{days} days and {count} case introductions remaining of your subscription.": "Il reste {days} jours et {count} présentations dans votre abonnement.",
"You do not have enough Habib Coins": "Vous n'avez pas assez de Pièces Habib",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Veuillez ajouter plus de Pièce Habib à votre solde (ou utilisez un code de réduction) pour pouvoir accéder à ce contenu",
"Do you have a discount code?": "Avez-vous un code de réduction ?",
"Enter discount code...": "Entrez le code de réduction...",
"Apply": "Appliquer",
"Clear": "Effacer",
"This field is required": "Ce champ est requis"
}

18
src/translations/locales/gu.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### કૌટુંબિક ધાર્મિક વાતાવરણના વિકલ્પો * **ધાર્મિક અને ચુસ્તપણે પાલન કરનાર:** આ એક કુટુંબનો ઉલ્લેખ કરે છે જે તમામ **ફરજિયાત ફરજો** કરવા માટે ખૂબ જ સમર્પિત હોય, **ધાર્મિક સીમાઓ** (જેમ કે મહરમના નિયમો)નું સખતપણે પાલન કરે અને જીવનના **ધાર્મિક વિધિઓ અને ઉપદેશોનું પાલન કરે**. * **ધાર્મિક (જવાબદારીઓનું પાલન કરનાર):** આ ધાર્મિક સમાજના માનક માળખામાં રહેતા **ધાર્મિક ફરજો** (જેમ કે પ્રાર્થના અને ઉપવાસ) અને **ઈસ્લામિક નૈતિકતા** માટે પ્રતિબદ્ધ કુટુંબ સૂચવે છે. * **પરંપરાગત (ધાર્મિક મૂલ્યોનું સન્માન):** આ એવા કુટુંબનું વર્ણન કરે છે જે નૈતિક મૂલ્યો પ્રત્યે નિષ્ઠા ધરાવે છે અને **ધર્મનું સન્માન કરે છે**, પરંતુ દરેક ચોક્કસ **ધાર્મિક કાયદા** અથવા જવાબદારીને સખત રીતે ચલાવી શકતા નથી. * **બિન-ધાર્મિક / બિનસાંપ્રદાયિક:** આ એવા કુટુંબનું પ્રતિનિધિત્વ કરે છે જ્યાં **ધાર્મિક ધાર્મિક વિધિઓ અને માળખા** તેમની દૈનિક **જીવનશૈલી, સંબંધો અથવા નિર્ણયો**ને નોંધપાત્ર રીતે પ્રભાવિત કરતા નથી, ધર્મ પ્રત્યે સામાન્ય સન્માન હોવા છતાં.",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 થી 170",
"170 to 180": "170 થી 180",
"175": "175",
"180 to 190": "180 થી 190",
"2": "2",
"2 minutes": "2 મિનિટ",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 મિનિટ",
"50 Coins": "50 Coins",
"6 minutes": "6 મિનિટ",
"70": "70",
"8 minutes": "8 મિનિટ",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "ચોક્કસ સરનામું જરૂરી નથી. તમે જ્યાં રહો છો તેનો સામાન્ય વિસ્તાર પૂરતો છે, જેમ કે શહેર, પ્રદેશ, પડોશ અથવા નજીકનું મોટું શહેર.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "હબીબ સિક્કા ખરીદો",
"Subscription required": "સબ્સ્ક્રિપ્શન જરૂરી",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "પ્રોફાઇલ જોવા માટે તમારે સબ્સ્ક્રિપ્શનની જરૂર છે. દરેક સબ્સ્ક્રિપ્શનમાં {count} મેચ સુધીનો સમાવેશ થાય છે.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "પ્રોફાઇલ જોવા માટે સબ્સ્ક્રિપ્શન જરૂરી છે. આ અમારી સપોર્ટ અને મેચિંગ સેવાઓને આવરી લેવામાં મદદ કરે છે અને ફક્ત પુરુષ વપરાશકર્તાઓને લાગુ પડે છે"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "પ્રોફાઇલ જોવા માટે સબ્સ્ક્રિપ્શન જરૂરી છે. આ અમારી સપોર્ટ અને મેચિંગ સેવાઓને આવરી લેવામાં મદદ કરે છે અને ફક્ત પુરુષ વપરાશકર્તાઓને લાગુ પડે છે",
"More detail": "વધુ વિગતો",
"{count} case introductions remaining of your subscription.": "તમારા સબ્સ્ક્રિપ્શનના {count} પરિચય બાકી છે.",
"{days} days and {count} case introductions remaining of your subscription.": "તમારા સબ્સ્ક્રિપ્શનના {days} દિવસ અને {count} પરિચય બાકી છે.",
"You do not have enough Habib Coins": "તમારી પાસે પૂરતા હબીબ કોઈન્સ નથી",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "કૃપા કરીને તમારા બેલેન્સમાં વધુ હબીબ સિક્કો ઉમેરો (અથવા ડિસ્કાઉન્ટ કોડ વાપરો) જેથી તમે આ સામગ્રી ઍક્સેસ કરી શકો",
"Do you have a discount code?": "શું તમારી પાસે ડિસ્કાઉન્ટ કોડ છે?",
"Enter discount code...": "ડિસ્કાઉન્ટ કોડ દાખલ કરો...",
"Apply": "લાગુ કરો",
"Clear": "સાફ કરો",
"This field is required": "આ ક્ષેત્ર આવશ્યક છે"
}

18
src/translations/locales/ha.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Zaɓuɓɓukan Yanayin Addini na Iyali * **Mai Kula da Addini da Tsare-tsare:** Wannan yana ƙayyadaddun iyali da suka sadaukar da kansu don aiwatar da dukkan abubuwan da suka wajaba**, da kiyaye ** iyakokin addini** (kamar dokokin Mahram), da kiyaye ** ladubban addini da koyarwar** a kowane fanni na rayuwa. * **Mai kiyaye Addini:** Wannan yana nuni da iyali da suka himmatu wajen aiwatar da muhimman ayyuka na addini** (kamar sallah da azumi) da kuma *Ladubban Musulunci**, suna rayuwa ne bisa tsarin al'umma na addini. * **Al'ada (Mutunta Darajojin Addini):** Wannan yana siffanta iyali mai riko da kyawawan dabi'u da *girmama addini**, amma ba za'a zartar da kowace takamaiman doka ta addini** ko farilla ba. * **Mai Addini/Na Zamani:** Wannan yana wakiltar dangi ne da **al'adun addini da tsare-tsare** ba sa tasiri sosai a rayuwar su ta yau da kullun, dangantakarsu, ko yanke hukunci**, duk da girmama addini gaba ɗaya.",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 zuwa 170",
"170 to 180": "170 zuwa 180",
"175": "175",
"180 to 190": "180 zuwa 190",
"2": "2",
"2 minutes": "Minti 2",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "Minti 5",
"50 Coins": "50 Coins",
"6 minutes": "Minti 6",
"70": "70",
"8 minutes": "Minti 8",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "Ba a buƙatar takamaiman adireshin. Babban yankin inda kuke zama ya wadatar, kamar birni, yanki, yanki, ko babban birni mafi kusa.",
@ -767,5 +767,15 @@
"Buy Habib Coins": "Sayi Tsabar Kuɗin Habib",
"Subscription required": "Ana buƙatar biyan kuɗi",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Don ganin bayanan mutane, kuna buƙatar biyan kuɗi. Kowane biyan kuɗi ya ƙunshi har zuwa {count} daidaituwa.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Don ganin bayanan mutane, ana buƙatar biyan kuɗi. Wannan yana taimakawa wajen biyan kuɗin sabis na tallafi da daidaitawa kuma yana shafi maza kawai"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Don ganin bayanan mutane, ana buƙatar biyan kuɗi. Wannan yana taimakawa wajen biyan kuɗin sabis na tallafi da daidaitawa kuma yana shafi maza kawai",
"More detail": "Ƙarin cikakkun bayanai",
"{count} case introductions remaining of your subscription.": "Gabatarwa {count} suka rage a cikin rijistarka.",
"{days} days and {count} case introductions remaining of your subscription.": "Kwanaki {days} da gabatarwa {count} suka rage a cikin rijistarka.",
"You do not have enough Habib Coins": "Ba ku da isasshen Tsabar Habib",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Da fatan za ka ƙara Tsabar Habib a ma'auninka (ko yi amfani da lambar rangwame) don samun damar wannan abun ciki",
"Do you have a discount code?": "Kana da lambar rangwame?",
"Enter discount code...": "Shigar da lambar rangwame...",
"Apply": "Yi amfani",
"Clear": "Share",
"This field is required": "Ana buƙatar wannan filin"
}

21
src/translations/locales/he.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "קנה מטבעות חביב",
"Subscription required": "נדרש מנוי",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "כדי לצפות בפרופילים, יש צורך במנוי. כל מנוי כולל עד {count} התאמות.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "כדי לצפות בפרופילים, נדרש מנוי. הדבר עוזר לכסות את שירותי התמיכה וההתאמה שלנו וחל רק על משתמשים גברים"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "כדי לצפות בפרופילים, נדרש מנוי. הדבר עוזר לכסות את שירותי התמיכה וההתאמה שלנו וחל רק על משתמשים גברים",
"More detail": "פרטים נוספים",
"Subscription Status": "סטטוס מנוי",
"{days} days remaining of your subscription.": "נותרו {days} ימים למנוי שלך.",
"No Active Subscription": "אין מנוי פעיל",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "כרגע אין לך מנוי פעיל. הפעלת מנוי אפשרית רק כאשר המקרה הראשון יוצג בפניך.",
"Renew Subscription": "חידוש מנוי",
"Renewing...": "מחדש...",
"Payment successful": "התשלום בוצע בהצלחה",
"Insufficient coin balance. Please recharge your account.": "יתרת מטבעות אינה מספיקה. אנא טען את חשבונך.",
"Got it": "הבנתי",
"{count} case introductions remaining of your subscription.": "נותרו {count} הצעות שידוך למנוי שלך.",
"{days} days and {count} case introductions remaining of your subscription.": "נותרו {days} ימים ו-{count} הצעات שידוך למנוי שלך.",
"You do not have enough Habib Coins": "אין לך מספיק מטבעות חביב",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "אנא הוסף עוד מטבעות חביב ליתרה שלך (או השתמש בקוד הנחה), כדי שתוכל להשתמש בהם לגישה לתוכן זה",
"Do you have a discount code?": "Do you have a discount code?",
"Enter discount code...": "Enter discount code...",
"Apply": "Apply",
"Clear": "Clear",
"This field is required": "This field is required"
}

18
src/translations/locales/hi.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### पारिवारिक धार्मिक माहौल के विकल्प * **धार्मिक और सख्ती से पालन करने वाले:** यह एक ऐसे परिवार को निर्दिष्ट करता है जो सभी **अनिवार्य कर्तव्यों** को निभाने, **धार्मिक सीमाओं** (जैसे महरम नियमों) को सख्ती से बनाए रखने और जीवन के सभी पहलुओं में **धार्मिक अनुष्ठानों और शिक्षाओं** को बनाए रखने के लिए अत्यधिक समर्पित है। * **धार्मिक (दायित्वों का पालन करने वाला):** यह एक धार्मिक समाज के मानक ढांचे के भीतर रहने वाले, मुख्य **धार्मिक कर्तव्यों** (जैसे प्रार्थना और उपवास) और **इस्लामी नैतिकता** के लिए प्रतिबद्ध परिवार को इंगित करता है। * **पारंपरिक (धार्मिक मूल्यों का सम्मान करने वाला):** यह एक ऐसे परिवार का वर्णन करता है जो नैतिक मूल्यों के प्रति समर्पण रखता है और **धर्म का सम्मान करता है**, लेकिन हर विशिष्ट **धार्मिक कानून** या दायित्व को सख्ती से निष्पादित नहीं कर सकता है। * **गैर-धार्मिक / धर्मनिरपेक्ष:** यह एक ऐसे परिवार का प्रतिनिधित्व करता है जहां धर्म के प्रति सामान्य सम्मान रखने के बावजूद **धार्मिक अनुष्ठान और ढांचे** उनकी दैनिक **जीवनशैली, रिश्तों या निर्णयों** को महत्वपूर्ण रूप से प्रभावित नहीं करते हैं।",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 से 170",
"170 to 180": "170 से 180",
"175": "175",
"180 to 190": "180 से 190",
"2": "2",
"2 minutes": "2 मिनट",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 मिनट",
"50 Coins": "50 Coins",
"6 minutes": "6 मिनट",
"70": "70",
"8 minutes": "8 मिनट",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "सटीक पते की आवश्यकता नहीं है. जहां आप रहते हैं उसका सामान्य क्षेत्र ही पर्याप्त है, जैसे शहर, क्षेत्र, पड़ोस, या निकटतम प्रमुख शहर।",
@ -767,5 +767,15 @@
"Buy Habib Coins": "हबीब सिक्के खरीदें",
"Subscription required": "सदस्यता आवश्यक",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "प्रोफाइल देखने के लिए सदस्यता की आवश्यकता है। प्रत्येक सदस्यता में {count} मैच शामिल हैं।",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "प्रोफाइल देखने के लिए सदस्यता आवश्यक है। यह हमारी सहायता और मिलान सेवाओं को कवर करने में मदद करती है और केवल पुरुष उपयोगकर्ताओं पर लागू होती है"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "प्रोफाइल देखने के लिए सदस्यता आवश्यक है। यह हमारी सहायता और मिलान सेवाओं को कवर करने में मदद करती है और केवल पुरुष उपयोगकर्ताओं पर लागू होती है",
"More detail": "अधिक विवरण",
"{count} case introductions remaining of your subscription.": "आपके सब्सक्रिप्शन में {count} परिचय शेष हैं।",
"{days} days and {count} case introductions remaining of your subscription.": "आपके सब्सक्रिप्शन में {days} दिन और {count} परिचय शेष हैं।",
"You do not have enough Habib Coins": "आपके पास पर्याप्त हबीब कॉइन नहीं हैं",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "कृपया अपने बैलेंस में और हबीब कॉइन जोड़ें (या छूट कोड का उपयोग करें) ताकि आप इस सामग्री तक पहुँच सकें",
"Do you have a discount code?": "क्या आपके पास छूट कोड है?",
"Enter discount code...": "छूट कोड दर्ज करें...",
"Apply": "लागू करें",
"Clear": "साफ़ करें",
"This field is required": "यह फ़ील्ड आवश्यक है"
}

21
src/translations/locales/id.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "Beli Koin Habib",
"Subscription required": "Langganan diperlukan",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Untuk melihat profil, Anda memerlukan langganan. Setiap langganan mencakup hingga {count} kecocokan.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Untuk melihat profil, diperlukan langganan. Ini membantu menutupi layanan dukungan dan pencocokan kami dan hanya berlaku untuk pengguna pria"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Untuk melihat profil, diperlukan langganan. Ini membantu menutupi layanan dukungan dan pencocokan kami dan hanya berlaku untuk pengguna pria",
"More detail": "Detail lebih lanjut",
"Subscription Status": "Status Langganan",
"{days} days remaining of your subscription.": "Sisa {days} hari dari langganan Anda.",
"No Active Subscription": "Tidak Ada Langganan Aktif",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "Saat ini Anda tidak memiliki langganan aktif. Aktivasi langganan hanya dapat dilakukan saat kasus pertama diperkenalkan kepada Anda.",
"Renew Subscription": "Perpanjang Langganan",
"Renewing...": "Memperpanjang...",
"Payment successful": "Pembayaran berhasil",
"Insufficient coin balance. Please recharge your account.": "Saldo koin tidak mencukupi. Silakan isi ulang akun Anda.",
"Got it": "Mengerti",
"{count} case introductions remaining of your subscription.": "Sisa {count} pengenalan profil dari langganan Anda.",
"{days} days and {count} case introductions remaining of your subscription.": "Sisa {days} hari dan {count} pengenalan profil dari langganan Anda.",
"You do not have enough Habib Coins": "Anda tidak memiliki cukup Koin Habib",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Silakan tambahkan lebih banyak Koin Habib ke saldo Anda (atau gunakan kode diskon) agar dapat mengakses konten ini",
"Do you have a discount code?": "Apakah Anda punya kode diskon?",
"Enter discount code...": "Masukkan kode diskon...",
"Apply": "Terapkan",
"Clear": "Hapus",
"This field is required": "Bidang ini wajib diisi"
}

21
src/translations/locales/ks.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "حبیب سِکّہٕ خریٖد کٔرِو",
"Subscription required": "رکنیت ضروٗری ہے",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "پروفایلس وُچھنہٕ خٲطرٕ رکنیت ضروٗری ہے۔ ہر رکنیت مٮ۪ں {count} میچ شامل ہِن۔",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "پروفایلس وُچھنہٕ خٲطرٕ رکنیت ضروٗری ہے۔ یِہ ساہٕ سپورٹ تہٕ میچِنگ خِدمات ہٕنٛد کَرنٕ مدد کَرِتھ تہٕ صرف مَرد صارفین پؠٹھ لاگوٗ ہے"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "پروفایلس وُچھنہٕ خٲطرٕ رکنیت ضروٗری ہے۔ یِہ ساہٕ سپورٹ تہٕ میچِنگ خِدمات ہٕنٛد کَرنٕ مدد کَرِتھ تہٕ صرف مَرد صارفین پؠٹھ لاگوٗ ہے",
"More detail": "مزید تفصیلات",
"Subscription Status": "سبسکرپشن حالت",
"{days} days remaining of your subscription.": "تہندِ سبسکرپشن کین {days} دوہ باقی۔",
"No Active Subscription": "کانہہ ایکٹو سبسکرپشن نہ",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "تہیہ نش چھ نہ ونکہ کانہہ ایکٹو سبسکرپشن۔ سبسکرپشن چالو گژھن صرف پتہ ممکن ییلہ گوڈنیک کیس تہیہ نش پیش ییہ کرنہ۔",
"Renew Subscription": "سبسکرپشن رینیو کٔریو",
"Renewing...": "رینیو گژھان...",
"Payment successful": "ادائیگی کامیابی سان مکمل گٔیہ",
"Insufficient coin balance. Please recharge your account.": "کوائن بیلنس چھ نہ کافی۔ مہربانی کٔرتھ پنن اکاؤنٹ ریچارج کٔریو۔",
"Got it": "بوزم",
"{count} case introductions remaining of your subscription.": "تہندِ سبسکرپشن کین {count} تعارف باقی۔",
"{days} days and {count} case introductions remaining of your subscription.": "تہندِ سبسکرپشن کین {days} دوہ تہ {count} تعارف باقی۔",
"You do not have enough Habib Coins": "تۄہی کول کافی حبیب کوین نہ چھ",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "مِہربانی کٔرِتھ پَنِن بیلَنس مَنٛز مَزیٖد حبیب کوین شامِل کٔرِو (یا ڈِسکاوُنٹ کوٗڈ اِستعمال کٔرِو) تاکِ تۄہِ اِس مَواد تٲم رَسایی حاصِل کٔرِ سَکِو",
"Do you have a discount code?": "کیٛا تۄہہِ نِش ڈِسکاوُنٹ کوٗڈ چھُ؟",
"Enter discount code...": "ڈِسکاوُنٹ کوٗڈ داخِل کٔرِو...",
"Apply": "لاگوٗ کٔرِو",
"Clear": "صاف کٔرِو",
"This field is required": "یہ فیلڈ ضروری چھُ"
}

21
src/translations/locales/pt.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "Comprar moedas Habib",
"Subscription required": "Assinatura necessária",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Para ver perfis, você precisa de uma assinatura. Cada assinatura inclui até {count} correspondências.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Para ver perfis, é necessária uma assinatura. Isso ajuda a cobrir nossos serviços de suporte e correspondência e se aplica apenas a usuários masculinos"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Para ver perfis, é necessária uma assinatura. Isso ajuda a cobrir nossos serviços de suporte e correspondência e se aplica apenas a usuários masculinos",
"More detail": "Mais detalhes",
"Subscription Status": "Status da Assinatura",
"{days} days remaining of your subscription.": "Restam {days} dias da sua assinatura.",
"No Active Subscription": "Nenhuma Assinatura Ativa",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "No momento, você não possui uma assinatura ativa. A ativação da assinatura só é possível quando o seu primeiro caso for apresentado a você.",
"Renew Subscription": "Renovar Assinatura",
"Renewing...": "Renovando...",
"Payment successful": "Pagamento efetuado com sucesso",
"Insufficient coin balance. Please recharge your account.": "Saldo de moedas insuficiente. Por favor, recarregue sua conta.",
"Got it": "Entendi",
"{count} case introductions remaining of your subscription.": "Restam {count} apresentações na sua assinatura.",
"{days} days and {count} case introductions remaining of your subscription.": "Restam {days} dias e {count} apresentações na sua assinatura.",
"You do not have enough Habib Coins": "Você não tem Moedas Habib suficientes",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Adicione mais Moeda Habib ao seu saldo (ou use um código de desconto) para poder acessar este conteúdo",
"Do you have a discount code?": "Você tem um código de desconto?",
"Enter discount code...": "Insira o código de desconto...",
"Apply": "Aplicar",
"Clear": "Limpar",
"This field is required": "Este campo é obrigatório"
}

17
src/translations/locales/ru.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options * **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life. * **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society. * **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation. * **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### Варианты религиозной атмосферы в семье * **Религиозные и строго соблюдающие**: это означает, что семья очень предана выполнению всех **обязательных обязанностей**, строго соблюдает **религиозные границы** (например, правила Махрама) и поддерживает **религиозные ритуалы и учения** во всех аспектах жизни. * **Религиозность (соблюдение обязательств):** Это указывает на семью, приверженную основным **религиозным обязанностям** (таким как молитва и пост) и **исламскую этику**, живущую в стандартных рамках религиозного общества. * **Традиционный (с уважением к религиозным ценностям):** описывает семью, которая придерживается моральных ценностей и **уважает религию**, но не может строго соблюдать каждый конкретный **религиозный закон** или обязательства. * **Нерелигиозные/светские.** Это семья, в которой **религиозные ритуалы и рамки** не оказывают существенного влияния на их повседневный **образ жизни, отношения или решения**, несмотря на общее уважение к религии.",
"(Complete Required Forms)": "(Заполните обязательные формы)",
"(after 2 days)": "(через 2 дня)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Требования к участию и членству",
"160 to 170": "от 160 до 170",
"170 to 180": "от 170 до 180",
"175": "175",
"180 to 190": "от 180 до 190",
"2": "2",
"2 minutes": "2 минуты",
"2. Privacy and Data Management": "2. Конфиденциальность и управление данными",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 минут",
"50 Coins": "50 монет",
"6 minutes": "6 минут",
"70": "70",
"8 minutes": "8 минут",
"A Path to Heavenly Marriage": "Путь к небесному браку",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "Точный адрес не требуется. Достаточно указать только общую территорию вашего проживания, например город, регион, район или ближайший крупный город.",
@ -772,5 +772,14 @@
"Buy Habib Coins": "Купить монеты Habib",
"Subscription required": "Требуется подписка",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Для просмотра профилей необходима подписка. Каждая подписка включает до {count} совпадений.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Для просмотра профилей требуется подписка. Это помогает покрыть наши услуги поддержки и подбора и применяется только к мужчинам"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Для просмотра профилей требуется подписка. Это помогает покрыть наши услуги поддержки и подбора и применяется только к мужчинам",
"{count} case introductions remaining of your subscription.": "Осталось {count} знакомств по вашей подписке.",
"{days} days and {count} case introductions remaining of your subscription.": "Осталось {days} дней и {count} знакомств по вашей подписке.",
"You do not have enough Habib Coins": "У вас недостаточно Хабиб Коины",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Пожалуйста, добавьте больше Хабиб Коин на ваш баланс (или используйте код скидки), чтобы получить доступ к этому контенту",
"Do you have a discount code?": "У вас есть код скидки?",
"Enter discount code...": "Введите код скидки...",
"Apply": "Применить",
"Clear": "Очистить",
"This field is required": "Это поле обязательно"
}

21
src/translations/locales/sw.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "Nunua Sarafu za Habib",
"Subscription required": "Usajili unahitajika",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Ili kuona wasifu, unahitaji usajili. Kila usajili unajumuisha hadi mechi {count}.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Ili kuona wasifu, usajili unahitajika. Hii husaidia kugharamia huduma zetu za usaidizi na kuoanisha na inatumika kwa watumiaji wa kiume pekee"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Ili kuona wasifu, usajili unahitajika. Hii husaidia kugharamia huduma zetu za usaidizi na kuoanisha na inatumika kwa watumiaji wa kiume pekee",
"More detail": "Maelezo zaidi",
"Subscription Status": "Hali ya Usajili",
"{days} days remaining of your subscription.": "Siku {days} zimesalia za usajili wako.",
"No Active Subscription": "Hakuna Usajili Amilifu",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "Kwa sasa huna usajili amilifu. Uamilisho wa usajili unawezekana tu wakati kesi yako ya kwanza itakapotambulishwa kwako.",
"Renew Subscription": "Sasisha Usajili",
"Renewing...": "Inasasisha...",
"Payment successful": "Malipo yamefanikiwa",
"Insufficient coin balance. Please recharge your account.": "Salio la sarafu halitoshi. Tafadhali ongeza salio kwenye akaunti yako.",
"Got it": "Nimeelewa",
"{count} case introductions remaining of your subscription.": "Zimesalia utambulisho {count} kwenye usajili wako.",
"{days} days and {count} case introductions remaining of your subscription.": "Zimesalia siku {days} na utambulisho {count} kwenye usajili wako.",
"You do not have enough Habib Coins": "Huna sarafu za kutosha za Habib",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Tafadhali ongeza Sarafu ya Habib zaidi kwenye salio lako (au tumia msimbo wa punguzo) ili uweze kufikia maudhui haya",
"Do you have a discount code?": "Je, una msimbo wa punguzo?",
"Enter discount code...": "Ingiza msimbo wa punguzo...",
"Apply": "Tekeleza",
"Clear": "Futa",
"This field is required": "Sehemu hii inahitajika"
}

21
src/translations/locales/tg.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "Харидани тангаҳои Ҳабиб",
"Subscription required": "Обуна лозим аст",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Барои дидани профилҳо, ба обуна ниёз доред. Ҳар як обуна то {count} мувофиқатро дар бар мегирад.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Барои дидани профилҳо, обуна лозим аст. Ин барои пардохти хидматҳои дастгирӣ ва мувофиқат кӯмак мекунад ва танҳо ба корбарони мард дахл дорад"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Барои дидани профилҳо, обуна лозим аст. Ин барои пардохти хидматҳои дастгирӣ ва мувофиқат кӯмак мекунад ва танҳо ба корбарони мард дахл дорад",
"More detail": "Тафсилоти бештар",
"Subscription Status": "Вазъияти обуна",
"{days} days remaining of your subscription.": "{days} рӯз аз эътибори обунаи шумо боқӣ мондааст.",
"No Active Subscription": "Обунаи фаъол нест",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "Шумо айни замон обунаи фаъол надоред. Фаъолсозии обуна танҳо пас аз муаррифии аввалин мавриди издивоҷ имконпазир аст.",
"Renew Subscription": "Тамдиди обуна",
"Renewing...": "Дар ҳоли тамдид...",
"Payment successful": "Пардохт бо мувафفاқият анҷом шуд",
"Insufficient coin balance. Please recharge your account.": "Бақияи тангаҳо кофӣ нест. Лутфан ҳисоби худро пур кунед.",
"Got it": "Фаҳмидам",
"{count} case introductions remaining of your subscription.": "{count} муаррифӣ аз эътибори обунаи шумо боқӣ мондааст.",
"{days} days and {count} case introductions remaining of your subscription.": "{days} рӯз ва {count} муаррифӣ аз эътибори обунаи шумо боқӣ мондааст.",
"You do not have enough Habib Coins": "Шумо Тангаҳои Ҳабиб-и кофӣ надоред",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Лутфан ба баланси худ Тангаи Ҳабиб бештар илова кунед (ё коди тахфиф истифода кунед) то ба ин мӯҳтаво дастрасӣ пайдо кунед",
"Do you have a discount code?": "Оё шумо коди тахфиф доред?",
"Enter discount code...": "Коди тахфифро ворид кунед...",
"Apply": "Татбиқ",
"Clear": "Покардан",
"This field is required": "Ин майдон ҳатмист"
}

21
src/translations/locales/tr.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "Habib Jetonu Satın Al",
"Subscription required": "Abonelik gerekli",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Profilleri görüntülemek için aboneliğe ihtiyacınız var. Her abonelik {count} eşleşme içerir.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Profilleri görüntülemek için abonelik gereklidir. Bu, destek ve eşleştirme hizmetlerimizi karşılamaya yardımcı olur ve yalnızca erkek kullanıcılar için geçerlidir"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Profilleri görüntülemek için abonelik gereklidir. Bu, destek ve eşleştirme hizmetlerimizi karşılamaya yardımcı olur ve yalnızca erkek kullanıcılar için geçerlidir",
"More detail": "Daha fazla detay",
"Subscription Status": "Abonelik Durumu",
"{days} days remaining of your subscription.": "Aboneliğinizden kalan {days} gün.",
"No Active Subscription": "Aktif Abonelik Yok",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "Şu anda aktif bir aboneliğiniz bulunmamaktadır. Abonelik aktivasyonu yalnızca ilk eşleşmeniz size sunulduğunda mümkündür.",
"Renew Subscription": "Aboneliği Yenile",
"Renewing...": "Yenileniyor...",
"Payment successful": "Ödeme başarılı",
"Insufficient coin balance. Please recharge your account.": "Yetersiz bakiye. Lütfen hesabınızı doldurun.",
"Got it": "Anladım",
"{count} case introductions remaining of your subscription.": "Aboneliğinizden kalan {count} eşleşme tanıtımı.",
"{days} days and {count} case introductions remaining of your subscription.": "Aboneliğinizden kalan {days} gün ve {count} eşleşme tanıtımı.",
"You do not have enough Habib Coins": "Yeterli Habib Coin'iniz yok",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Bu içeriğe erişmek için bakiyenize daha fazla Habib Coin ekleyin (veya indirim kodu kullanın)",
"Do you have a discount code?": "İndirim kodunuz var mı?",
"Enter discount code...": "İndirim kodunu girin...",
"Apply": "Uygula",
"Clear": "Temizle",
"This field is required": "Bu alan gereklidir"
}

21
src/translations/locales/ul.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "د حبیب سکې واخلئ",
"Subscription required": "ګډون ته اړتیا ده",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "د پروفایلونو لیدلو لپاره، تاسو ته ګډون ته اړتیا ده. هر ګډون کې تر {count} میچونه شاملې دي.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "د پروفایلونو لیدلو لپاره ګډون ته اړتیا ده. دا زموږ د ملاتړ او میچ کولو خدماتو لپاره مرسته کوي او یوازې د نارینه کاروونکو لپاره پلي کیږي"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "د پروفایلونو لیدلو لپاره ګډون ته اړتیا ده. دا زموږ د ملاتړ او میچ کولو خدماتو لپاره مرسته کوي او یوازې د نارینه کاروونکو لپاره پلي کیږي",
"More detail": "تەپسىلاتى",
"Subscription Status": "مۇشتەرى بولۇش ھالىتى",
"{days} days remaining of your subscription.": "مۇشتەرىلىكىڭىزنىڭ قالغان كۈنى {days}.",
"No Active Subscription": "ئاكتىپ مۇشتەرىلىك يوق",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "نۆۋەتتە ئاكتىپ مۇشتەرىلىكىڭىز يوق. مۇشتەرىلىكنى قوزغىتىش پەقەت تۇنجى تاللاش سىزگە تونۇشتۇرۇلغاندىلا مۇمكىن بولىدۇ.",
"Renew Subscription": "مۇشتەرىلىكنى ئۇزارتىش",
"Renewing...": "ئۇزارتىلىۋاتىدۇ...",
"Payment successful": "تۆلەم مۇۋەپپەقىيەتلىك بولدى",
"Insufficient coin balance. Please recharge your account.": "تەڭگە قالدۇقى يېتەرسىز. ھېساباتىڭىزنى تولۇقلاڭ.",
"Got it": "چۈشەندىم",
"{count} case introductions remaining of your subscription.": "مۇشتەرىلىكىڭىزنىڭ قالغان تونۇشتۇرۇشى {count}.",
"{days} days and {count} case introductions remaining of your subscription.": "مۇشتەرىلىكىڭىزنىڭ قالغان كۈنى {days} ۋە تونۇشتۇرۇشى {count}.",
"You do not have enough Habib Coins": "Aap ke paas kafi Habib Coins nahi hain",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Baraaye meharbani apne balance mein mazeed Habib Coin shamil karein (ya discount code istemal karein) taake aap is mawaad tak rasaai haasil kar sakein",
"Do you have a discount code?": "Kya aap ke paas discount code hai?",
"Enter discount code...": "Discount code darj karein...",
"Apply": "Laagoo karein",
"Clear": "Saaf karein",
"This field is required": "Ye Field Zaroori Hai"
}

21
src/translations/locales/ur.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "حبیب سکے خریدیں",
"Subscription required": "رکنیت ضروری ہے",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "پروفائلز دیکھنے کے لیے رکنیت ضروری ہے۔ ہر رکنیت میں {count} میچ شامل ہیں۔",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "پروفائلز دیکھنے کے لیے رکنیت ضروری ہے۔ یہ ہماری سپورٹ اور میچنگ سروسز کی لاگت پوری کرنے میں مدد کرتا ہے اور صرف مرد صارفین پر لاگو ہوتا ہے"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "پروفائلز دیکھنے کے لیے رکنیت ضروری ہے۔ یہ ہماری سپورٹ اور میچنگ سروسز کی لاگت پوری کرنے میں مدد کرتا ہے اور صرف مرد صارفین پر لاگو ہوتا ہے",
"More detail": "مزید تفصیلات",
"Subscription Status": "سبسکرپشن کی صورتحال",
"{days} days remaining of your subscription.": "آپ کی سبسکرپشن کے {days} دن باقی ہیں۔",
"No Active Subscription": "کوئی فعال سبسکرپشن نہیں",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "اس وقت آپ کی کوئی فعال سبسکرپشن نہیں ہے۔ سبسکرپشن کی فعالیت صرف اس وقت ممکن ہے جب آپ کو پہلا کیس متعارف کرایا جائے۔",
"Renew Subscription": "سبسکرپشن کی تجدید کریں",
"Renewing...": "تجدید ہو رہی ہے...",
"Payment successful": "ادائیگی کامیاب رہی",
"Insufficient coin balance. Please recharge your account.": "سکے کا بیلنس ناکافی ہے۔ براہ کرم اپنا اکاؤنٹ ریچارج کریں۔",
"Got it": "سمجھ گیا",
"{count} case introductions remaining of your subscription.": "آپ کی سبسکرپشن کے {count} تعارف باقی ہیں۔",
"{days} days and {count} case introductions remaining of your subscription.": "آپ کی سبسکرپشن کے {days} دن اور {count} تعارف باقی ہیں۔",
"You do not have enough Habib Coins": "آپ کے پاس حبیب کوائنز ناکافی ہیں",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "براہ کرم اپنے بیلنس میں مزید حبیب کوین شامل کریں (یا ڈسکاؤنٹ کوڈ استعمال کریں) تاکہ آپ اس مواد تک رسائی حاصل کر سکیں",
"Do you have a discount code?": "کیا آپ کے پاس ڈسکاؤنٹ کوڈ ہے؟",
"Enter discount code...": "ڈسکاؤنٹ کوڈ درج کریں...",
"Apply": "لاگو کریں",
"Clear": "صاف کریں",
"This field is required": "یہ فیلڈ ضروری ہے"
}

21
src/translations/locales/uz.json

@ -295,5 +295,24 @@
"Buy Habib Coins": "Habib tangalarini sotib oling",
"Subscription required": "Obuna talab qilinadi",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "Profillarni ko'rish uchun obuna kerak. Har bir obuna {count}tagacha moslikni o'z ichiga oladi.",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Profillarni ko'rish uchun obuna talab qilinadi. Bu bizning qo'llab-quvvatlash va moslashtirish xizmatlarimizni qoplashga yordam beradi va faqat erkak foydalanuvchilarga tegishli"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "Profillarni ko'rish uchun obuna talab qilinadi. Bu bizning qo'llab-quvvatlash va moslashtirish xizmatlarimizni qoplashga yordam beradi va faqat erkak foydalanuvchilarga tegishli",
"More detail": "Batafsil",
"Subscription Status": "Obuna holati",
"{days} days remaining of your subscription.": "Obunangizdan {days} kun qoldi.",
"No Active Subscription": "Faol obuna yo'q",
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you.": "Hozirda sizda faol obuna yo'q. Obunani faollashtirish faqat birinchi nomzod sizga taqdim etilgandagina mumkin.",
"Renew Subscription": "Obunani uzaytirish",
"Renewing...": "Uzaytirilmoqda...",
"Payment successful": "To'lov muvaffaqiyatli amalga oshirildi",
"Insufficient coin balance. Please recharge your account.": "Tanga balansi yetarli emas. Iltimos, hisobingizni to'ldiring.",
"Got it": "Tushundim",
"{count} case introductions remaining of your subscription.": "Obunangizdan {count} ta tanishtiruv qoldi.",
"{days} days and {count} case introductions remaining of your subscription.": "Obunangizdan {days} kun va {count} ta tanishtiruv qoldi.",
"You do not have enough Habib Coins": "Сизда йэтарли Habib tangalari мавжуд эмас",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "Iltimos, balansiga koʻproq Habib tangasi qoʻshing (yoki chegirma kodi ishlating) bu kontentga kirish uchun",
"Do you have a discount code?": "Chegirma kodingiz bormi?",
"Enter discount code...": "Chegirma kodini kiriting...",
"Apply": "Qoʻllash",
"Clear": "Tozalash",
"This field is required": "Бу майдон тўлдирилиши шарт"
}

18
src/translations/locales/zh.json

@ -1,4 +1,7 @@
{
"2": "2",
"70": "70",
"175": "175",
"### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.": "### 家庭宗教氛围选项 * **宗教和严格遵守:** 这指定一个家庭高度致力于履行所有**义务义务**,严格维护**宗教界限**(例如 Mahram 规则),并在生活的各个方面坚持**宗教仪式和教义**。 * **宗教(义务遵守者):** 这表明一个家庭致力于核心**宗教义务**(例如祈祷和斋戒)和**伊斯兰道德**,生活在宗教社会的标准框架内。 * **传统(尊重宗教价值观):** 这描述了一个恪守道德价值观并**尊重宗教**的家庭,但可能不严格执行每一项具体的**宗教法律**或义务。 * **非宗教/世俗:** 这代表一个家庭,尽管普遍尊重宗教,但**宗教仪式和框架**不会显着影响他们的日常生活**生活方式、关系或决定**。",
"(Complete Required Forms)": "(Complete Required Forms)",
"(after 2 days)": "(after 2 days)",
@ -11,9 +14,7 @@
"1. Eligibility and Membership Requirements": "1. Eligibility and Membership Requirements",
"160 to 170": "160 至 170",
"170 to 180": "170 至 180",
"175": "175",
"180 to 190": "180 至 190",
"2": "2",
"2 minutes": "2分钟",
"2. Privacy and Data Management": "2. Privacy and Data Management",
"25-30": "25-30",
@ -24,7 +25,6 @@
"5 minutes": "5 分钟",
"50 Coins": "50 Coins",
"6 minutes": "6分钟",
"70": "70",
"8 minutes": "8分钟",
"A Path to Heavenly Marriage": "A Path to Heavenly Marriage",
"A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.": "不需要精确的地址。只需您居住地的大致区域即可,例如城市、地区、社区或最近的主要城市。",
@ -767,5 +767,15 @@
"Buy Habib Coins": "购买Habib币",
"Subscription required": "需要订阅",
"To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "要查看个人资料,您需要订阅。每次订阅最多包含{count}次匹配。",
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "查看个人资料需要订阅。这有助于支付我们的支持和匹配服务费用,仅适用于男性用户"
"To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "查看个人资料需要订阅。这有助于支付我们的支持和匹配服务费用,仅适用于男性用户",
"More detail": "更多详情",
"{count} case introductions remaining of your subscription.": "您的订阅还剩 {count} 次匹配推荐。",
"{days} days and {count} case introductions remaining of your subscription.": "您的订阅还剩 {days} 天和 {count} 次匹配推荐。",
"You do not have enough Habib Coins": "您的哈比币不足",
"Please add more Habib Coins to your balance (or use discount code), so that you can use them to access this content": "请向您的余额中添加更多 Habib 金币(或使用折扣码),以便访问此内容",
"Do you have a discount code?": "您有折扣码吗?",
"Enter discount code...": "输入折扣码...",
"Apply": "应用",
"Clear": "清除",
"This field is required": "此字段为必填项"
}
Loading…
Cancel
Save