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.
 
 
 
 
 

517 lines
18 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 { FaLock } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import { DotsLoader } from "@/components/Componentes/button";
import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background";
import { IoClose } from "react-icons/io5";
import { useHabcoinPaymentMutation } from "@/hooks/marriage/use-habcoin-payment";
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 { 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 fieldCandidates = {
name: ["name", "full_name", "fullname", "first_name", "display_name"],
occupation: [
"occupation",
"job",
"profession",
"career",
"work",
"education",
"highest_level_of_education",
],
age: ["age"],
city: [
"city",
"current_city",
"residence_city",
"location",
"residence",
"birth_city",
],
maritalStatus: [
"marital_status",
"maritalstatus",
"relationship_status",
"current_marital_status",
],
cityPreference: [
"city_preference",
"citypreference",
"preferred_city",
"preferred_location",
"future_residence",
],
} 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 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) {
return key
.replace(/^q\d+[_-]?/i, "")
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function toDisplayField(field: MarriageField): DisplayField | null {
const value = formatFieldValue(field.value);
if (!value) {
return null;
}
return {
id: field.key || field.label || value,
label: field.label || titleFromKey(field.key),
value,
};
}
function pickField(
fields: MarriageField[],
candidates: readonly string[],
usedIndexes: Set<number>,
) {
const candidateSet = new Set(candidates.map(normalizeFieldName));
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;
}
}
return null;
}
function useMatchSummaryDisplay(matchSummary: MarriageMatchSummary | null) {
return useMemo(() => {
const fields = matchSummary?.public_info ?? [];
const usedIndexes = new Set<number>();
const name = pickField(fields, fieldCandidates.name, usedIndexes);
const occupation = pickField(
fields,
fieldCandidates.occupation,
usedIndexes,
);
const age = pickField(fields, fieldCandidates.age, usedIndexes);
const city = pickField(fields, fieldCandidates.city, usedIndexes);
const maritalStatus = pickField(
fields,
fieldCandidates.maritalStatus,
usedIndexes,
);
const cityPreference = pickField(
fields,
fieldCandidates.cityPreference,
usedIndexes,
);
const extraFields = fields
.filter((_, index) => !usedIndexes.has(index))
.map(toDisplayField)
.filter((field): field is DisplayField => Boolean(field))
.slice(0, 4);
return {
age,
city,
cityPreference,
extraFields,
maritalStatus,
name:
name?.value ??
(matchSummary?.id ? `Profile #${matchSummary.id}` : null),
occupation,
};
}, [matchSummary]);
}
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 NewMatchPage() {
const router = useRouter();
const { dictionary: t, locale } = useI18n();
const { top, bottom } = useViewPaddings();
const { data: profile, isError, isLoading } = useMarriageProfileQuery();
const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(null);
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);
await paymentMutation.mutateAsync(recommendedPlanId);
setIsPaymentSheetOpen(false);
router.push(localizePath("/new-match/profile", locale));
} 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") {
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(() => {
if (!profile) {
return;
}
const targetPath = getSubmitPath(profile);
if (targetPath !== "/new-match") {
router.replace(localizePath(targetPath, locale));
}
}, [profile, locale, router]);
const matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary);
const pairedFields = [matchDisplay.age, matchDisplay.city].filter(
(field): field is DisplayField => Boolean(field),
);
const isFemaleProfile = profile?.gender === "female";
const matchHeadingTitle = isFemaleProfile
? t.match.newMatchTitleFemale
: t.match.newMatchTitleMale;
const matchHeadingDescription = isFemaleProfile
? t.match.newMatchDescriptionFemale
: t.match.newMatchDescriptionMale;
const isMale = profile?.gender === "male";
const hasActiveSub = !!profile?.active_subscription;
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 />
<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">
<section className="mt-[36px] rounded-[15px] bg-[linear-gradient(180deg,#F0445B_0%,#F4556E_100%)] px-[17px] pt-[18px] pb-[17px] text-white shadow-[0_18px_38px_rgba(240,68,91,0.25)]">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<DotsLoader />
</div>
) : isError ? (
<p className="py-8 text-[13px] font-semibold">
Unable to load match summary.
</p>
) : matchSummary ? (
<>
<h2 className="break-words text-[13px] leading-[1.4] font-bold">
<span>Name: </span>
<span>{matchDisplay.name}</span>
</h2>
<div className="mt-[3px] min-h-[48px]">
{matchDisplay.occupation ? (
<FieldLine field={matchDisplay.occupation} />
) : null}
{pairedFields.length ? (
<p className="break-words text-[10px] leading-[1.85] font-medium text-white">
{pairedFields.map((field, index) => (
<span key={field.id}>
{index > 0 ? <span> | </span> : null}
<span>
{field.label}: {field.value}
</span>
</span>
))}
</p>
) : null}
{matchDisplay.maritalStatus ? (
<FieldLine field={matchDisplay.maritalStatus} />
) : null}
{matchDisplay.cityPreference ? (
<FieldLine field={matchDisplay.cityPreference} />
) : null}
</div>
<button
type="button"
onClick={() => {
if (isMale && !hasActiveSub) {
setIsPaymentSheetOpen(true);
} else {
router.push(localizePath("/new-match/profile", locale));
}
}}
className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white py-[12px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none hover:bg-white/95 transition-colors cursor-pointer"
>
{t.match.viewMoreDetails}
</button>
</>
) : (
<p className="py-8 text-[13px] font-semibold">
No match summary is available yet.
</p>
)}
</section>
<div className="w-full space-y-3">
<AdvisorActionsCard
className="mt-[36px]"
title={t.findingMatch.advisorTitle}
description={t.findingMatch.advisorDescription}
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorHref="/questions-list"
/>
</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 max-w-[375px] bg-background/95 px-[17px] pt-3 pb-[16px] backdrop-blur-md"
>
<div
className="inline-flex w-full items-center justify-center gap-2 rounded-[9px] border-none bg-[#D1D1D6] px-4 py-[17px] text-center text-[#747474] shadow-none"
role="status"
>
<FaLock aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span className="group-16 leading-none font-semibold">
{t.requestSent.profileLocked}
</span>
</div>
</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);
}}
>
<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.paymentModal?.title || "Verification & Subscription Activation"}
</h3>
<p className="text-xs text-gray-500 leading-relaxed mb-4 text-center">
{t.paymentModal?.verificationText || "This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."}
</p>
<div className="w-full bg-gray-50 rounded-xl p-3 mb-4 text-center">
<span className="text-[10px] text-gray-400 block mb-0.5">
{t.paymentModal?.activeFor3Months || "Valid for 3 months"}
</span>
<span className="text-base font-bold text-[#FF4E67]">
{t.paymentModal?.cost || "50 Habib Coins"}
</span>
</div>
<p className="text-[10px] text-gray-400 leading-normal mb-6 text-center">
{t.paymentModal?.disclaimerText || "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users."}
</p>
{paymentError && (
<div className="w-full bg-[#FEF2F2] text-[#B91C1C] text-xs p-3 rounded-xl mb-4 text-center font-medium">
{paymentError}
</div>
)}
<div className="grid w-full grid-cols-[1fr_2fr] 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-[18px] border border-[#9A9A9A] bg-[#F7F7F7] px-4 py-[18px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t.common?.decline || "Decline"}</span>
</div>
</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 py-[16px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate min-w-0">{t.paymentModal?.pay || "Pay"}</span>
<span className="inline-flex items-center gap-1 rounded-full bg-[#E43B51] p-1.5 text-xs font-semibold leading-none text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.12)] shrink-0 min-w-0 max-w-[120px]">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={18}
height={18}
className="shrink-0"
/>
<span className="truncate">{t.paymentModal?.cost || "50 Coins"}</span>
</span>
</div>
</button>
</div>
</div>
</div>
</div>
)}
</>
);
}