Browse Source
fix(build): migrate production build to webpack, fix page props typing, and resolve React hydration freeze
staging
fix(build): migrate production build to webpack, fix page props typing, and resolve React hydration freeze
staging
9 changed files with 1138 additions and 1127 deletions
-
2auto_deploy_staging.sh
-
1next.config.ts
-
2package.json
-
374src/app/marriage-advisors/page.tsx
-
753src/app/new-match/profile/page.tsx
-
373src/components/Componentes/marriage-advisors-client.tsx
-
4src/components/Componentes/marriage-advisors-overlay.tsx
-
4src/components/Componentes/match-profile-overlay.tsx
-
752src/components/Componentes/new-match-profile-client.tsx
@ -1,373 +1,5 @@ |
|||
"use client"; |
|||
import MarriageAdvisorsClient from "@/components/Componentes/marriage-advisors-client"; |
|||
|
|||
import { useState } from "react"; |
|||
import Image from "next/image"; |
|||
import { Ic } from "@/icons"; |
|||
import StickyHeader from "@/components/Componentes/sticky-header"; |
|||
import NavigationButton from "@/components/Componentes/navigation-button"; |
|||
import { PageBackground } from "@/components/Componentes/page-background"; |
|||
import SupportSheet from "@/components/Componentes/support-sheet"; |
|||
import { useI18n } from "@/translations/provider"; |
|||
import { |
|||
useMarriageAdvisorsQuery, |
|||
type MarriageConsultant, |
|||
} from "@/hooks/marriage/use-marriage-advisors"; |
|||
import { postActionToFlutter } from "@/lib/webview-actions"; |
|||
|
|||
const FALLBACK_AVATAR = "/assets/images/Avatar Image.png"; |
|||
|
|||
type MarriageAdvisorsPageProps = { |
|||
onClose?: () => void; |
|||
}; |
|||
|
|||
export default function MarriageAdvisorsPage({ |
|||
onClose, |
|||
}: MarriageAdvisorsPageProps = {}) { |
|||
const { dictionary: t } = useI18n(); |
|||
const [isSupportOpen, setIsSupportOpen] = useState(false); |
|||
|
|||
const { data, isLoading, isError, refetch } = useMarriageAdvisorsQuery(); |
|||
const advisors = data?.results ?? []; |
|||
|
|||
const mainStyle = { |
|||
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, |
|||
backgroundColor: "#F5F5F5", |
|||
backgroundRepeat: "repeat-y", |
|||
backgroundSize: "100% auto", |
|||
}; |
|||
|
|||
// Opens the native Talk consultant page; the Flutter side fetches the
|
|||
// consultant by username, so only the bare username is needed. Outside the
|
|||
// WebView (plain browser) postActionToFlutter is a no-op returning false.
|
|||
const openConsultantPage = (username: string) => { |
|||
postActionToFlutter("open_consultant_page", { consultant: username }); |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
<main |
|||
className="-mx-[17px] flex h-dvh flex-col overflow-hidden pb-10" |
|||
style={mainStyle} |
|||
> |
|||
<StickyHeader sticky className="shrink-0"> |
|||
<div className="flex items-center justify-between gap-3"> |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="back" |
|||
iconLabel={t["Go back"]} |
|||
onClick={onClose} |
|||
/> |
|||
<h1 |
|||
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" |
|||
> |
|||
{t["Marriage advisors"]} |
|||
</h1> |
|||
<div className="size-10 shrink-0" /> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-[17px] pb-32 pt-5 flex flex-col items-center gap-4"> |
|||
{isLoading && <AdvisorsSkeleton />} |
|||
|
|||
{isError && ( |
|||
<div className="flex w-[343px] flex-col items-center gap-3 rounded-[15px] border border-[#EBEBEB] bg-white px-4 py-8 text-center shadow-[0px_4px_12px_rgba(0,0,0,0.05)]"> |
|||
<p |
|||
className="text-[14px] leading-[20px] text-[#747474]" |
|||
> |
|||
{t["Failed to load advisors."]} |
|||
</p> |
|||
<button |
|||
type="button" |
|||
onClick={() => refetch()} |
|||
className="cursor-pointer rounded-[20px] border-none bg-[rgba(14,177,60,0.1)] px-4 py-1.5 text-[12px] font-semibold text-[#0EB13C] transition-transform active:scale-95" |
|||
> |
|||
{t["Try again"]} |
|||
</button> |
|||
</div> |
|||
)} |
|||
|
|||
{!isLoading && !isError && advisors.length === 0 && ( |
|||
<div className="flex w-[343px] flex-col items-center rounded-[15px] border border-[#EBEBEB] bg-white px-4 py-8 text-center shadow-[0px_4px_12px_rgba(0,0,0,0.05)]"> |
|||
<p |
|||
className="text-[14px] leading-[20px] text-[#747474]" |
|||
> |
|||
{t["No advisors are currently available."]} |
|||
</p> |
|||
</div> |
|||
)} |
|||
|
|||
{advisors.map((advisor) => ( |
|||
<AdvisorCard |
|||
key={advisor.username} |
|||
advisor={advisor} |
|||
onOpen={() => openConsultantPage(advisor.username)} |
|||
/> |
|||
))} |
|||
</section> |
|||
</main> |
|||
|
|||
<SupportSheet |
|||
isOpen={isSupportOpen} |
|||
onClose={() => setIsSupportOpen(false)} |
|||
/> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
function AdvisorCard({ |
|||
advisor, |
|||
onOpen, |
|||
}: { |
|||
advisor: MarriageConsultant; |
|||
onOpen: () => void; |
|||
}) { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const isRtl = locale === "fa" || locale === "ar" || locale === "ur"; |
|||
const unreadCount = advisor.unread_count ?? 0; |
|||
const isOnline = advisor.status === "online"; |
|||
const rating = Number(advisor.avg_rate ?? 0); |
|||
const hasChat = advisor.contact_type?.includes("chat"); |
|||
const hasVoice = advisor.contact_type?.includes("voice"); |
|||
const hasVideo = advisor.contact_type?.includes("video"); |
|||
const hasActiveContact = hasChat || hasVoice || hasVideo; |
|||
const topics = advisor.topics?.filter(Boolean) ?? []; |
|||
|
|||
return ( |
|||
<div |
|||
role="button" |
|||
tabIndex={0} |
|||
aria-label={advisor.fullname ?? advisor.username} |
|||
onClick={onOpen} |
|||
onKeyDown={(event) => { |
|||
if (event.key === "Enter" || event.key === " ") { |
|||
event.preventDefault(); |
|||
onOpen(); |
|||
} |
|||
}} |
|||
className="relative w-full max-w-[343px] cursor-pointer transition-transform active:scale-[0.98] outline-none" |
|||
> |
|||
{/* Outer Card Container */} |
|||
<div |
|||
className={`w-full rounded-[15px] bg-white py-3 shadow-[0_4px_16px_rgba(0,0,0,0.06)] transition-all ${ |
|||
unreadCount > 0 |
|||
? "border border-[#ECA533]" |
|||
: "border border-[#EBEBEB]" |
|||
}`}
|
|||
> |
|||
<div className="flex flex-col"> |
|||
{/* Header Row: Avatar + Info + Rating */} |
|||
<div className="flex items-center gap-3 px-3"> |
|||
{/* Avatar with Status Dot */} |
|||
<div className="relative size-12 shrink-0"> |
|||
{advisor.avatar_url ? ( |
|||
// eslint-disable-next-line @next/next/no-img-element
|
|||
<img |
|||
src={advisor.avatar_url} |
|||
alt={advisor.fullname ?? advisor.username} |
|||
className="size-full rounded-full object-cover bg-[#EBEBEB]" |
|||
onError={(event) => { |
|||
event.currentTarget.src = FALLBACK_AVATAR; |
|||
}} |
|||
/> |
|||
) : ( |
|||
<div className="size-full rounded-full bg-[#EBEBEB] flex items-center justify-center"> |
|||
<svg |
|||
width="24" |
|||
height="24" |
|||
viewBox="0 0 33 32" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="text-[#8B8B8B]" |
|||
> |
|||
<path |
|||
d="M16.5 17.3333C15.4802 17.3333 14.4833 17.0401 13.6353 16.4907C12.7874 15.9413 12.1265 15.1604 11.7362 14.2467C11.346 13.3331 11.2439 12.3278 11.4428 11.3579C11.6418 10.388 12.1329 9.49706 12.854 8.7978C13.5751 8.09853 14.4939 7.62233 15.4941 7.4294C16.4943 7.23648 17.531 7.33549 18.4732 7.71393C19.4154 8.09237 20.2207 8.73323 20.7873 9.55548C21.3538 10.3777 21.6562 11.3444 21.6562 12.3333C21.6562 13.6594 21.113 14.9312 20.146 15.8689C19.179 16.8065 17.8675 17.3333 16.5 17.3333ZM16.5 9.33333C15.8881 9.33333 15.29 9.50928 14.7812 9.83892C14.2724 10.1686 13.8759 10.6371 13.6417 11.1853C13.4076 11.7335 13.3463 12.3367 13.4657 12.9186C13.5851 13.5005 13.8797 14.0351 14.3124 14.4547C14.7451 14.8742 15.2963 15.1599 15.8964 15.2757C16.4966 15.3914 17.1186 15.332 17.6839 15.105C18.2492 14.8779 18.7324 14.4934 19.0724 14C19.4123 13.5067 19.5938 12.9267 19.5938 12.3333C19.5938 11.5377 19.2678 10.7746 18.6876 10.212C18.1074 9.6494 17.3205 9.33333 16.5 9.33333Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
</div> |
|||
)} |
|||
|
|||
{/* Status indicator dot badge (top-start) */} |
|||
<div className="absolute top-0 -left-0.5 rtl:-right-0.5 rtl:left-auto flex size-3 items-center justify-center rounded-full bg-white p-0.5"> |
|||
<div |
|||
className={`flex size-2.5 items-center justify-center rounded-full ${ |
|||
isOnline ? "bg-[#0EB13C]/20" : "bg-white" |
|||
}`}
|
|||
> |
|||
<div |
|||
className={`size-1.5 rounded-full ${ |
|||
isOnline ? "bg-[#0EB13C]" : "bg-[#747474]" |
|||
}`}
|
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Consultant Info */} |
|||
<div className="min-w-0 flex-1 flex flex-col justify-center gap-1"> |
|||
<div className="flex items-center gap-2"> |
|||
{advisor.is_ai && ( |
|||
<span className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#F45EA0] to-[#704EE9] text-[9px] font-bold text-white leading-none"> |
|||
AI |
|||
</span> |
|||
)} |
|||
<h3 className="truncate font-sans text-[15px] font-bold text-[#151C31] leading-tight text-start"> |
|||
{advisor.fullname ?? advisor.username} |
|||
</h3> |
|||
{rating > 0 && ( |
|||
<div className="ms-auto flex shrink-0 items-center gap-1"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 24 23" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 text-[#F4C51D]" |
|||
> |
|||
<path |
|||
d="M12.0137 0.5C12.3276 0.5 12.6304 0.567504 12.9268 0.705078C13.2033 0.83346 13.4498 1.03558 13.6641 1.32812L13.6699 1.33594L16.8877 5.55078L16.9814 5.67285L17.127 5.72168L22.0283 7.36426V7.36523C22.5075 7.53628 22.8608 7.81149 23.1113 8.18945C23.3746 8.58681 23.5 8.99869 23.5 9.43457C23.5 9.64108 23.4672 9.85698 23.3975 10.083C23.3466 10.2477 23.2761 10.4069 23.1855 10.5615L23.0889 10.7139L19.9551 15.2051L19.8613 15.3398L19.8652 15.5029L19.9766 20.2646C19.9765 20.8763 19.7652 21.3933 19.3281 21.8408C18.8902 22.2891 18.3925 22.5 17.8125 22.5C17.8116 22.5 17.8078 22.4994 17.8008 22.499C17.7924 22.4985 17.7811 22.4984 17.7666 22.4971C17.737 22.4944 17.6975 22.4898 17.6484 22.4834C17.5546 22.4711 17.4318 22.4514 17.2783 22.4258L12.123 20.9561L11.9854 20.917L11.8477 20.9561L6.71484 22.4355L6.69043 22.4434L6.66699 22.4521C6.64021 22.4629 6.60211 22.4717 6.54785 22.4717H6.24316C5.63483 22.4717 5.11477 22.2631 4.66016 21.8291C4.22037 21.4092 4.00813 20.8947 4.02344 20.248L4.13477 15.5029L4.13867 15.3408L4.0459 15.207L0.910156 10.6846C0.770004 10.4806 0.668646 10.2734 0.601562 10.0635C0.533314 9.8498 0.500011 9.64082 0.5 9.43457C0.5 8.99869 0.625355 8.58681 0.888672 8.18945C1.13917 7.81149 1.49253 7.53628 1.97168 7.36523L1.9707 7.36426L6.84668 5.72168L6.99023 5.67285L7.08301 5.55273L10.3291 1.33789L10.3359 1.32812C10.5481 1.0385 10.7969 0.836235 11.082 0.707031C11.3901 0.567552 11.6997 0.500029 12.0137 0.5Z" |
|||
fill="#F4C51D" |
|||
/> |
|||
</svg> |
|||
<span className="font-sans text-[10px] font-semibold text-[#F4C51D]"> |
|||
{rating.toFixed(1)} |
|||
</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<p className="truncate font-sans text-[10px] font-semibold text-[#73889D] text-start"> |
|||
{advisor.slogan ?? ""} |
|||
</p> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Contact Chips */} |
|||
{hasActiveContact && ( |
|||
<div className="flex flex-wrap items-center gap-2 pt-3 px-4"> |
|||
{hasChat && ( |
|||
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 14 14" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 shrink-0 text-[#0EB13C]" |
|||
> |
|||
<path |
|||
d="M7.01331 0.333252C10.8 0.333252 13.6666 3.43791 13.6666 6.9899C13.6666 11.1094 10.3066 13.6666 6.99998 13.6666C5.90665 13.6666 4.69331 13.3728 3.71998 12.7986C3.37998 12.5916 3.09331 12.4381 2.72665 12.5583L1.37998 12.9589C1.03998 13.0657 0.733313 12.7986 0.833313 12.4381L1.27998 10.9425C1.35331 10.7355 1.33998 10.5152 1.23331 10.3416C0.65998 9.28668 0.333313 8.13162 0.333313 7.00993C0.333313 3.498 3.13998 0.333252 7.01331 0.333252ZM10.06 6.16199C9.58665 6.16199 9.20665 6.54257 9.20665 7.01661C9.20665 7.48398 9.58665 7.87122 10.06 7.87122C10.5333 7.87122 10.9133 7.48398 10.9133 7.01661C10.9133 6.54257 10.5333 6.16199 10.06 6.16199ZM6.98665 6.16199C6.51998 6.15532 6.13331 6.54257 6.13331 7.00993C6.13331 7.48398 6.51331 7.86455 6.98665 7.87122C7.45998 7.87122 7.83998 7.48398 7.83998 7.01661C7.83998 6.54257 7.45998 6.16199 6.98665 6.16199ZM3.91331 6.16199C3.43998 6.16199 3.05998 6.54257 3.05998 7.01661C3.05998 7.48398 3.44665 7.87122 3.91331 7.87122C4.38665 7.86455 4.76665 7.48398 4.76665 7.01661C4.76665 6.54257 4.38665 6.16199 3.91331 6.16199Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
<span>{t["Text Message"] || "Text Message"}</span> |
|||
</div> |
|||
)} |
|||
|
|||
{hasVoice && ( |
|||
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 12 12" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 shrink-0 text-[#0EB13C]" |
|||
> |
|||
<path |
|||
d="M11.5643 0.576816L9.12681 0.0143159C8.86196 -0.0466215 8.59009 0.0916597 8.48228 0.340097L7.35728 2.9651C7.25884 3.19478 7.32446 3.46432 7.51899 3.62135L8.93931 4.78385C8.09556 6.5815 6.62134 8.07682 4.78618 8.93697L3.62368 7.51666C3.46431 7.32213 3.19712 7.2565 2.96743 7.35494L0.342432 8.47994C0.0916504 8.5901 -0.0466309 8.86197 0.0143066 9.12682L0.576807 11.5643C0.6354 11.8174 0.8604 12.0003 1.12524 12.0003C7.12759 12.0003 12.0002 7.13697 12.0002 1.12525C12.0002 0.862753 11.8198 0.63541 11.5643 0.576816Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
<span>{t["Voice Call"] || "Voice Call"}</span> |
|||
</div> |
|||
)} |
|||
|
|||
{hasVideo && ( |
|||
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 16 16" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 shrink-0 text-[#0EB13C]" |
|||
> |
|||
<path |
|||
fillRule="evenodd" |
|||
clipRule="evenodd" |
|||
d="M3.33398 3.33334C2.80355 3.33334 2.29484 3.54406 1.91977 3.91913C1.5447 4.2942 1.33398 4.80291 1.33398 5.33334V10.6667C1.33398 11.1971 1.5447 11.7058 1.91977 12.0809C2.29484 12.456 2.80355 12.6667 3.33398 12.6667H9.33398C9.86442 12.6667 10.3731 12.456 10.7482 12.0809C11.1233 11.7058 11.334 11.1971 11.334 10.6667V9.60934L12.9607 11.236C13.59 11.866 14.6673 11.4193 14.6673 10.5287V5.47134C14.6673 4.58068 13.59 4.13401 12.9607 4.76468L11.334 6.39001V5.33334C11.334 4.80291 11.1233 4.2942 10.7482 3.91913C10.3731 3.54406 9.86442 3.33334 9.33398 3.33334H3.33398Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
<span>{t["Video Call"] || "Video Call"}</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
{/* Topics Badges */} |
|||
{topics.length > 0 && ( |
|||
<> |
|||
<div className="my-2.5 border-t border-[#EBEBEB]" /> |
|||
<div className="flex flex-wrap items-center gap-2 px-4 pb-1"> |
|||
{topics.map((topic) => ( |
|||
<span |
|||
key={topic} |
|||
className="inline-flex items-center rounded-[24px] bg-[#F4F4F8] px-2.5 py-1 font-sans text-[10px] font-semibold text-[#747474]" |
|||
> |
|||
{topic} |
|||
</span> |
|||
))} |
|||
</div> |
|||
</> |
|||
)} |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Floating Unread Badge at Top Corner */} |
|||
{unreadCount > 0 && ( |
|||
<div className="absolute -top-2.5 end-0 z-10 flex items-center gap-1 rounded-[16px] bg-[#ECA533] px-2 py-0.5 text-white shadow-sm"> |
|||
<span className="text-[10px] font-bold">{unreadCount}</span> |
|||
<span className="text-[8.5px] font-normal"> |
|||
{t["New Message"] || "New Message"} |
|||
</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function AdvisorsSkeleton() { |
|||
return ( |
|||
<> |
|||
{[0, 1, 2].map((index) => ( |
|||
<div |
|||
key={index} |
|||
className="w-full max-w-[343px] animate-pulse rounded-[15px] border border-[#EBEBEB] bg-white py-3 shadow-[0_4px_16px_rgba(0,0,0,0.04)]" |
|||
> |
|||
<div className="flex items-center gap-3 px-3"> |
|||
<div className="size-12 shrink-0 rounded-full bg-[#F0F0F0]" /> |
|||
<div className="min-w-0 flex-1 flex flex-col gap-1.5"> |
|||
<div className="flex items-center justify-between"> |
|||
<div className="h-4 w-32 rounded bg-[#F0F0F0]" /> |
|||
<div className="h-3 w-8 rounded bg-[#F0F0F0]" /> |
|||
</div> |
|||
<div className="h-3 w-44 rounded bg-[#F0F0F0]" /> |
|||
</div> |
|||
</div> |
|||
<div className="flex gap-2 pt-3 px-4"> |
|||
<div className="h-5 w-20 rounded-[16px] bg-[#F0F0F0]" /> |
|||
<div className="h-5 w-20 rounded-[16px] bg-[#F0F0F0]" /> |
|||
</div> |
|||
<div className="my-2.5 border-t border-[#F0F0F0]" /> |
|||
<div className="flex gap-2 px-4 pb-1"> |
|||
<div className="h-5 w-16 rounded-[24px] bg-[#F0F0F0]" /> |
|||
<div className="h-5 w-20 rounded-[24px] bg-[#F0F0F0]" /> |
|||
<div className="h-5 w-14 rounded-[24px] bg-[#F0F0F0]" /> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</> |
|||
); |
|||
export default function MarriageAdvisorsPage() { |
|||
return <MarriageAdvisorsClient />; |
|||
} |
|||
@ -1,752 +1,5 @@ |
|||
"use client"; |
|||
import NewMatchProfileClient from "@/components/Componentes/new-match-profile-client"; |
|||
|
|||
import Image from "next/image"; |
|||
import { useRouter } from "next/navigation"; |
|||
import { useEffect, useMemo, useState } from "react"; |
|||
import Button from "@/components/Componentes/button"; |
|||
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet"; |
|||
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet"; |
|||
import InformationSheet from "@/components/Componentes/information-sheet"; |
|||
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; |
|||
import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot"; |
|||
import NavigationButton from "@/components/Componentes/navigation-button"; |
|||
import { PageBackground } from "@/components/Componentes/page-background"; |
|||
import StickyHeader from "@/components/Componentes/sticky-header"; |
|||
import type { |
|||
MarriageCaseStatus, |
|||
MarriageField, |
|||
MarriageFieldValue, |
|||
MarriageGender, |
|||
MarriagePhoneFieldValue, |
|||
MarriageProfileResponse, |
|||
} from "@/hooks/marriage/types"; |
|||
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; |
|||
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; |
|||
import { getSubmitPath } from "@/lib/get-submit-path"; |
|||
import { markMatchStarted } from "@/lib/match-start-grace"; |
|||
import { localizePath } from "@/translations/config"; |
|||
import { useI18n } from "@/translations/provider"; |
|||
|
|||
import { |
|||
formatFieldLabel, |
|||
formatFieldValue, |
|||
formatOptionValue, |
|||
getOrderedSummaryFields, |
|||
isMarriagePhoneFieldValue, |
|||
titleFromKey, |
|||
} from "@/lib/marriage-field-formatter"; |
|||
|
|||
function isImageField(field: MarriageField) { |
|||
return /(avatar|image|photo|picture|portrait|upload)/i.test( |
|||
`${field.key} ${field.label}`, |
|||
); |
|||
} |
|||
|
|||
function isFirstNameField(field: MarriageField) { |
|||
const key = (field.key || "").toLowerCase(); |
|||
return ( |
|||
key === "first_name" || |
|||
key === "personal_identity.first_name" || |
|||
key.endsWith(".first_name") || |
|||
key.endsWith("_first_name") || |
|||
key === "full_name" || |
|||
key === "fullname" || |
|||
key === "q1_full_name" || |
|||
key.endsWith(".full_name") || |
|||
key.endsWith("_full_name") |
|||
); |
|||
} |
|||
|
|||
function canAcceptProfile( |
|||
gender: MarriageGender | null | undefined, |
|||
status: MarriageCaseStatus | null | undefined, |
|||
) { |
|||
if (!gender || !status) { |
|||
return false; |
|||
} |
|||
|
|||
if (gender === "female") { |
|||
return status === "introduced" || status === "male_accepted"; |
|||
} |
|||
|
|||
return status === "introduced"; |
|||
} |
|||
|
|||
function MatchField({ |
|||
field, |
|||
dictionary, |
|||
}: { |
|||
field: MarriageField; |
|||
dictionary?: Record<string, string>; |
|||
}) { |
|||
const value = formatOptionValue(field.value, dictionary); |
|||
|
|||
if (!value || isImageField(field)) { |
|||
return null; |
|||
} |
|||
|
|||
const label = formatFieldLabel(field, dictionary); |
|||
|
|||
return ( |
|||
<div className="flex flex-col items-start w-full border-b border-black/10 pb-[14px] mt-[14px]"> |
|||
<p className="text-[12px] font-semibold leading-[17px] text-[#978787]"> |
|||
{label} |
|||
</p> |
|||
<p className="text-[16px] font-bold leading-[20px] text-[#111111] mt-[7px] text-left"> |
|||
{value} |
|||
</p> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function MatchPublicProfileFields({ |
|||
publicInfo, |
|||
dictionary, |
|||
}: { |
|||
publicInfo: MarriageField[] | null | undefined; |
|||
dictionary?: Record<string, string>; |
|||
}) { |
|||
const visibleFields = useMemo(() => { |
|||
if (!publicInfo) return []; |
|||
return publicInfo.filter((field) => { |
|||
if ( |
|||
field.value === null || |
|||
field.value === "" || |
|||
isImageField(field) || |
|||
isFirstNameField(field) |
|||
) { |
|||
return false; |
|||
} |
|||
if ((field as any).private === true) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}); |
|||
}, [publicInfo]); |
|||
|
|||
if (!visibleFields.length) { |
|||
return ( |
|||
<div className="mt-6 rounded-[16px] bg-white/70 p-5 text-center shadow-xs"> |
|||
<p className="group-12 font-medium text-[#747474]"> |
|||
{dictionary?.["No public information is available to display."] || |
|||
"No public information is available to display."} |
|||
</p> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<div className="flex flex-col w-full mt-6" style={{ gap: "14px" }}> |
|||
{visibleFields.map((field) => ( |
|||
<MatchField |
|||
key={field.key} |
|||
field={field} |
|||
dictionary={dictionary} |
|||
/> |
|||
))} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function NewMatchProfileSkeleton({ |
|||
hideBackButton = false, |
|||
onClose, |
|||
}: { |
|||
hideBackButton?: boolean; |
|||
onClose?: () => void; |
|||
}) { |
|||
const { dictionary: t } = useI18n(); |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
<main className="-mx-[17px] flex h-dvh flex-col overflow-hidden bg-[#F5F5F5]"> |
|||
<StickyHeader sticky className="shrink-0"> |
|||
<div className="flex items-center justify-between gap-3"> |
|||
{!hideBackButton ? ( |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="back" |
|||
iconLabel={t["Go back"]} |
|||
onClick={onClose} |
|||
/> |
|||
) : ( |
|||
<div className="size-10 shrink-0" /> |
|||
)} |
|||
<h1 |
|||
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" |
|||
> |
|||
{t["More detail"]} |
|||
</h1> |
|||
<div className="size-10 shrink-0" /> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="min-h-0 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" /> |
|||
</div> |
|||
|
|||
<div className="mt-6 space-y-4 rounded-[18px] bg-white/80 p-5 shadow-xs"> |
|||
<LoadingSkeleton className="h-4 w-44 rounded-md" /> |
|||
|
|||
<div className="space-y-4 pt-1"> |
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-24" /> |
|||
<LoadingSkeleton className="h-4 w-48" /> |
|||
</div> |
|||
|
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-28" /> |
|||
<LoadingSkeleton className="h-4 w-36" /> |
|||
</div> |
|||
|
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-20" /> |
|||
<LoadingSkeleton className="h-4 w-56" /> |
|||
</div> |
|||
|
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-32" /> |
|||
<LoadingSkeleton className="h-4 w-40" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
<div |
|||
style={{ paddingBottom: `calc(16px + var(--safe-bottom, 0px))` }} |
|||
className="shrink-0 z-30 w-full rounded-t-[24px] bg-white px-[17px] pt-4 shadow-[0_-4px_24px_rgba(0,0,0,0.08)]" |
|||
> |
|||
<div className="flex gap-3"> |
|||
<LoadingSkeleton className="w-1/3 h-[52px] rounded-[12px]" /> |
|||
<LoadingSkeleton className="w-2/3 h-[52px] rounded-[12px]" /> |
|||
</div> |
|||
</div> |
|||
</main> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
function formatBoldText(text: string) { |
|||
if (!text) return ""; |
|||
const parts = text.split(/\*\*([^*]+)\*\*/g); |
|||
return parts.map((part, index) => { |
|||
if (index % 2 === 1) { |
|||
return ( |
|||
<strong key={index} className="font-bold text-[#111111]"> |
|||
{part} |
|||
</strong> |
|||
); |
|||
} |
|||
return part; |
|||
}); |
|||
} |
|||
|
|||
type NewMatchProfilePageProps = { |
|||
onClose?: () => void; |
|||
profile?: MarriageProfileResponse; |
|||
}; |
|||
|
|||
export default function NewMatchProfilePage({ |
|||
onClose, |
|||
profile: profileProp, |
|||
}: NewMatchProfilePageProps = {}) { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const router = useRouter(); |
|||
const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false); |
|||
const [isFemaleConsentChecked, setIsFemaleConsentChecked] = useState(false); |
|||
const [isDeclineSheetOpen, setIsDeclineSheetOpen] = useState(false); |
|||
const [isMaleDeclineWarningOpen, setIsMaleDeclineWarningOpen] = |
|||
useState(false); |
|||
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = |
|||
useState(false); |
|||
const { |
|||
data: queriedProfile, |
|||
isLoading: isQueryLoading, |
|||
refetch: refetchProfile, |
|||
} = useMarriageProfileQuery({ |
|||
enabled: !profileProp, |
|||
}); |
|||
|
|||
const profile = profileProp ?? queriedProfile; |
|||
const isLoading = !profileProp && isQueryLoading; |
|||
|
|||
useEffect(() => { |
|||
// If rendered as an overlay (onClose is passed), NEVER redirect away from current page
|
|||
if (!profile || Boolean(onClose)) { |
|||
return; |
|||
} |
|||
const targetPath = getSubmitPath(profile); |
|||
const caseStatus = profile?.active_case?.status; |
|||
const isProfileMatched = profile?.status === "matched"; |
|||
const isViewingAllowed = |
|||
caseStatus === "female_accepted" || |
|||
caseStatus === "payment_pending" || |
|||
caseStatus === "payment_done" || |
|||
caseStatus === "contacted" || |
|||
caseStatus === "finalized" || |
|||
isProfileMatched; |
|||
|
|||
if ( |
|||
targetPath !== "/new-match" && |
|||
targetPath !== "/request-sent" && |
|||
targetPath !== "/request-accepted" && |
|||
!isViewingAllowed |
|||
) { |
|||
router.replace(localizePath(targetPath, locale)); |
|||
} |
|||
}, [profile, locale, router, onClose]); |
|||
|
|||
const caseId = profile?.active_case?.case_id; |
|||
const caseStatus = profile?.active_case?.status; |
|||
const isFemaleProfile = profile?.gender === "female"; |
|||
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { |
|||
onSuccess: async (data, variables) => { |
|||
onClose?.(); |
|||
if (variables.action === "accept") { |
|||
const nextPath = |
|||
profile?.gender === "female" |
|||
? "/request-accepted" |
|||
: "/request-sent"; |
|||
router.replace(localizePath(nextPath, locale)); |
|||
return; |
|||
} |
|||
|
|||
router.replace(localizePath("/finding-match", locale)); |
|||
}, |
|||
}); |
|||
|
|||
const candidateName = useMemo(() => { |
|||
const publicInfo = profile?.match_summary?.public_info ?? []; |
|||
const firstNameIdx = publicInfo.findIndex( |
|||
(f) => |
|||
f.key === "personal_identity.first_name" || |
|||
f.key?.endsWith(".first_name") || |
|||
f.key === "first_name", |
|||
); |
|||
const lastNameIdx = publicInfo.findIndex( |
|||
(f) => |
|||
f.key === "personal_identity.last_name" || |
|||
f.key?.endsWith(".last_name") || |
|||
f.key === "last_name", |
|||
); |
|||
|
|||
if (firstNameIdx !== -1 && publicInfo[firstNameIdx].value) { |
|||
const firstName = formatFieldValue(publicInfo[firstNameIdx].value); |
|||
if (lastNameIdx !== -1 && publicInfo[lastNameIdx].value) { |
|||
const lastName = formatFieldValue(publicInfo[lastNameIdx].value); |
|||
return `${firstName} ${lastName}`.trim(); |
|||
} |
|||
return firstName || ""; |
|||
} |
|||
|
|||
const nameField = publicInfo.find( |
|||
(f) => |
|||
f.key === "q1_full_name" || |
|||
f.key.endsWith(".full_name") || |
|||
f.key.toLowerCase().includes("fullname") || |
|||
f.key.toLowerCase().includes("display_name"), |
|||
); |
|||
const formatted = formatFieldValue(nameField?.value ?? null); |
|||
if (formatted) return formatted; |
|||
|
|||
const firstVal = publicInfo.find((f) => Boolean(f.value))?.value ?? null; |
|||
return ( |
|||
formatFieldValue(firstVal) || |
|||
(profile?.match_summary?.id |
|||
? `Profile #${profile.match_summary.id}` |
|||
: "Profile") |
|||
); |
|||
}, [profile?.match_summary]); |
|||
|
|||
const isCandidateFemale = |
|||
profile?.match_summary?.gender === "female" || profile?.gender === "male"; |
|||
const avatarSrc = isCandidateFemale |
|||
? "/assets/images/female_avatar.svg" |
|||
: "/assets/images/Group 1597880481.png"; |
|||
|
|||
const isRedirecting = useMemo(() => { |
|||
if (!profile) return false; |
|||
const targetPath = getSubmitPath(profile); |
|||
const caseStatus = profile?.active_case?.status; |
|||
const isProfileMatched = profile?.status === "matched"; |
|||
const isViewingAllowed = |
|||
caseStatus === "payment_done" || |
|||
caseStatus === "contacted" || |
|||
caseStatus === "finalized" || |
|||
isProfileMatched; |
|||
|
|||
return ( |
|||
targetPath !== "/new-match" && |
|||
targetPath !== "/request-sent" && |
|||
!isViewingAllowed |
|||
); |
|||
}, [profile]); |
|||
|
|||
const isSubmitting = respondMutation.isPending; |
|||
|
|||
if ((isLoading && !profile) || !profile) { |
|||
return ( |
|||
<NewMatchProfileSkeleton |
|||
hideBackButton={isSubmitting} |
|||
onClose={onClose} |
|||
/> |
|||
); |
|||
} |
|||
const isAcceptProfileEnabled = |
|||
Boolean(caseId) && |
|||
!isSubmitting && |
|||
canAcceptProfile(profile?.gender, caseStatus); |
|||
const isDeclineProfileEnabled = isAcceptProfileEnabled; |
|||
|
|||
const nameParts = candidateName.trim().split(/\s+/); |
|||
const _firstName = nameParts[0] || ""; |
|||
const _lastName = nameParts.slice(1).join(" ") || ""; |
|||
|
|||
const mainClass = "-mx-[17px] flex h-dvh flex-col overflow-hidden"; |
|||
|
|||
const mainStyle = { |
|||
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, |
|||
backgroundColor: "#F5F5F5", |
|||
backgroundRepeat: "repeat-y", |
|||
backgroundSize: "100% auto", |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
{isRequestSheetOpen ? ( |
|||
isFemaleProfile ? ( |
|||
<FemaleConsentSheet |
|||
title={t["Final Consent"]} |
|||
description={formatBoldText( |
|||
t[ |
|||
"Approving this profile will **display your contact number** to the gentleman. Ensure **family consensus** before proceeding." |
|||
], |
|||
)} |
|||
buttons={({ close }) => ( |
|||
<div className="space-y-5"> |
|||
<button |
|||
type="button" |
|||
onClick={() => setIsFemaleConsentChecked((value) => !value)} |
|||
className="flex w-full items-start gap-4 rounded-[15px] border-2 border-white bg-white/50 px-5 py-5 text-left shadow-[0_8px_24px_rgba(0,0,0,0.05)]" |
|||
> |
|||
<span |
|||
className={[ |
|||
"mt-1 flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full border-1", |
|||
isFemaleConsentChecked |
|||
? "border-[#F0445B] bg-[#F0445B]" |
|||
: "border-[#2B2B2B] bg-white", |
|||
].join(" ")} |
|||
aria-hidden="true" |
|||
> |
|||
{isFemaleConsentChecked ? ( |
|||
<span className="h-3 w-3 rounded-full bg-[#F0445B]" /> |
|||
) : null} |
|||
</span> |
|||
<span className="text-xs leading-[1.35] text-[#3F3F3F]"> |
|||
{formatBoldText( |
|||
t[ |
|||
"I confirm the family has **reviewed this profile** and **consents to communicate**." |
|||
], |
|||
)} |
|||
</span> |
|||
</button> |
|||
|
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button |
|||
variant="outlined" |
|||
className="py-[18px] text-[18px]" |
|||
onClick={close} |
|||
> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
className="py-[18px] text-[18px]" |
|||
disabled={ |
|||
!caseId || |
|||
isSubmitting || |
|||
!isAcceptProfileEnabled || |
|||
!isFemaleConsentChecked |
|||
} |
|||
onClick={async () => { |
|||
close(); |
|||
if (!isAcceptProfileEnabled) { |
|||
return; |
|||
} |
|||
|
|||
markMatchStarted(); |
|||
await respondMutation.mutateAsync({ action: "accept" }); |
|||
}} |
|||
> |
|||
{t.Confirm} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
)} |
|||
onClose={() => { |
|||
setIsFemaleConsentChecked(false); |
|||
setIsRequestSheetOpen(false); |
|||
}} |
|||
/> |
|||
) : ( |
|||
<InformationSheet |
|||
icon="check" |
|||
title={t["Request to Proceed"]} |
|||
description={ |
|||
t[ |
|||
"Upon your approval, your request will be sent to the lady, and we must await her response. This process may take between 2 to 4 days. Thank you for your patience." |
|||
] |
|||
} |
|||
buttons={({ close }) => ( |
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button |
|||
variant="outlined" |
|||
className="py-[18px]" |
|||
onClick={close} |
|||
> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
className="py-[18px]" |
|||
disabled={ |
|||
!isAcceptProfileEnabled || respondMutation.isPending |
|||
} |
|||
isLoading={respondMutation.isPending} |
|||
onClick={async () => { |
|||
close(); |
|||
if (!isAcceptProfileEnabled) { |
|||
return; |
|||
} |
|||
|
|||
markMatchStarted(); |
|||
await respondMutation.mutateAsync({ action: "accept" }); |
|||
}} |
|||
> |
|||
{t.Confirm} |
|||
</Button> |
|||
</div> |
|||
)} |
|||
onClose={() => setIsRequestSheetOpen(false)} |
|||
/> |
|||
) |
|||
) : null} |
|||
{isDeclineSheetOpen ? ( |
|||
<InformationSheet |
|||
icon="warning" |
|||
title={t["Decline Profile"] || t["Reject Profile"]} |
|||
description={ |
|||
t[ |
|||
"Are you sure you've fully reviewed the profile and want to decline this profile?" |
|||
] || |
|||
t[ |
|||
"Are you sure you've fully reviewed the profile and want to reject this profile?" |
|||
] |
|||
} |
|||
buttons={({ close }) => ( |
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button variant="outlined" className="py-[18px]" onClick={close}> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
onClick={() => { |
|||
close(); |
|||
setIsDismissReasonSheetOpen(true); |
|||
}} |
|||
> |
|||
{t.Decline || t["Decline"] || t.Reject} |
|||
</Button> |
|||
</div> |
|||
)} |
|||
onClose={() => setIsDeclineSheetOpen(false)} |
|||
/> |
|||
) : null} |
|||
{isMaleDeclineWarningOpen ? ( |
|||
<InformationSheet |
|||
icon="warning" |
|||
title={t["Decline Warning"] || t["Rejection Warning"]} |
|||
description={ |
|||
t[ |
|||
"Please review the person’s full profile once more before making your final decision." |
|||
] |
|||
} |
|||
buttons={({ close }) => ( |
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button variant="outlined" className="py-[18px]" onClick={close}> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
className="py-[18px]" |
|||
onClick={() => { |
|||
close(); |
|||
setIsDismissReasonSheetOpen(true); |
|||
}} |
|||
> |
|||
{t.Decline || t["Decline"] || t.Reject} |
|||
</Button> |
|||
</div> |
|||
)} |
|||
onClose={() => setIsMaleDeclineWarningOpen(false)} |
|||
/> |
|||
) : null} |
|||
{isDismissReasonSheetOpen ? ( |
|||
<DismissReasonSheet |
|||
onClose={() => setIsDismissReasonSheetOpen(false)} |
|||
onSubmit={async (reason) => { |
|||
if (!caseId) { |
|||
return; |
|||
} |
|||
|
|||
await respondMutation.mutateAsync({ |
|||
action: "reject", |
|||
custom_note: reason, |
|||
}); |
|||
}} |
|||
/> |
|||
) : null} |
|||
|
|||
<main className={mainClass} style={mainStyle}> |
|||
<StickyHeader sticky className="shrink-0"> |
|||
<div className="flex items-center justify-between gap-3"> |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="back" |
|||
iconLabel={t["Go back"]} |
|||
onClick={onClose} |
|||
/> |
|||
<h1 |
|||
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" |
|||
> |
|||
{t["More detail"]} |
|||
</h1> |
|||
<div className="size-10 shrink-0" /> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="min-h-0 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={{ |
|||
width: "343px", |
|||
height: isCandidateFemale ? "124px" : "125px", |
|||
gap: "10px", |
|||
}} |
|||
> |
|||
{/* Frame 2095586585 - Avatar Section */} |
|||
<div |
|||
className="flex flex-col items-center justify-center w-full" |
|||
style={{ |
|||
height: "125px", |
|||
gap: "8px", |
|||
}} |
|||
> |
|||
{/* Avatar Group */} |
|||
<div className="relative overflow-hidden rounded-full w-[90px] h-[90px]"> |
|||
<Image |
|||
src={avatarSrc} |
|||
alt="" |
|||
width={90} |
|||
height={90} |
|||
className="w-full h-full object-cover" |
|||
/> |
|||
</div> |
|||
|
|||
{/* Frame 2095586663 - Names Section */} |
|||
<div |
|||
className="flex flex-row items-center justify-center w-full animate-fade-in" |
|||
style={{ |
|||
height: "26px", |
|||
}} |
|||
> |
|||
<span |
|||
className="font-bold text-[22px] leading-[26px] text-[#F0445B] text-center" |
|||
style={{ |
|||
WebkitTextStroke: "2px rgba(255, 255, 255, 0.5)", |
|||
paintOrder: "stroke fill", |
|||
}} |
|||
> |
|||
{candidateName} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<MatchPublicProfileFields |
|||
publicInfo={profile?.match_summary?.public_info} |
|||
dictionary={t} |
|||
/> |
|||
</section> |
|||
|
|||
<div |
|||
style={{ paddingBottom: `calc(16px + var(--safe-bottom, 0px))` }} |
|||
className="shrink-0 z-30 w-full rounded-t-[24px] bg-white px-[17px] pt-4 shadow-[0_-4px_24px_rgba(0,0,0,0.08)]" |
|||
> |
|||
{(Boolean(onClose) && !isAcceptProfileEnabled) || |
|||
caseStatus === "female_accepted" || |
|||
caseStatus === "payment_pending" || |
|||
caseStatus === "payment_done" || |
|||
caseStatus === "contacted" || |
|||
caseStatus === "finalized" || |
|||
profile?.status === "matched" ? ( |
|||
<button |
|||
type="button" |
|||
onClick={() => (onClose ? onClose() : router.back())} |
|||
className="w-full h-[52px] flex items-center justify-center rounded-[12px] bg-[#F5F5F5] text-[#36363C] font-semibold text-[16px] transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#EBEBEB]" |
|||
> |
|||
{t.Back} |
|||
</button> |
|||
) : ( |
|||
<div className="flex gap-3"> |
|||
<button |
|||
type="button" |
|||
disabled={!isDeclineProfileEnabled || isSubmitting} |
|||
onClick={() => { |
|||
if (isFemaleProfile) { |
|||
setIsDeclineSheetOpen(true); |
|||
} else { |
|||
setIsMaleDeclineWarningOpen(true); |
|||
} |
|||
}} |
|||
className="inline-flex w-1/3 h-[52px] items-center justify-center rounded-[12px] border border-[#BFBFBF] bg-white px-4 text-[16px] font-semibold text-[#9A9A9A] disabled:cursor-not-allowed disabled:opacity-50" |
|||
> |
|||
{t.Decline || t["Decline"] || t.Reject} |
|||
</button> |
|||
<button |
|||
type="button" |
|||
disabled={!isAcceptProfileEnabled || isSubmitting} |
|||
onClick={() => { |
|||
if (!isAcceptProfileEnabled) { |
|||
return; |
|||
} |
|||
|
|||
if (isFemaleProfile) { |
|||
setIsFemaleConsentChecked(false); |
|||
} |
|||
|
|||
setIsRequestSheetOpen(true); |
|||
}} |
|||
className="inline-flex w-2/3 h-[52px] whitespace-nowrap items-center justify-center gap-1 rounded-[12px] bg-[#F0445B] px-4 text-[16px] font-semibold text-white shadow-[0_8px_16px_rgba(240,68,91,0.24)] disabled:cursor-not-allowed disabled:opacity-50" |
|||
> |
|||
{isSubmitting ? ( |
|||
<LoadingThreeDot /> |
|||
) : ( |
|||
<> |
|||
<Image |
|||
src="/assets/images/Icfdason.svg" |
|||
alt="" |
|||
width={28} |
|||
height={28} |
|||
/> |
|||
<span>{t["Accept Profile"]}</span> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</main> |
|||
</> |
|||
); |
|||
export default function NewMatchProfilePage() { |
|||
return <NewMatchProfileClient />; |
|||
} |
|||
@ -0,0 +1,373 @@ |
|||
"use client"; |
|||
|
|||
import { useState } from "react"; |
|||
import Image from "next/image"; |
|||
import { Ic } from "@/icons"; |
|||
import StickyHeader from "@/components/Componentes/sticky-header"; |
|||
import NavigationButton from "@/components/Componentes/navigation-button"; |
|||
import { PageBackground } from "@/components/Componentes/page-background"; |
|||
import SupportSheet from "@/components/Componentes/support-sheet"; |
|||
import { useI18n } from "@/translations/provider"; |
|||
import { |
|||
useMarriageAdvisorsQuery, |
|||
type MarriageConsultant, |
|||
} from "@/hooks/marriage/use-marriage-advisors"; |
|||
import { postActionToFlutter } from "@/lib/webview-actions"; |
|||
|
|||
const FALLBACK_AVATAR = "/assets/images/Avatar Image.png"; |
|||
|
|||
type MarriageAdvisorsClientProps = { |
|||
onClose?: () => void; |
|||
}; |
|||
|
|||
export default function MarriageAdvisorsClient({ |
|||
onClose, |
|||
}: MarriageAdvisorsClientProps = {}) { |
|||
const { dictionary: t } = useI18n(); |
|||
const [isSupportOpen, setIsSupportOpen] = useState(false); |
|||
|
|||
const { data, isLoading, isError, refetch } = useMarriageAdvisorsQuery(); |
|||
const advisors = data?.results ?? []; |
|||
|
|||
const mainStyle = { |
|||
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, |
|||
backgroundColor: "#F5F5F5", |
|||
backgroundRepeat: "repeat-y", |
|||
backgroundSize: "100% auto", |
|||
}; |
|||
|
|||
// Opens the native Talk consultant page; the Flutter side fetches the
|
|||
// consultant by username, so only the bare username is needed. Outside the
|
|||
// WebView (plain browser) postActionToFlutter is a no-op returning false.
|
|||
const openConsultantPage = (username: string) => { |
|||
postActionToFlutter("open_consultant_page", { consultant: username }); |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
<main |
|||
className="-mx-[17px] flex h-dvh flex-col overflow-hidden pb-10" |
|||
style={mainStyle} |
|||
> |
|||
<StickyHeader sticky className="shrink-0"> |
|||
<div className="flex items-center justify-between gap-3"> |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="back" |
|||
iconLabel={t["Go back"]} |
|||
onClick={onClose} |
|||
/> |
|||
<h1 |
|||
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" |
|||
> |
|||
{t["Marriage advisors"]} |
|||
</h1> |
|||
<div className="size-10 shrink-0" /> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-[17px] pb-32 pt-5 flex flex-col items-center gap-4"> |
|||
{isLoading && <AdvisorsSkeleton />} |
|||
|
|||
{isError && ( |
|||
<div className="flex w-[343px] flex-col items-center gap-3 rounded-[15px] border border-[#EBEBEB] bg-white px-4 py-8 text-center shadow-[0px_4px_12px_rgba(0,0,0,0.05)]"> |
|||
<p |
|||
className="text-[14px] leading-[20px] text-[#747474]" |
|||
> |
|||
{t["Failed to load advisors."]} |
|||
</p> |
|||
<button |
|||
type="button" |
|||
onClick={() => refetch()} |
|||
className="cursor-pointer rounded-[20px] border-none bg-[rgba(14,177,60,0.1)] px-4 py-1.5 text-[12px] font-semibold text-[#0EB13C] transition-transform active:scale-95" |
|||
> |
|||
{t["Try again"]} |
|||
</button> |
|||
</div> |
|||
)} |
|||
|
|||
{!isLoading && !isError && advisors.length === 0 && ( |
|||
<div className="flex w-[343px] flex-col items-center rounded-[15px] border border-[#EBEBEB] bg-white px-4 py-8 text-center shadow-[0px_4px_12px_rgba(0,0,0,0.05)]"> |
|||
<p |
|||
className="text-[14px] leading-[20px] text-[#747474]" |
|||
> |
|||
{t["No advisors are currently available."]} |
|||
</p> |
|||
</div> |
|||
)} |
|||
|
|||
{advisors.map((advisor) => ( |
|||
<AdvisorCard |
|||
key={advisor.username} |
|||
advisor={advisor} |
|||
onOpen={() => openConsultantPage(advisor.username)} |
|||
/> |
|||
))} |
|||
</section> |
|||
</main> |
|||
|
|||
<SupportSheet |
|||
isOpen={isSupportOpen} |
|||
onClose={() => setIsSupportOpen(false)} |
|||
/> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
function AdvisorCard({ |
|||
advisor, |
|||
onOpen, |
|||
}: { |
|||
advisor: MarriageConsultant; |
|||
onOpen: () => void; |
|||
}) { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const isRtl = locale === "fa" || locale === "ar" || locale === "ur"; |
|||
const unreadCount = advisor.unread_count ?? 0; |
|||
const isOnline = advisor.status === "online"; |
|||
const rating = Number(advisor.avg_rate ?? 0); |
|||
const hasChat = advisor.contact_type?.includes("chat"); |
|||
const hasVoice = advisor.contact_type?.includes("voice"); |
|||
const hasVideo = advisor.contact_type?.includes("video"); |
|||
const hasActiveContact = hasChat || hasVoice || hasVideo; |
|||
const topics = advisor.topics?.filter(Boolean) ?? []; |
|||
|
|||
return ( |
|||
<div |
|||
role="button" |
|||
tabIndex={0} |
|||
aria-label={advisor.fullname ?? advisor.username} |
|||
onClick={onOpen} |
|||
onKeyDown={(event) => { |
|||
if (event.key === "Enter" || event.key === " ") { |
|||
event.preventDefault(); |
|||
onOpen(); |
|||
} |
|||
}} |
|||
className="relative w-full max-w-[343px] cursor-pointer transition-transform active:scale-[0.98] outline-none" |
|||
> |
|||
{/* Outer Card Container */} |
|||
<div |
|||
className={`w-full rounded-[15px] bg-white py-3 shadow-[0_4px_16px_rgba(0,0,0,0.06)] transition-all ${ |
|||
unreadCount > 0 |
|||
? "border border-[#ECA533]" |
|||
: "border border-[#EBEBEB]" |
|||
}`}
|
|||
> |
|||
<div className="flex flex-col"> |
|||
{/* Header Row: Avatar + Info + Rating */} |
|||
<div className="flex items-center gap-3 px-3"> |
|||
{/* Avatar with Status Dot */} |
|||
<div className="relative size-12 shrink-0"> |
|||
{advisor.avatar_url ? ( |
|||
// eslint-disable-next-line @next/next/no-img-element
|
|||
<img |
|||
src={advisor.avatar_url} |
|||
alt={advisor.fullname ?? advisor.username} |
|||
className="size-full rounded-full object-cover bg-[#EBEBEB]" |
|||
onError={(event) => { |
|||
event.currentTarget.src = FALLBACK_AVATAR; |
|||
}} |
|||
/> |
|||
) : ( |
|||
<div className="size-full rounded-full bg-[#EBEBEB] flex items-center justify-center"> |
|||
<svg |
|||
width="24" |
|||
height="24" |
|||
viewBox="0 0 33 32" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="text-[#8B8B8B]" |
|||
> |
|||
<path |
|||
d="M16.5 17.3333C15.4802 17.3333 14.4833 17.0401 13.6353 16.4907C12.7874 15.9413 12.1265 15.1604 11.7362 14.2467C11.346 13.3331 11.2439 12.3278 11.4428 11.3579C11.6418 10.388 12.1329 9.49706 12.854 8.7978C13.5751 8.09853 14.4939 7.62233 15.4941 7.4294C16.4943 7.23648 17.531 7.33549 18.4732 7.71393C19.4154 8.09237 20.2207 8.73323 20.7873 9.55548C21.3538 10.3777 21.6562 11.3444 21.6562 12.3333C21.6562 13.6594 21.113 14.9312 20.146 15.8689C19.179 16.8065 17.8675 17.3333 16.5 17.3333ZM16.5 9.33333C15.8881 9.33333 15.29 9.50928 14.7812 9.83892C14.2724 10.1686 13.8759 10.6371 13.6417 11.1853C13.4076 11.7335 13.3463 12.3367 13.4657 12.9186C13.5851 13.5005 13.8797 14.0351 14.3124 14.4547C14.7451 14.8742 15.2963 15.1599 15.8964 15.2757C16.4966 15.3914 17.1186 15.332 17.6839 15.105C18.2492 14.8779 18.7324 14.4934 19.0724 14C19.4123 13.5067 19.5938 12.9267 19.5938 12.3333C19.5938 11.5377 19.2678 10.7746 18.6876 10.212C18.1074 9.6494 17.3205 9.33333 16.5 9.33333Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
</div> |
|||
)} |
|||
|
|||
{/* Status indicator dot badge (top-start) */} |
|||
<div className="absolute top-0 -left-0.5 rtl:-right-0.5 rtl:left-auto flex size-3 items-center justify-center rounded-full bg-white p-0.5"> |
|||
<div |
|||
className={`flex size-2.5 items-center justify-center rounded-full ${ |
|||
isOnline ? "bg-[#0EB13C]/20" : "bg-white" |
|||
}`}
|
|||
> |
|||
<div |
|||
className={`size-1.5 rounded-full ${ |
|||
isOnline ? "bg-[#0EB13C]" : "bg-[#747474]" |
|||
}`}
|
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Consultant Info */} |
|||
<div className="min-w-0 flex-1 flex flex-col justify-center gap-1"> |
|||
<div className="flex items-center gap-2"> |
|||
{advisor.is_ai && ( |
|||
<span className="inline-flex size-4 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-[#F45EA0] to-[#704EE9] text-[9px] font-bold text-white leading-none"> |
|||
AI |
|||
</span> |
|||
)} |
|||
<h3 className="truncate font-sans text-[15px] font-bold text-[#151C31] leading-tight text-start"> |
|||
{advisor.fullname ?? advisor.username} |
|||
</h3> |
|||
{rating > 0 && ( |
|||
<div className="ms-auto flex shrink-0 items-center gap-1"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 24 23" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 text-[#F4C51D]" |
|||
> |
|||
<path |
|||
d="M12.0137 0.5C12.3276 0.5 12.6304 0.567504 12.9268 0.705078C13.2033 0.83346 13.4498 1.03558 13.6641 1.32812L13.6699 1.33594L16.8877 5.55078L16.9814 5.67285L17.127 5.72168L22.0283 7.36426V7.36523C22.5075 7.53628 22.8608 7.81149 23.1113 8.18945C23.3746 8.58681 23.5 8.99869 23.5 9.43457C23.5 9.64108 23.4672 9.85698 23.3975 10.083C23.3466 10.2477 23.2761 10.4069 23.1855 10.5615L23.0889 10.7139L19.9551 15.2051L19.8613 15.3398L19.8652 15.5029L19.9766 20.2646C19.9765 20.8763 19.7652 21.3933 19.3281 21.8408C18.8902 22.2891 18.3925 22.5 17.8125 22.5C17.8116 22.5 17.8078 22.4994 17.8008 22.499C17.7924 22.4985 17.7811 22.4984 17.7666 22.4971C17.737 22.4944 17.6975 22.4898 17.6484 22.4834C17.5546 22.4711 17.4318 22.4514 17.2783 22.4258L12.123 20.9561L11.9854 20.917L11.8477 20.9561L6.71484 22.4355L6.69043 22.4434L6.66699 22.4521C6.64021 22.4629 6.60211 22.4717 6.54785 22.4717H6.24316C5.63483 22.4717 5.11477 22.2631 4.66016 21.8291C4.22037 21.4092 4.00813 20.8947 4.02344 20.248L4.13477 15.5029L4.13867 15.3408L4.0459 15.207L0.910156 10.6846C0.770004 10.4806 0.668646 10.2734 0.601562 10.0635C0.533314 9.8498 0.500011 9.64082 0.5 9.43457C0.5 8.99869 0.625355 8.58681 0.888672 8.18945C1.13917 7.81149 1.49253 7.53628 1.97168 7.36523L1.9707 7.36426L6.84668 5.72168L6.99023 5.67285L7.08301 5.55273L10.3291 1.33789L10.3359 1.32812C10.5481 1.0385 10.7969 0.836235 11.082 0.707031C11.3901 0.567552 11.6997 0.500029 12.0137 0.5Z" |
|||
fill="#F4C51D" |
|||
/> |
|||
</svg> |
|||
<span className="font-sans text-[10px] font-semibold text-[#F4C51D]"> |
|||
{rating.toFixed(1)} |
|||
</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<p className="truncate font-sans text-[10px] font-semibold text-[#73889D] text-start"> |
|||
{advisor.slogan ?? ""} |
|||
</p> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Contact Chips */} |
|||
{hasActiveContact && ( |
|||
<div className="flex flex-wrap items-center gap-2 pt-3 px-4"> |
|||
{hasChat && ( |
|||
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 14 14" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 shrink-0 text-[#0EB13C]" |
|||
> |
|||
<path |
|||
d="M7.01331 0.333252C10.8 0.333252 13.6666 3.43791 13.6666 6.9899C13.6666 11.1094 10.3066 13.6666 6.99998 13.6666C5.90665 13.6666 4.69331 13.3728 3.71998 12.7986C3.37998 12.5916 3.09331 12.4381 2.72665 12.5583L1.37998 12.9589C1.03998 13.0657 0.733313 12.7986 0.833313 12.4381L1.27998 10.9425C1.35331 10.7355 1.33998 10.5152 1.23331 10.3416C0.65998 9.28668 0.333313 8.13162 0.333313 7.00993C0.333313 3.498 3.13998 0.333252 7.01331 0.333252ZM10.06 6.16199C9.58665 6.16199 9.20665 6.54257 9.20665 7.01661C9.20665 7.48398 9.58665 7.87122 10.06 7.87122C10.5333 7.87122 10.9133 7.48398 10.9133 7.01661C10.9133 6.54257 10.5333 6.16199 10.06 6.16199ZM6.98665 6.16199C6.51998 6.15532 6.13331 6.54257 6.13331 7.00993C6.13331 7.48398 6.51331 7.86455 6.98665 7.87122C7.45998 7.87122 7.83998 7.48398 7.83998 7.01661C7.83998 6.54257 7.45998 6.16199 6.98665 6.16199ZM3.91331 6.16199C3.43998 6.16199 3.05998 6.54257 3.05998 7.01661C3.05998 7.48398 3.44665 7.87122 3.91331 7.87122C4.38665 7.86455 4.76665 7.48398 4.76665 7.01661C4.76665 6.54257 4.38665 6.16199 3.91331 6.16199Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
<span>{t["Text Message"] || "Text Message"}</span> |
|||
</div> |
|||
)} |
|||
|
|||
{hasVoice && ( |
|||
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 12 12" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 shrink-0 text-[#0EB13C]" |
|||
> |
|||
<path |
|||
d="M11.5643 0.576816L9.12681 0.0143159C8.86196 -0.0466215 8.59009 0.0916597 8.48228 0.340097L7.35728 2.9651C7.25884 3.19478 7.32446 3.46432 7.51899 3.62135L8.93931 4.78385C8.09556 6.5815 6.62134 8.07682 4.78618 8.93697L3.62368 7.51666C3.46431 7.32213 3.19712 7.2565 2.96743 7.35494L0.342432 8.47994C0.0916504 8.5901 -0.0466309 8.86197 0.0143066 9.12682L0.576807 11.5643C0.6354 11.8174 0.8604 12.0003 1.12524 12.0003C7.12759 12.0003 12.0002 7.13697 12.0002 1.12525C12.0002 0.862753 11.8198 0.63541 11.5643 0.576816Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
<span>{t["Voice Call"] || "Voice Call"}</span> |
|||
</div> |
|||
)} |
|||
|
|||
{hasVideo && ( |
|||
<div className="inline-flex items-center gap-1 rounded-[16px] bg-[#0EB13C]/10 px-2 py-1 text-[10px] font-semibold text-[#0EB13C]"> |
|||
<svg |
|||
width="12" |
|||
height="12" |
|||
viewBox="0 0 16 16" |
|||
fill="none" |
|||
xmlns="http://www.w3.org/2000/svg" |
|||
className="size-3 shrink-0 text-[#0EB13C]" |
|||
> |
|||
<path |
|||
fillRule="evenodd" |
|||
clipRule="evenodd" |
|||
d="M3.33398 3.33334C2.80355 3.33334 2.29484 3.54406 1.91977 3.91913C1.5447 4.2942 1.33398 4.80291 1.33398 5.33334V10.6667C1.33398 11.1971 1.5447 11.7058 1.91977 12.0809C2.29484 12.456 2.80355 12.6667 3.33398 12.6667H9.33398C9.86442 12.6667 10.3731 12.456 10.7482 12.0809C11.1233 11.7058 11.334 11.1971 11.334 10.6667V9.60934L12.9607 11.236C13.59 11.866 14.6673 11.4193 14.6673 10.5287V5.47134C14.6673 4.58068 13.59 4.13401 12.9607 4.76468L11.334 6.39001V5.33334C11.334 4.80291 11.1233 4.2942 10.7482 3.91913C10.3731 3.54406 9.86442 3.33334 9.33398 3.33334H3.33398Z" |
|||
fill="currentColor" |
|||
/> |
|||
</svg> |
|||
<span>{t["Video Call"] || "Video Call"}</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
)} |
|||
|
|||
{/* Topics Badges */} |
|||
{topics.length > 0 && ( |
|||
<> |
|||
<div className="my-2.5 border-t border-[#EBEBEB]" /> |
|||
<div className="flex flex-wrap items-center gap-2 px-4 pb-1"> |
|||
{topics.map((topic) => ( |
|||
<span |
|||
key={topic} |
|||
className="inline-flex items-center rounded-[24px] bg-[#F4F4F8] px-2.5 py-1 font-sans text-[10px] font-semibold text-[#747474]" |
|||
> |
|||
{topic} |
|||
</span> |
|||
))} |
|||
</div> |
|||
</> |
|||
)} |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Floating Unread Badge at Top Corner */} |
|||
{unreadCount > 0 && ( |
|||
<div className="absolute -top-2.5 end-0 z-10 flex items-center gap-1 rounded-[16px] bg-[#ECA533] px-2 py-0.5 text-white shadow-sm"> |
|||
<span className="text-[10px] font-bold">{unreadCount}</span> |
|||
<span className="text-[8.5px] font-normal"> |
|||
{t["New Message"] || "New Message"} |
|||
</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function AdvisorsSkeleton() { |
|||
return ( |
|||
<> |
|||
{[0, 1, 2].map((index) => ( |
|||
<div |
|||
key={index} |
|||
className="w-full max-w-[343px] animate-pulse rounded-[15px] border border-[#EBEBEB] bg-white py-3 shadow-[0_4px_16px_rgba(0,0,0,0.04)]" |
|||
> |
|||
<div className="flex items-center gap-3 px-3"> |
|||
<div className="size-12 shrink-0 rounded-full bg-[#F0F0F0]" /> |
|||
<div className="min-w-0 flex-1 flex flex-col gap-1.5"> |
|||
<div className="flex items-center justify-between"> |
|||
<div className="h-4 w-32 rounded bg-[#F0F0F0]" /> |
|||
<div className="h-3 w-8 rounded bg-[#F0F0F0]" /> |
|||
</div> |
|||
<div className="h-3 w-44 rounded bg-[#F0F0F0]" /> |
|||
</div> |
|||
</div> |
|||
<div className="flex gap-2 pt-3 px-4"> |
|||
<div className="h-5 w-20 rounded-[16px] bg-[#F0F0F0]" /> |
|||
<div className="h-5 w-20 rounded-[16px] bg-[#F0F0F0]" /> |
|||
</div> |
|||
<div className="my-2.5 border-t border-[#F0F0F0]" /> |
|||
<div className="flex gap-2 px-4 pb-1"> |
|||
<div className="h-5 w-16 rounded-[24px] bg-[#F0F0F0]" /> |
|||
<div className="h-5 w-20 rounded-[24px] bg-[#F0F0F0]" /> |
|||
<div className="h-5 w-14 rounded-[24px] bg-[#F0F0F0]" /> |
|||
</div> |
|||
</div> |
|||
))} |
|||
</> |
|||
); |
|||
} |
|||
@ -0,0 +1,752 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import { useRouter } from "next/navigation"; |
|||
import { useEffect, useMemo, useState } from "react"; |
|||
import Button from "@/components/Componentes/button"; |
|||
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet"; |
|||
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet"; |
|||
import InformationSheet from "@/components/Componentes/information-sheet"; |
|||
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; |
|||
import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot"; |
|||
import NavigationButton from "@/components/Componentes/navigation-button"; |
|||
import { PageBackground } from "@/components/Componentes/page-background"; |
|||
import StickyHeader from "@/components/Componentes/sticky-header"; |
|||
import type { |
|||
MarriageCaseStatus, |
|||
MarriageField, |
|||
MarriageFieldValue, |
|||
MarriageGender, |
|||
MarriagePhoneFieldValue, |
|||
MarriageProfileResponse, |
|||
} from "@/hooks/marriage/types"; |
|||
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; |
|||
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; |
|||
import { getSubmitPath } from "@/lib/get-submit-path"; |
|||
import { markMatchStarted } from "@/lib/match-start-grace"; |
|||
import { localizePath } from "@/translations/config"; |
|||
import { useI18n } from "@/translations/provider"; |
|||
|
|||
import { |
|||
formatFieldLabel, |
|||
formatFieldValue, |
|||
formatOptionValue, |
|||
getOrderedSummaryFields, |
|||
isMarriagePhoneFieldValue, |
|||
titleFromKey, |
|||
} from "@/lib/marriage-field-formatter"; |
|||
|
|||
function isImageField(field: MarriageField) { |
|||
return /(avatar|image|photo|picture|portrait|upload)/i.test( |
|||
`${field.key} ${field.label}`, |
|||
); |
|||
} |
|||
|
|||
function isFirstNameField(field: MarriageField) { |
|||
const key = (field.key || "").toLowerCase(); |
|||
return ( |
|||
key === "first_name" || |
|||
key === "personal_identity.first_name" || |
|||
key.endsWith(".first_name") || |
|||
key.endsWith("_first_name") || |
|||
key === "full_name" || |
|||
key === "fullname" || |
|||
key === "q1_full_name" || |
|||
key.endsWith(".full_name") || |
|||
key.endsWith("_full_name") |
|||
); |
|||
} |
|||
|
|||
function canAcceptProfile( |
|||
gender: MarriageGender | null | undefined, |
|||
status: MarriageCaseStatus | null | undefined, |
|||
) { |
|||
if (!gender || !status) { |
|||
return false; |
|||
} |
|||
|
|||
if (gender === "female") { |
|||
return status === "introduced" || status === "male_accepted"; |
|||
} |
|||
|
|||
return status === "introduced"; |
|||
} |
|||
|
|||
function MatchField({ |
|||
field, |
|||
dictionary, |
|||
}: { |
|||
field: MarriageField; |
|||
dictionary?: Record<string, string>; |
|||
}) { |
|||
const value = formatOptionValue(field.value, dictionary); |
|||
|
|||
if (!value || isImageField(field)) { |
|||
return null; |
|||
} |
|||
|
|||
const label = formatFieldLabel(field, dictionary); |
|||
|
|||
return ( |
|||
<div className="flex flex-col items-start w-full border-b border-black/10 pb-[14px] mt-[14px]"> |
|||
<p className="text-[12px] font-semibold leading-[17px] text-[#978787]"> |
|||
{label} |
|||
</p> |
|||
<p className="text-[16px] font-bold leading-[20px] text-[#111111] mt-[7px] text-left"> |
|||
{value} |
|||
</p> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function MatchPublicProfileFields({ |
|||
publicInfo, |
|||
dictionary, |
|||
}: { |
|||
publicInfo: MarriageField[] | null | undefined; |
|||
dictionary?: Record<string, string>; |
|||
}) { |
|||
const visibleFields = useMemo(() => { |
|||
if (!publicInfo) return []; |
|||
return publicInfo.filter((field) => { |
|||
if ( |
|||
field.value === null || |
|||
field.value === "" || |
|||
isImageField(field) || |
|||
isFirstNameField(field) |
|||
) { |
|||
return false; |
|||
} |
|||
if ((field as any).private === true) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}); |
|||
}, [publicInfo]); |
|||
|
|||
if (!visibleFields.length) { |
|||
return ( |
|||
<div className="mt-6 rounded-[16px] bg-white/70 p-5 text-center shadow-xs"> |
|||
<p className="group-12 font-medium text-[#747474]"> |
|||
{dictionary?.["No public information is available to display."] || |
|||
"No public information is available to display."} |
|||
</p> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<div className="flex flex-col w-full mt-6" style={{ gap: "14px" }}> |
|||
{visibleFields.map((field) => ( |
|||
<MatchField |
|||
key={field.key} |
|||
field={field} |
|||
dictionary={dictionary} |
|||
/> |
|||
))} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function NewMatchProfileSkeleton({ |
|||
hideBackButton = false, |
|||
onClose, |
|||
}: { |
|||
hideBackButton?: boolean; |
|||
onClose?: () => void; |
|||
}) { |
|||
const { dictionary: t } = useI18n(); |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
<main className="-mx-[17px] flex h-dvh flex-col overflow-hidden bg-[#F5F5F5]"> |
|||
<StickyHeader sticky className="shrink-0"> |
|||
<div className="flex items-center justify-between gap-3"> |
|||
{!hideBackButton ? ( |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="back" |
|||
iconLabel={t["Go back"]} |
|||
onClick={onClose} |
|||
/> |
|||
) : ( |
|||
<div className="size-10 shrink-0" /> |
|||
)} |
|||
<h1 |
|||
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" |
|||
> |
|||
{t["More detail"]} |
|||
</h1> |
|||
<div className="size-10 shrink-0" /> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="min-h-0 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" /> |
|||
</div> |
|||
|
|||
<div className="mt-6 space-y-4 rounded-[18px] bg-white/80 p-5 shadow-xs"> |
|||
<LoadingSkeleton className="h-4 w-44 rounded-md" /> |
|||
|
|||
<div className="space-y-4 pt-1"> |
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-24" /> |
|||
<LoadingSkeleton className="h-4 w-48" /> |
|||
</div> |
|||
|
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-28" /> |
|||
<LoadingSkeleton className="h-4 w-36" /> |
|||
</div> |
|||
|
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-20" /> |
|||
<LoadingSkeleton className="h-4 w-56" /> |
|||
</div> |
|||
|
|||
<div className="space-y-1.5 border-b border-[#000000]/05 pb-3"> |
|||
<LoadingSkeleton className="h-3 w-32" /> |
|||
<LoadingSkeleton className="h-4 w-40" /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
|
|||
<div |
|||
style={{ paddingBottom: `calc(16px + var(--safe-bottom, 0px))` }} |
|||
className="shrink-0 z-30 w-full rounded-t-[24px] bg-white px-[17px] pt-4 shadow-[0_-4px_24px_rgba(0,0,0,0.08)]" |
|||
> |
|||
<div className="flex gap-3"> |
|||
<LoadingSkeleton className="w-1/3 h-[52px] rounded-[12px]" /> |
|||
<LoadingSkeleton className="w-2/3 h-[52px] rounded-[12px]" /> |
|||
</div> |
|||
</div> |
|||
</main> |
|||
</> |
|||
); |
|||
} |
|||
|
|||
function formatBoldText(text: string) { |
|||
if (!text) return ""; |
|||
const parts = text.split(/\*\*([^*]+)\*\*/g); |
|||
return parts.map((part, index) => { |
|||
if (index % 2 === 1) { |
|||
return ( |
|||
<strong key={index} className="font-bold text-[#111111]"> |
|||
{part} |
|||
</strong> |
|||
); |
|||
} |
|||
return part; |
|||
}); |
|||
} |
|||
|
|||
type NewMatchProfileClientProps = { |
|||
onClose?: () => void; |
|||
profile?: MarriageProfileResponse; |
|||
}; |
|||
|
|||
export default function NewMatchProfileClient({ |
|||
onClose, |
|||
profile: profileProp, |
|||
}: NewMatchProfileClientProps = {}) { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const router = useRouter(); |
|||
const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false); |
|||
const [isFemaleConsentChecked, setIsFemaleConsentChecked] = useState(false); |
|||
const [isDeclineSheetOpen, setIsDeclineSheetOpen] = useState(false); |
|||
const [isMaleDeclineWarningOpen, setIsMaleDeclineWarningOpen] = |
|||
useState(false); |
|||
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = |
|||
useState(false); |
|||
const { |
|||
data: queriedProfile, |
|||
isLoading: isQueryLoading, |
|||
refetch: refetchProfile, |
|||
} = useMarriageProfileQuery({ |
|||
enabled: !profileProp, |
|||
}); |
|||
|
|||
const profile = profileProp ?? queriedProfile; |
|||
const isLoading = !profileProp && isQueryLoading; |
|||
|
|||
useEffect(() => { |
|||
// If rendered as an overlay (onClose is passed), NEVER redirect away from current page
|
|||
if (!profile || Boolean(onClose)) { |
|||
return; |
|||
} |
|||
const targetPath = getSubmitPath(profile); |
|||
const caseStatus = profile?.active_case?.status; |
|||
const isProfileMatched = profile?.status === "matched"; |
|||
const isViewingAllowed = |
|||
caseStatus === "female_accepted" || |
|||
caseStatus === "payment_pending" || |
|||
caseStatus === "payment_done" || |
|||
caseStatus === "contacted" || |
|||
caseStatus === "finalized" || |
|||
isProfileMatched; |
|||
|
|||
if ( |
|||
targetPath !== "/new-match" && |
|||
targetPath !== "/request-sent" && |
|||
targetPath !== "/request-accepted" && |
|||
!isViewingAllowed |
|||
) { |
|||
router.replace(localizePath(targetPath, locale)); |
|||
} |
|||
}, [profile, locale, router, onClose]); |
|||
|
|||
const caseId = profile?.active_case?.case_id; |
|||
const caseStatus = profile?.active_case?.status; |
|||
const isFemaleProfile = profile?.gender === "female"; |
|||
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { |
|||
onSuccess: async (data, variables) => { |
|||
onClose?.(); |
|||
if (variables.action === "accept") { |
|||
const nextPath = |
|||
profile?.gender === "female" |
|||
? "/request-accepted" |
|||
: "/request-sent"; |
|||
router.replace(localizePath(nextPath, locale)); |
|||
return; |
|||
} |
|||
|
|||
router.replace(localizePath("/finding-match", locale)); |
|||
}, |
|||
}); |
|||
|
|||
const candidateName = useMemo(() => { |
|||
const publicInfo = profile?.match_summary?.public_info ?? []; |
|||
const firstNameIdx = publicInfo.findIndex( |
|||
(f) => |
|||
f.key === "personal_identity.first_name" || |
|||
f.key?.endsWith(".first_name") || |
|||
f.key === "first_name", |
|||
); |
|||
const lastNameIdx = publicInfo.findIndex( |
|||
(f) => |
|||
f.key === "personal_identity.last_name" || |
|||
f.key?.endsWith(".last_name") || |
|||
f.key === "last_name", |
|||
); |
|||
|
|||
if (firstNameIdx !== -1 && publicInfo[firstNameIdx].value) { |
|||
const firstName = formatFieldValue(publicInfo[firstNameIdx].value); |
|||
if (lastNameIdx !== -1 && publicInfo[lastNameIdx].value) { |
|||
const lastName = formatFieldValue(publicInfo[lastNameIdx].value); |
|||
return `${firstName} ${lastName}`.trim(); |
|||
} |
|||
return firstName || ""; |
|||
} |
|||
|
|||
const nameField = publicInfo.find( |
|||
(f) => |
|||
f.key === "q1_full_name" || |
|||
f.key.endsWith(".full_name") || |
|||
f.key.toLowerCase().includes("fullname") || |
|||
f.key.toLowerCase().includes("display_name"), |
|||
); |
|||
const formatted = formatFieldValue(nameField?.value ?? null); |
|||
if (formatted) return formatted; |
|||
|
|||
const firstVal = publicInfo.find((f) => Boolean(f.value))?.value ?? null; |
|||
return ( |
|||
formatFieldValue(firstVal) || |
|||
(profile?.match_summary?.id |
|||
? `Profile #${profile.match_summary.id}` |
|||
: "Profile") |
|||
); |
|||
}, [profile?.match_summary]); |
|||
|
|||
const isCandidateFemale = |
|||
profile?.match_summary?.gender === "female" || profile?.gender === "male"; |
|||
const avatarSrc = isCandidateFemale |
|||
? "/assets/images/female_avatar.svg" |
|||
: "/assets/images/Group 1597880481.png"; |
|||
|
|||
const isRedirecting = useMemo(() => { |
|||
if (!profile) return false; |
|||
const targetPath = getSubmitPath(profile); |
|||
const caseStatus = profile?.active_case?.status; |
|||
const isProfileMatched = profile?.status === "matched"; |
|||
const isViewingAllowed = |
|||
caseStatus === "payment_done" || |
|||
caseStatus === "contacted" || |
|||
caseStatus === "finalized" || |
|||
isProfileMatched; |
|||
|
|||
return ( |
|||
targetPath !== "/new-match" && |
|||
targetPath !== "/request-sent" && |
|||
!isViewingAllowed |
|||
); |
|||
}, [profile]); |
|||
|
|||
const isSubmitting = respondMutation.isPending; |
|||
|
|||
if ((isLoading && !profile) || !profile) { |
|||
return ( |
|||
<NewMatchProfileSkeleton |
|||
hideBackButton={isSubmitting} |
|||
onClose={onClose} |
|||
/> |
|||
); |
|||
} |
|||
const isAcceptProfileEnabled = |
|||
Boolean(caseId) && |
|||
!isSubmitting && |
|||
canAcceptProfile(profile?.gender, caseStatus); |
|||
const isDeclineProfileEnabled = isAcceptProfileEnabled; |
|||
|
|||
const nameParts = candidateName.trim().split(/\s+/); |
|||
const _firstName = nameParts[0] || ""; |
|||
const _lastName = nameParts.slice(1).join(" ") || ""; |
|||
|
|||
const mainClass = "-mx-[17px] flex h-dvh flex-col overflow-hidden"; |
|||
|
|||
const mainStyle = { |
|||
backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, |
|||
backgroundColor: "#F5F5F5", |
|||
backgroundRepeat: "repeat-y", |
|||
backgroundSize: "100% auto", |
|||
}; |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
{isRequestSheetOpen ? ( |
|||
isFemaleProfile ? ( |
|||
<FemaleConsentSheet |
|||
title={t["Final Consent"]} |
|||
description={formatBoldText( |
|||
t[ |
|||
"Approving this profile will **display your contact number** to the gentleman. Ensure **family consensus** before proceeding." |
|||
], |
|||
)} |
|||
buttons={({ close }) => ( |
|||
<div className="space-y-5"> |
|||
<button |
|||
type="button" |
|||
onClick={() => setIsFemaleConsentChecked((value) => !value)} |
|||
className="flex w-full items-start gap-4 rounded-[15px] border-2 border-white bg-white/50 px-5 py-5 text-left shadow-[0_8px_24px_rgba(0,0,0,0.05)]" |
|||
> |
|||
<span |
|||
className={[ |
|||
"mt-1 flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full border-1", |
|||
isFemaleConsentChecked |
|||
? "border-[#F0445B] bg-[#F0445B]" |
|||
: "border-[#2B2B2B] bg-white", |
|||
].join(" ")} |
|||
aria-hidden="true" |
|||
> |
|||
{isFemaleConsentChecked ? ( |
|||
<span className="h-3 w-3 rounded-full bg-[#F0445B]" /> |
|||
) : null} |
|||
</span> |
|||
<span className="text-xs leading-[1.35] text-[#3F3F3F]"> |
|||
{formatBoldText( |
|||
t[ |
|||
"I confirm the family has **reviewed this profile** and **consents to communicate**." |
|||
], |
|||
)} |
|||
</span> |
|||
</button> |
|||
|
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button |
|||
variant="outlined" |
|||
className="py-[18px] text-[18px]" |
|||
onClick={close} |
|||
> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
className="py-[18px] text-[18px]" |
|||
disabled={ |
|||
!caseId || |
|||
isSubmitting || |
|||
!isAcceptProfileEnabled || |
|||
!isFemaleConsentChecked |
|||
} |
|||
onClick={async () => { |
|||
close(); |
|||
if (!isAcceptProfileEnabled) { |
|||
return; |
|||
} |
|||
|
|||
markMatchStarted(); |
|||
await respondMutation.mutateAsync({ action: "accept" }); |
|||
}} |
|||
> |
|||
{t.Confirm} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
)} |
|||
onClose={() => { |
|||
setIsFemaleConsentChecked(false); |
|||
setIsRequestSheetOpen(false); |
|||
}} |
|||
/> |
|||
) : ( |
|||
<InformationSheet |
|||
icon="check" |
|||
title={t["Request to Proceed"]} |
|||
description={ |
|||
t[ |
|||
"Upon your approval, your request will be sent to the lady, and we must await her response. This process may take between 2 to 4 days. Thank you for your patience." |
|||
] |
|||
} |
|||
buttons={({ close }) => ( |
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button |
|||
variant="outlined" |
|||
className="py-[18px]" |
|||
onClick={close} |
|||
> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
className="py-[18px]" |
|||
disabled={ |
|||
!isAcceptProfileEnabled || respondMutation.isPending |
|||
} |
|||
isLoading={respondMutation.isPending} |
|||
onClick={async () => { |
|||
close(); |
|||
if (!isAcceptProfileEnabled) { |
|||
return; |
|||
} |
|||
|
|||
markMatchStarted(); |
|||
await respondMutation.mutateAsync({ action: "accept" }); |
|||
}} |
|||
> |
|||
{t.Confirm} |
|||
</Button> |
|||
</div> |
|||
)} |
|||
onClose={() => setIsRequestSheetOpen(false)} |
|||
/> |
|||
) |
|||
) : null} |
|||
{isDeclineSheetOpen ? ( |
|||
<InformationSheet |
|||
icon="warning" |
|||
title={t["Decline Profile"] || t["Reject Profile"]} |
|||
description={ |
|||
t[ |
|||
"Are you sure you've fully reviewed the profile and want to decline this profile?" |
|||
] || |
|||
t[ |
|||
"Are you sure you've fully reviewed the profile and want to reject this profile?" |
|||
] |
|||
} |
|||
buttons={({ close }) => ( |
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button variant="outlined" className="py-[18px]" onClick={close}> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
onClick={() => { |
|||
close(); |
|||
setIsDismissReasonSheetOpen(true); |
|||
}} |
|||
> |
|||
{t.Decline || t["Decline"] || t.Reject} |
|||
</Button> |
|||
</div> |
|||
)} |
|||
onClose={() => setIsDeclineSheetOpen(false)} |
|||
/> |
|||
) : null} |
|||
{isMaleDeclineWarningOpen ? ( |
|||
<InformationSheet |
|||
icon="warning" |
|||
title={t["Decline Warning"] || t["Rejection Warning"]} |
|||
description={ |
|||
t[ |
|||
"Please review the person’s full profile once more before making your final decision." |
|||
] |
|||
} |
|||
buttons={({ close }) => ( |
|||
<div className="grid w-full grid-cols-2 gap-3"> |
|||
<Button variant="outlined" className="py-[18px]" onClick={close}> |
|||
{t.Cancel} |
|||
</Button> |
|||
<Button |
|||
className="py-[18px]" |
|||
onClick={() => { |
|||
close(); |
|||
setIsDismissReasonSheetOpen(true); |
|||
}} |
|||
> |
|||
{t.Decline || t["Decline"] || t.Reject} |
|||
</Button> |
|||
</div> |
|||
)} |
|||
onClose={() => setIsMaleDeclineWarningOpen(false)} |
|||
/> |
|||
) : null} |
|||
{isDismissReasonSheetOpen ? ( |
|||
<DismissReasonSheet |
|||
onClose={() => setIsDismissReasonSheetOpen(false)} |
|||
onSubmit={async (reason) => { |
|||
if (!caseId) { |
|||
return; |
|||
} |
|||
|
|||
await respondMutation.mutateAsync({ |
|||
action: "reject", |
|||
custom_note: reason, |
|||
}); |
|||
}} |
|||
/> |
|||
) : null} |
|||
|
|||
<main className={mainClass} style={mainStyle}> |
|||
<StickyHeader sticky className="shrink-0"> |
|||
<div className="flex items-center justify-between gap-3"> |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="back" |
|||
iconLabel={t["Go back"]} |
|||
onClick={onClose} |
|||
/> |
|||
<h1 |
|||
className="min-w-0 flex-1 text-center font-semibold text-[14px] leading-[16px] text-white" |
|||
> |
|||
{t["More detail"]} |
|||
</h1> |
|||
<div className="size-10 shrink-0" /> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="min-h-0 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={{ |
|||
width: "343px", |
|||
height: isCandidateFemale ? "124px" : "125px", |
|||
gap: "10px", |
|||
}} |
|||
> |
|||
{/* Frame 2095586585 - Avatar Section */} |
|||
<div |
|||
className="flex flex-col items-center justify-center w-full" |
|||
style={{ |
|||
height: "125px", |
|||
gap: "8px", |
|||
}} |
|||
> |
|||
{/* Avatar Group */} |
|||
<div className="relative overflow-hidden rounded-full w-[90px] h-[90px]"> |
|||
<Image |
|||
src={avatarSrc} |
|||
alt="" |
|||
width={90} |
|||
height={90} |
|||
className="w-full h-full object-cover" |
|||
/> |
|||
</div> |
|||
|
|||
{/* Frame 2095586663 - Names Section */} |
|||
<div |
|||
className="flex flex-row items-center justify-center w-full animate-fade-in" |
|||
style={{ |
|||
height: "26px", |
|||
}} |
|||
> |
|||
<span |
|||
className="font-bold text-[22px] leading-[26px] text-[#F0445B] text-center" |
|||
style={{ |
|||
WebkitTextStroke: "2px rgba(255, 255, 255, 0.5)", |
|||
paintOrder: "stroke fill", |
|||
}} |
|||
> |
|||
{candidateName} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<MatchPublicProfileFields |
|||
publicInfo={profile?.match_summary?.public_info} |
|||
dictionary={t} |
|||
/> |
|||
</section> |
|||
|
|||
<div |
|||
style={{ paddingBottom: `calc(16px + var(--safe-bottom, 0px))` }} |
|||
className="shrink-0 z-30 w-full rounded-t-[24px] bg-white px-[17px] pt-4 shadow-[0_-4px_24px_rgba(0,0,0,0.08)]" |
|||
> |
|||
{(Boolean(onClose) && !isAcceptProfileEnabled) || |
|||
caseStatus === "female_accepted" || |
|||
caseStatus === "payment_pending" || |
|||
caseStatus === "payment_done" || |
|||
caseStatus === "contacted" || |
|||
caseStatus === "finalized" || |
|||
profile?.status === "matched" ? ( |
|||
<button |
|||
type="button" |
|||
onClick={() => (onClose ? onClose() : router.back())} |
|||
className="w-full h-[52px] flex items-center justify-center rounded-[12px] bg-[#F5F5F5] text-[#36363C] font-semibold text-[16px] transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#EBEBEB]" |
|||
> |
|||
{t.Back} |
|||
</button> |
|||
) : ( |
|||
<div className="flex gap-3"> |
|||
<button |
|||
type="button" |
|||
disabled={!isDeclineProfileEnabled || isSubmitting} |
|||
onClick={() => { |
|||
if (isFemaleProfile) { |
|||
setIsDeclineSheetOpen(true); |
|||
} else { |
|||
setIsMaleDeclineWarningOpen(true); |
|||
} |
|||
}} |
|||
className="inline-flex w-1/3 h-[52px] items-center justify-center rounded-[12px] border border-[#BFBFBF] bg-white px-4 text-[16px] font-semibold text-[#9A9A9A] disabled:cursor-not-allowed disabled:opacity-50" |
|||
> |
|||
{t.Decline || t["Decline"] || t.Reject} |
|||
</button> |
|||
<button |
|||
type="button" |
|||
disabled={!isAcceptProfileEnabled || isSubmitting} |
|||
onClick={() => { |
|||
if (!isAcceptProfileEnabled) { |
|||
return; |
|||
} |
|||
|
|||
if (isFemaleProfile) { |
|||
setIsFemaleConsentChecked(false); |
|||
} |
|||
|
|||
setIsRequestSheetOpen(true); |
|||
}} |
|||
className="inline-flex w-2/3 h-[52px] whitespace-nowrap items-center justify-center gap-1 rounded-[12px] bg-[#F0445B] px-4 text-[16px] font-semibold text-white shadow-[0_8px_16px_rgba(240,68,91,0.24)] disabled:cursor-not-allowed disabled:opacity-50" |
|||
> |
|||
{isSubmitting ? ( |
|||
<LoadingThreeDot /> |
|||
) : ( |
|||
<> |
|||
<Image |
|||
src="/assets/images/Icfdason.svg" |
|||
alt="" |
|||
width={28} |
|||
height={28} |
|||
/> |
|||
<span>{t["Accept Profile"]}</span> |
|||
</> |
|||
)} |
|||
</button> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</main> |
|||
</> |
|||
); |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue