You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
942 lines
33 KiB
942 lines
33 KiB
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useHabibWebReady } from "@/hooks/use-habib-web-ready";
|
|
import { FaLock } from "react-icons/fa6";
|
|
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 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,
|
|
MarriageFieldValue,
|
|
MarriageMatchSummary,
|
|
MarriagePhoneFieldValue,
|
|
} from "@/hooks/marriage/types";
|
|
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
|
|
import { useViewPaddings } from "@/hooks/use-view-paddings";
|
|
import { getSubmitPath } from "@/lib/get-submit-path";
|
|
import {
|
|
buyHabibCoinPackages,
|
|
isInFlutterWebView,
|
|
} from "@/lib/webview-actions";
|
|
import { localizePath } from "@/translations/config";
|
|
import { useI18n } from "@/translations/provider";
|
|
|
|
const advisorAvatars = [
|
|
{ id: "advisor-primary", src: "/assets/images/Avatar Image.png" },
|
|
{ id: "advisor-secondary", src: "/assets/images/Ellipse 370.png" },
|
|
{ id: "advisor-tertiary", src: "/assets/images/Avatar Image.png" },
|
|
];
|
|
|
|
const fieldCandidateMatchers = {
|
|
name: [
|
|
"name",
|
|
"full_name",
|
|
"fullname",
|
|
"first_name",
|
|
"last_name",
|
|
"display_name",
|
|
],
|
|
occupation: [
|
|
"job_title",
|
|
"occupation",
|
|
"job",
|
|
"profession",
|
|
"career",
|
|
"work",
|
|
"highest_level_of_education",
|
|
"field_of_study",
|
|
"employment_status",
|
|
],
|
|
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",
|
|
],
|
|
cityPreference: [
|
|
"willingness_to_relocate",
|
|
"city_preference",
|
|
"citypreference",
|
|
"preferred_city",
|
|
"preferred_location",
|
|
"future_residence",
|
|
"residence_preference_after_marriage",
|
|
],
|
|
} as const;
|
|
|
|
type DisplayField = {
|
|
id: string;
|
|
label: string;
|
|
value: string;
|
|
};
|
|
|
|
function normalizeFieldName(value: string) {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/^q\d+[_-]?/, "")
|
|
.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 {
|
|
if (Array.isArray(value)) {
|
|
const formattedItems = value
|
|
.map((item) => formatOptionValue(item as MarriageFieldValue, dictionary))
|
|
.filter(Boolean);
|
|
return formattedItems.length ? formattedItems.join(", ") : 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;
|
|
}
|
|
|
|
if (isMarriagePhoneFieldValue(value)) {
|
|
return `+${value.countryCode}${value.phoneNumber}`;
|
|
}
|
|
|
|
if (typeof value === "boolean") {
|
|
return value ? "Yes" : "No";
|
|
}
|
|
|
|
return String(value);
|
|
}
|
|
|
|
function isMarriagePhoneFieldValue(
|
|
value: unknown,
|
|
): value is MarriagePhoneFieldValue {
|
|
if (!value || typeof value !== "object") {
|
|
return false;
|
|
}
|
|
|
|
const phoneValue = value as Partial<MarriagePhoneFieldValue>;
|
|
|
|
return (
|
|
typeof phoneValue.countryCode === "string" &&
|
|
typeof phoneValue.phoneNumber === "string"
|
|
);
|
|
}
|
|
|
|
function titleFromKey(key: string) {
|
|
const leafKey = key.includes(".") ? key.split(".").pop()! : key;
|
|
return leafKey
|
|
.replace(/^q\d+[_-]?/i, "")
|
|
.replace(/[_-]+/g, " ")
|
|
.replace(/\s+/g, " ")
|
|
.trim()
|
|
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
}
|
|
|
|
function formatFieldLabel(
|
|
field: MarriageField,
|
|
dictionary?: Record<string, string>,
|
|
): string {
|
|
const englishTitle = titleFromKey(field.key);
|
|
if (!dictionary) return field.label || englishTitle;
|
|
|
|
if (field.label && dictionary[field.label]) {
|
|
return dictionary[field.label];
|
|
}
|
|
|
|
if (dictionary[englishTitle]) {
|
|
return dictionary[englishTitle];
|
|
}
|
|
|
|
return field.label || englishTitle;
|
|
}
|
|
|
|
function toDisplayField(
|
|
field: MarriageField,
|
|
dictionary?: Record<string, string>,
|
|
): DisplayField | null {
|
|
const value = formatOptionValue(field.value, dictionary);
|
|
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: field.key || field.label || value,
|
|
label: formatFieldLabel(field, dictionary),
|
|
value,
|
|
};
|
|
}
|
|
|
|
function pickField(
|
|
fields: MarriageField[],
|
|
candidates: readonly string[],
|
|
usedIndexes: Set<number>,
|
|
dictionary?: Record<string, string>,
|
|
): DisplayField | null {
|
|
for (const [fieldIndex, field] of fields.entries()) {
|
|
if (usedIndexes.has(fieldIndex)) {
|
|
continue;
|
|
}
|
|
|
|
if (matchesCandidate(field, candidates)) {
|
|
const displayField = toDisplayField(field, dictionary);
|
|
if (displayField) {
|
|
usedIndexes.add(fieldIndex);
|
|
return displayField;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function useMatchSummaryDisplay(
|
|
matchSummary: MarriageMatchSummary | null,
|
|
t?: Record<string, string>,
|
|
) {
|
|
return useMemo(() => {
|
|
const fields = matchSummary?.public_info ?? [];
|
|
const usedIndexes = new Set<number>();
|
|
|
|
// 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,
|
|
fieldCandidateMatchers.occupation,
|
|
usedIndexes,
|
|
t,
|
|
);
|
|
|
|
// 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,
|
|
fieldCandidateMatchers.maritalStatus,
|
|
usedIndexes,
|
|
t,
|
|
);
|
|
|
|
// 6. City Preference / Relocation
|
|
const cityPreference = pickField(
|
|
fields,
|
|
fieldCandidateMatchers.cityPreference,
|
|
usedIndexes,
|
|
t,
|
|
);
|
|
|
|
const extraFields = fields
|
|
.filter((_, index) => !usedIndexes.has(index))
|
|
.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: displayName,
|
|
occupation,
|
|
};
|
|
}, [matchSummary, t]);
|
|
}
|
|
|
|
function FieldLine({ field }: { field: DisplayField }) {
|
|
return (
|
|
<p className="break-words text-[10px] leading-[1.85] font-medium text-white">
|
|
<span>{field.label}: </span>
|
|
<span>{field.value}</span>
|
|
</p>
|
|
);
|
|
}
|
|
|
|
export default function NewMatchClient() {
|
|
const router = useRouter();
|
|
const { dictionary: t, locale } = useI18n();
|
|
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 ?? "", {
|
|
onSuccess: () => {
|
|
router.replace(localizePath("/finding-match", locale));
|
|
},
|
|
});
|
|
|
|
const handlePayment = async () => {
|
|
const recommendedPlanId = profile?.recommended_plan?.id;
|
|
if (!recommendedPlanId) return;
|
|
|
|
try {
|
|
setPaymentError(null);
|
|
setIsInsufficientCoins(false);
|
|
await paymentMutation.mutateAsync({
|
|
objectId: recommendedPlanId,
|
|
discountCode: appliedDiscount?.valid ? appliedDiscount.code : undefined,
|
|
});
|
|
setIsPaymentSheetOpen(false);
|
|
openProfile();
|
|
} catch (err: any) {
|
|
console.error("Payment failed", err);
|
|
const msg =
|
|
err?.response?.data?.error || err?.message || "Payment failed";
|
|
const modalT = (t as any).paymentModal || {};
|
|
if (msg === "Not enough coins") {
|
|
setIsInsufficientCoins(true);
|
|
setPaymentError(
|
|
modalT.insufficientCoins ||
|
|
"Insufficient coin balance. Please recharge your account.",
|
|
);
|
|
} else {
|
|
setPaymentError(msg);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleDecline = async () => {
|
|
if (!caseId) return;
|
|
try {
|
|
await respondMutation.mutateAsync({ action: "reject" });
|
|
} catch (err) {
|
|
console.error("Decline failed", err);
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
const targetPath = getSubmitPath(profile);
|
|
if (targetPath !== "/new-match") {
|
|
router.replace(localizePath(targetPath, locale));
|
|
}
|
|
}, [profile, locale, router, isLoading, isError]);
|
|
|
|
// Signal Flutter to lift its loading cover once the profile is available.
|
|
useHabibWebReady(!!profile && !isLoading);
|
|
|
|
const isRedirecting = useMemo(() => {
|
|
if (!profile) return false;
|
|
return getSubmitPath(profile) !== "/new-match";
|
|
}, [profile]);
|
|
|
|
const matchSummary = profile?.match_summary ?? null;
|
|
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 (
|
|
<>
|
|
<PageBackground />
|
|
<main
|
|
style={{
|
|
paddingBottom: `${bottom + 20}px`,
|
|
paddingTop: `${Math.max(12, top + 4)}px`,
|
|
}}
|
|
className="-mx-[17px] min-h-screen h-full text-center px-4"
|
|
>
|
|
<PageHeader profile={profile} />
|
|
|
|
<div>
|
|
{/* Header Section Skeleton */}
|
|
<section className="flex flex-col items-center mt-9">
|
|
<LoadingSkeleton className="h-[60px] w-[60px] rounded-full" />
|
|
<LoadingSkeleton className="h-6 w-[220px] mt-4" />
|
|
<LoadingSkeleton className="h-4 w-[280px] mt-3" />
|
|
<LoadingSkeleton className="h-4 w-[240px] mt-1.5" />
|
|
</section>
|
|
|
|
<div className="flex h-full flex-col justify-between">
|
|
{/* Match Card Skeleton */}
|
|
<section className="mt-[36px] rounded-[15px] border border-white/80 bg-white px-[17px] pt-[18px] pb-[17px] shadow-[0_18px_45px_rgba(15,23,42,0.06)] flex flex-col items-center">
|
|
{/* Name line */}
|
|
<LoadingSkeleton className="h-5 w-[140px]" />
|
|
|
|
{/* Subtitle / Details lines */}
|
|
<div className="mt-3 w-full flex flex-col items-center gap-2 min-h-[48px]">
|
|
<LoadingSkeleton className="h-3 w-[180px]" />
|
|
<LoadingSkeleton className="h-3 w-[120px]" />
|
|
<LoadingSkeleton className="h-3 w-[150px]" />
|
|
</div>
|
|
|
|
{/* Button skeleton */}
|
|
<LoadingSkeleton className="mt-[15px] h-[40px] w-full rounded-[10px]" />
|
|
</section>
|
|
|
|
{/* Advisor Card Skeleton */}
|
|
<section className="mt-[36px]">
|
|
<div className="rounded-[13px] border border-white/80 bg-white px-3 py-3.5 text-left shadow-[0_18px_45px_rgba(15,23,42,0.06)] backdrop-blur-sm">
|
|
<LoadingSkeleton className="h-[16px] w-[140px]" />
|
|
<div className="mt-2 space-y-1">
|
|
<LoadingSkeleton className="h-[10px] w-[260px]" />
|
|
<LoadingSkeleton className="h-[10px] w-[180px]" />
|
|
</div>
|
|
|
|
<div className="mt-4 flex items-center justify-between gap-3">
|
|
<div className="flex items-center pl-1">
|
|
<LoadingSkeleton className="h-[30px] w-[30px] rounded-full border-2 border-white" />
|
|
<LoadingSkeleton className="-ml-1.5 h-[30px] w-[30px] rounded-full border-2 border-white" />
|
|
<LoadingSkeleton className="-ml-1.5 h-[30px] w-[30px] rounded-full border-2 border-white" />
|
|
<LoadingSkeleton className="-ml-1.5 h-[30px] w-[30px] rounded-full border-2 border-white" />
|
|
</div>
|
|
|
|
<LoadingSkeleton className="h-[38px] w-[120px] rounded-[9px]" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
const pairedFields = [matchDisplay.age, matchDisplay.city].filter(
|
|
(field): field is DisplayField => Boolean(field),
|
|
);
|
|
|
|
const isFemaleProfile = profile?.gender === "female";
|
|
const matchHeadingTitle = isFemaleProfile
|
|
? t["New Marriage Proposal"]
|
|
: t["YOU HAVE A NEW MATCH!"];
|
|
const matchHeadingDescription = isFemaleProfile
|
|
? t[
|
|
"A suitable match has been found for you. If approved, your profile will be evaluated to proceed with the introduction process."
|
|
]
|
|
: t[
|
|
"If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information."
|
|
];
|
|
|
|
const isMale = profile?.gender === "male";
|
|
const hasActiveSub =
|
|
!!profile?.active_subscription &&
|
|
profile.active_subscription.is_active !== false &&
|
|
profile.active_subscription.is_valid !== false;
|
|
const isMatchAvailable = !!profile?.match_summary;
|
|
|
|
return (
|
|
<>
|
|
<PageBackground />
|
|
|
|
<main
|
|
style={{
|
|
paddingBottom: `${profile?.can_edit_profile === false ? 100 + bottom : 20 + bottom}px`,
|
|
paddingTop: `${Math.max(12, top + 4)}px`,
|
|
}}
|
|
className="-mx-[17px] min-h-screen h-full text-center px-4"
|
|
>
|
|
<PageHeader profile={profile} />
|
|
|
|
<div className="">
|
|
<section className="flex flex-col items-center mt-9">
|
|
<div
|
|
aria-hidden="true"
|
|
className="relative flex h-[60px] w-[60px] items-center justify-center rounded-full bg-[#FF4E67] shadow-[0_12px_28px_rgba(240,68,91,0.22)]"
|
|
>
|
|
<Image
|
|
src={"/assets/images/Ellipse 1210.svg"}
|
|
width={70}
|
|
height={70}
|
|
alt="notification"
|
|
/>
|
|
</div>
|
|
<h2 className="text-[22px] font-bold mt-3.5">
|
|
{matchHeadingTitle}
|
|
</h2>
|
|
<p className="mt-[11px] max-w-[322px] text-[12px] leading-[1.35] font-semibold text-[#7C7C7C]">
|
|
{matchHeadingDescription}
|
|
</p>
|
|
</section>
|
|
<div className="flex h-full flex-col justify-between">
|
|
{isLoading ? (
|
|
<section className="mt-[36px] rounded-[15px] border border-white/80 bg-white px-[17px] pt-[18px] pb-[17px] shadow-[0_18px_45px_rgba(15,23,42,0.06)]">
|
|
<div className="flex flex-col items-center py-2">
|
|
{/* Name line */}
|
|
<LoadingSkeleton className="h-5 w-[140px]" />
|
|
|
|
{/* Subtitle / Details lines */}
|
|
<div className="mt-3 w-full flex flex-col items-center gap-2 min-h-[48px]">
|
|
<LoadingSkeleton className="h-3 w-[180px]" />
|
|
<LoadingSkeleton className="h-3 w-[120px]" />
|
|
<LoadingSkeleton className="h-3 w-[150px]" />
|
|
</div>
|
|
|
|
{/* Button skeleton */}
|
|
<LoadingSkeleton className="mt-[15px] h-[40px] w-full rounded-[10px]" />
|
|
</div>
|
|
</section>
|
|
) : (
|
|
<section className="mt-[36px] rounded-[15px] bg-[linear-gradient(180deg,#F0445B_0%,#F4556E_100%)] px-[17px] pt-[18px] pb-[17px] text-white shadow-[0_18px_38px_rgba(240,68,91,0.25)]">
|
|
{isError ? (
|
|
<p className="py-8 text-[13px] font-semibold">
|
|
Unable to load match summary.
|
|
</p>
|
|
) : matchSummary ? (
|
|
<>
|
|
<h2 className="break-words text-[13px] leading-[1.4] font-bold">
|
|
<span>{matchDisplay.name}</span>
|
|
</h2>
|
|
|
|
<div className="mt-[3px] min-h-[48px]">
|
|
{matchDisplay.occupation ? (
|
|
<FieldLine field={matchDisplay.occupation} />
|
|
) : null}
|
|
|
|
{pairedFields.length ? (
|
|
<p className="break-words text-[10px] leading-[1.85] font-medium text-white">
|
|
{pairedFields.map((field, index) => (
|
|
<span key={field.id}>
|
|
{index > 0 ? <span> | </span> : null}
|
|
<span>
|
|
{field.label}: {field.value}
|
|
</span>
|
|
</span>
|
|
))}
|
|
</p>
|
|
) : null}
|
|
|
|
{matchDisplay.maritalStatus ? (
|
|
<FieldLine field={matchDisplay.maritalStatus} />
|
|
) : null}
|
|
{matchDisplay.cityPreference ? (
|
|
<FieldLine field={matchDisplay.cityPreference} />
|
|
) : null}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (isMale && !hasActiveSub) {
|
|
setIsPaymentSheetOpen(true);
|
|
} else {
|
|
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"
|
|
>
|
|
{t["View more details"]}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<p className="py-8 text-[13px] font-semibold">
|
|
No match summary is available yet.
|
|
</p>
|
|
)}
|
|
</section>
|
|
)}
|
|
<div className="w-full space-y-3">
|
|
<AdvisorActionsCard
|
|
className="mt-[36px]"
|
|
title={t["Get an advisor"]}
|
|
description={
|
|
t[
|
|
"Not sure what to do next? Our psychology section is here to guide you at every step."
|
|
]
|
|
}
|
|
avatars={advisorAvatars}
|
|
extraCount={7}
|
|
getAdvisorLabel={t["Get Advisor"]}
|
|
onGetAdvisor={openAdvisors}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{profile?.can_edit_profile === false && (
|
|
<div
|
|
style={{ paddingBottom: `${16 + bottom}px` }}
|
|
className="fixed inset-x-0 bottom-0 z-20 mx-auto w-full sm:max-w-[375px] bg-background/95 px-[17px] pt-3 pb-[16px] backdrop-blur-md"
|
|
>
|
|
<div
|
|
className="inline-flex w-full items-center justify-center gap-2 rounded-[9px] border-none bg-[#D1D1D6] px-4 h-[52px] text-center text-[#747474] shadow-none"
|
|
role="status"
|
|
>
|
|
<FaLock aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
|
|
<span className="group-16 leading-none font-semibold">
|
|
{t["Profile is locked"]}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isPaymentSheetOpen && isInventoryLoading && (
|
|
<InformationSheet
|
|
isLoading={true}
|
|
style={{
|
|
paddingBottom: bottom > 0 ? `${14 + bottom}px` : undefined,
|
|
}}
|
|
onClose={() => {
|
|
setIsPaymentSheetOpen(false);
|
|
setPaymentError(null);
|
|
setIsInsufficientCoins(false);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{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[
|
|
"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="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["Habib Coins"] || "Habib Coins"}
|
|
</span>
|
|
</div>
|
|
|
|
<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>
|
|
)
|
|
}
|
|
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-[11px] text-center font-medium">
|
|
{paymentError}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
|
|
<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={handleDecline}
|
|
>
|
|
<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 />
|
|
) : (
|
|
<span className="truncate">
|
|
{t["Decline"] || "Decline"}
|
|
</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["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>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
<MarriageAdvisorsOverlay open={isAdvisorOpen} onClose={closeAdvisors} />
|
|
|
|
<MatchProfileOverlay open={isProfileOpen} onClose={closeProfile} />
|
|
</>
|
|
);
|
|
}
|