Browse Source

feat: add subscription management, navigation buttons, and supporting UI components with i18n support

Dev
parent
commit
7af36bef4c
  1. 77
      public/assets/images/diamond-color.svg
  2. 89
      src/app/new-match/new-match-client.tsx
  3. 47
      src/app/request-accepted/request-accepted-client.tsx
  4. 31
      src/app/request-sent/request-sent-client.tsx
  5. 30
      src/components/Componentes/advisor-actions-card.tsx
  6. 111
      src/components/Componentes/information-sheet.tsx
  7. 6
      src/components/Componentes/navigation-button.tsx
  8. 18
      src/components/Componentes/page-header.tsx
  9. 19
      src/components/Componentes/swipe-button.tsx
  10. 11
      src/translations/locales/en.json
  11. 8
      src/translations/locales/fa.json

77
public/assets/images/diamond-color.svg

@ -0,0 +1,77 @@
<svg width="64" height="64" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Facet Gradients matching the exact vibrant spectrum -->
<linearGradient id="g_top_left" x1="6" y1="42" x2="22" y2="18" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#00C2FF"/>
<stop offset="100%" stop-color="#6366F1"/>
</linearGradient>
<linearGradient id="g_top_mid_left" x1="22" y1="18" x2="32" y2="42" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#8B5CF6"/>
<stop offset="100%" stop-color="#D946EF"/>
</linearGradient>
<linearGradient id="g_top_center" x1="32" y1="42" x2="68" y2="18" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#C084FC"/>
<stop offset="50%" stop-color="#F472B6"/>
<stop offset="100%" stop-color="#FDA4AF"/>
</linearGradient>
<linearGradient id="g_top_mid_right" x1="50" y1="18" x2="78" y2="42" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#F43F5E"/>
<stop offset="100%" stop-color="#FB7185"/>
</linearGradient>
<linearGradient id="g_top_right" x1="68" y1="42" x2="94" y2="42" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#FB7185"/>
<stop offset="100%" stop-color="#FB923C"/>
</linearGradient>
<linearGradient id="g_bot_left" x1="6" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#00BAFF"/>
<stop offset="60%" stop-color="#3B82F6"/>
<stop offset="100%" stop-color="#6366F1"/>
</linearGradient>
<linearGradient id="g_bot_mid_left" x1="32" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#6366F1"/>
<stop offset="50%" stop-color="#8B5CF6"/>
<stop offset="100%" stop-color="#A855F7"/>
</linearGradient>
<linearGradient id="g_bot_mid_right" x1="50" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#EC4899"/>
<stop offset="60%" stop-color="#D946EF"/>
<stop offset="100%" stop-color="#A855F7"/>
</linearGradient>
<linearGradient id="g_bot_right" x1="94" y1="42" x2="50" y2="88" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#F43F5E"/>
<stop offset="50%" stop-color="#FB7185"/>
<stop offset="100%" stop-color="#FB923C"/>
</linearGradient>
<filter id="subtle_glow" x="0" y="0" width="100" height="100" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
</filter>
</defs>
<g stroke="white" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round">
<!-- Top Row Facets -->
<polygon points="22,18 6,42 32,42" fill="url(#g_top_left)" />
<polygon points="22,18 50,18 32,42" fill="url(#g_top_mid_left)" />
<polygon points="32,42 50,18 68,42" fill="url(#g_top_center)" />
<polygon points="50,18 78,18 68,42" fill="url(#g_top_mid_right)" />
<polygon points="78,18 68,42 94,42" fill="url(#g_top_right)" />
<!-- Bottom Row Facets -->
<polygon points="6,42 32,42 50,88" fill="url(#g_bot_left)" />
<polygon points="32,42 50,42 50,88" fill="url(#g_bot_mid_left)" />
<polygon points="50,42 68,42 50,88" fill="url(#g_bot_mid_right)" />
<polygon points="68,42 94,42 50,88" fill="url(#g_bot_right)" />
</g>
<!-- Outer highlight border for extra crispness -->
<polygon points="22,18 78,18 94,42 50,88 6,42" fill="none" stroke="white" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round" />
</svg>

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

@ -26,6 +26,7 @@ import MatchProfileOverlay, {
import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background";
import InformationSheet from "@/components/Componentes/information-sheet";
import SwipeButton from "@/components/Componentes/swipe-button";
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot";
import { DiscountWidget } from "@/components/Componentes/discount-widget";
@ -235,29 +236,28 @@ function useMatchSummaryDisplay(
const fields = matchSummary?.public_info ?? [];
const usedIndexes = new Set<number>();
// 1. Name: Combine first_name and last_name if available, or find general name
// 1. Name: Show only first_name (do not show last_name)
let displayName: string | null = null;
const firstNameIdx = fields.findIndex(
(f) =>
f.key === "personal_identity.first_name" ||
f.key?.endsWith(".first_name"),
f.key?.endsWith(".first_name") ||
f.key === "first_name",
);
const lastNameIdx = fields.findIndex(
(f) =>
f.key === "personal_identity.last_name" ||
f.key?.endsWith(".last_name"),
f.key?.endsWith(".last_name") ||
f.key === "last_name",
);
if (firstNameIdx !== -1 && fields[firstNameIdx].value) {
usedIndexes.add(firstNameIdx);
const firstName = formatFieldValue(fields[firstNameIdx].value);
if (lastNameIdx !== -1 && fields[lastNameIdx].value) {
if (lastNameIdx !== -1) {
usedIndexes.add(lastNameIdx);
const lastName = formatFieldValue(fields[lastNameIdx].value);
displayName = `${firstName} ${lastName}`.trim();
} else {
displayName = firstName;
}
if (firstNameIdx !== -1 && fields[firstNameIdx].value) {
usedIndexes.add(firstNameIdx);
displayName = formatFieldValue(fields[firstNameIdx].value);
} else {
const nameField = pickField(
fields,
@ -266,7 +266,8 @@ function useMatchSummaryDisplay(
t,
);
if (nameField) {
displayName = nameField.value;
const rawName = String(nameField.value).trim();
displayName = rawName.split(/\s+/)[0] || rawName;
}
}
@ -454,26 +455,31 @@ function ProfileInfoItem({
{icon}
</div>
<div className="flex min-w-0 flex-col text-start">
<div className="flex min-w-0 flex-1 flex-col text-start">
<span
className="
text-[12px]
leading-tight
font-bold
text-[#1E293B]
truncate
"
>
{field.label}
</span>
<span
title={typeof field.value === "string" ? field.value : undefined}
className="
mt-0.5
break-words
text-[12px]
leading-[1.35]
font-semibold
text-[#F0445B]
line-clamp-2
overflow-hidden
text-ellipsis
break-words
"
>
{field.value}
@ -492,6 +498,7 @@ export default function NewMatchClient() {
const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay();
const { data: profile, isError, isLoading } = useMarriageProfileQuery();
const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false);
const [isDeclineConfirmOpen, setIsDeclineConfirmOpen] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(null);
const [isInsufficientCoins, setIsInsufficientCoins] = useState(false);
@ -559,6 +566,16 @@ export default function NewMatchClient() {
}
};
const openDeclineConfirm = () => {
setIsPaymentSheetOpen(false);
setIsDeclineConfirmOpen(true);
};
const cancelDeclineConfirm = () => {
setIsDeclineConfirmOpen(false);
setIsPaymentSheetOpen(true);
};
useEffect(() => {
console.log("🔍 [NewMatchClient] Current React Query Profile State:", {
isLoading,
@ -933,7 +950,7 @@ export default function NewMatchClient() {
{profile?.can_edit_profile === false && (
<div
style={{ paddingBottom: `${16 + bottom}px` }}
className="fixed inset-x-0 bottom-0 z-20 mx-auto w-full sm:max-w-[375px] bg-background/95 px-[17px] pt-3 pb-[16px] backdrop-blur-md"
className="fixed inset-x-0 bottom-0 z-20 mx-auto w-full sm:max-w-[375px] bg-[#F9F8F8] px-[17px] pt-3 pb-[16px]"
>
<div
className="inline-flex w-full items-center justify-center gap-2 rounded-[9px] border-none bg-[#D1D1D6] px-4 h-[52px] text-center text-[#747474] shadow-none"
@ -1042,7 +1059,7 @@ export default function NewMatchClient() {
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}
onClick={openDeclineConfirm}
>
<div className="inline-flex w-full items-center justify-center rounded-[11px] border border-[#747474] bg-transparent px-2 h-[52px] text-[16px] font-semibold text-[#747474] transition-opacity active:opacity-90 min-w-0">
{respondMutation.isPending ? (
@ -1106,6 +1123,46 @@ export default function NewMatchClient() {
/>
)}
{isDeclineConfirmOpen && (
<InformationSheet
icon="warning"
title={
(t as any)["Are you sure?"] ||
(locale === "fa" ? "آیا مطمئن هستید؟" : "Are you sure?")
}
description={
<div className="flex flex-col gap-2 text-center mt-1 group-12 text-[#4D4D4D] leading-relaxed">
<p className="font-medium text-[14px]">
{t[
"Are you sure you've fully reviewed the profile and want to reject this profile?"
] ||
(locale === "fa"
? "آیا از رد این پیشنهاد مطمئن هستید؟ در صورت رد، این مورد دیگر در دسترس نخواهد بود."
: "Are you sure you want to decline this proposal? Once declined, this match will no longer be available.")}
</p>
</div>
}
buttons={({ close }) => (
<SwipeButton
disabled={respondMutation.isPending}
text={t["Decline"] || (locale === "fa" ? "رد پیشنهاد" : "Decline")}
cancelText={t["Cancel"] || (locale === "fa" ? "انصراف" : "Cancel")}
onCancel={() => {
close();
cancelDeclineConfirm();
}}
onSuccess={async () => {
await handleDecline();
close();
setIsDeclineConfirmOpen(false);
setIsPaymentSheetOpen(false);
}}
/>
)}
onClose={cancelDeclineConfirm}
/>
)}
<MarriageAdvisorsOverlay open={isAdvisorOpen} onClose={closeAdvisors} />
<MatchProfileOverlay open={isProfileOpen} onClose={closeProfile} />

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

@ -266,7 +266,7 @@ export default function RequestAcceptedClient() {
const secondaryActionText = isFemaleProfile
? t["Contact Received"]
: caseStatus === "payment_done" || caseStatus === "contacted"
? t["View contact number"]
? t["Contact"]
: t["Pay and get contact"];
const contactInfoPhoneItems = getContactInfoPhoneItems(
contactInfoQuery.data?.contact_info,
@ -549,29 +549,34 @@ export default function RequestAcceptedClient() {
</div>
) : (
<>
<div className="relative isolate flex items-center justify-center">
{/* Illustration */}
<div className="relative mt-6 flex items-center justify-center">
{/* soft glow */}
<div className="absolute h-[115px] w-[115px] rounded-full bg-[#FF5C7D]/10 blur-xl" />
<div className="relative z-10">
<Image
src="/assets/images/Group 15978804fdasf68.svg"
alt={t["Request accepted"]}
width={131}
height={125}
priority
className="relative z-10"
/>
</div>
</div>
<h1 className="mt-11 group-16 leading-none font-black tracking-[0.02em] text-[#171717] uppercase">
<h1 className="mt-8 text-[20px] leading-none font-black tracking-[0.03em] text-[#171717] uppercase">
{titleText}
</h1>
{caseStatus === "contacted" ||
isFemaleContactConfirmed ||
(isFemaleProfile && contactStatusMutation.isPending) ? (
<div className="w-full border border-[#ECECEC] bg-white rounded-[15px] mt-6 px-4 py-4 text-center shadow-sm max-w-[315px] flex flex-col items-center justify-center min-h-[100px]">
<div className="w-full border border-[#E2E8F0] bg-white/90 backdrop-blur-sm rounded-[20px] mt-6 px-5 py-4 text-center shadow-sm max-w-[340px] flex flex-col items-center justify-center min-h-[90px]">
{isFemaleProfile && contactStatusMutation.isPending ? (
<LoadingThreeDot className="text-[#E03950]" />
) : (
<p className="text-[#555555] group-14 font-medium leading-relaxed">
<p className="text-[#475569] text-[14px] font-medium leading-relaxed">
{isFemaleProfile
? t[
"Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized."
@ -583,7 +588,7 @@ export default function RequestAcceptedClient() {
)}
</div>
) : (
<p className="mt-4 max-w-[315px] group-12 leading-[1.45] font-semibold text-[#777777]">
<p className="mt-5 max-w-[340px] text-[15px] leading-[1.6] font-medium text-[#777777]">
{noContactReportedSuccess
? t[
"Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you."
@ -602,14 +607,14 @@ export default function RequestAcceptedClient() {
{caseStatus === "contacted" ||
isFemaleContactConfirmed ||
(isFemaleProfile && contactStatusMutation.isPending) ? (
<div className="flex mt-8 w-full gap-3 justify-center">
<div className="flex mt-8 w-full gap-3 justify-center max-w-[350px] mx-auto">
{isFemaleProfile &&
contactStatusMutation.isPending ? null : isFemaleProfile ? (
<button
type="button"
onClick={() => setIsOutcomeSheetOpen(true)}
disabled={outcomeMutation.isPending}
className="w-full max-w-[315px] h-[52px] rounded-[15px] bg-gradient-to-r from-[#FE6F82] to-[#E03950] text-white font-semibold group-14 flex items-center justify-center shadow-lg shadow-[#FE6F82]/30 transition-transform active:scale-[0.98] cursor-pointer hover:opacity-95 disabled:opacity-50"
className="w-full h-[50px] px-6 rounded-full bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[15px] flex items-center justify-center shadow-sm transition-transform active:scale-[0.98] cursor-pointer hover:opacity-95 disabled:opacity-50"
>
{outcomeMutation.isPending ? (
<LoadingThreeDot className="text-white" />
@ -622,7 +627,7 @@ export default function RequestAcceptedClient() {
<button
type="button"
onClick={() => openProfile()}
className="flex-1 h-[52px] rounded-[15px] border border-[#BFBFBF] bg-white text-[#36363C] font-semibold group-14 flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F5F5F5]"
className="flex-1 h-[50px] px-3 rounded-full border border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#334155] font-bold text-[14px] shadow-sm flex items-center justify-center transition-transform active:scale-[0.98] cursor-pointer hover:bg-[#F8FAFC]"
>
{t["View Profile"]}
</button>
@ -631,7 +636,7 @@ export default function RequestAcceptedClient() {
type="button"
onClick={() => setIsOutcomeSheetOpen(true)}
disabled={outcomeMutation.isPending}
className="flex-1 h-[52px] rounded-[15px] bg-gradient-to-r from-[#FE6F82] to-[#E03950] text-white font-semibold group-14 flex items-center justify-center shadow-lg shadow-[#FE6F82]/30 transition-transform active:scale-[0.98] cursor-pointer hover:opacity-95 disabled:opacity-50"
className="flex-1 h-[50px] px-3 rounded-full bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[14px] flex items-center justify-center shadow-sm transition-transform active:scale-[0.98] cursor-pointer hover:opacity-95 disabled:opacity-50"
>
{outcomeMutation.isPending ? (
<LoadingThreeDot className="text-white" />
@ -643,7 +648,7 @@ export default function RequestAcceptedClient() {
)}
</div>
) : (
<div className="flex mt-9 w-full justify-center gap-4 max-w-[315px] mx-auto">
<div className="flex mt-8 w-full justify-center gap-3.5 max-w-[360px] mx-auto">
{isFemaleProfile ? (
<button
type="button"
@ -652,7 +657,7 @@ export default function RequestAcceptedClient() {
contactStatusMutation.isPending ||
noContactReportedSuccess
}
className="flex-1 h-[44px] rounded-[15px] border border-[#BFBFBF] bg-white text-[#36363C] font-semibold group-14 flex items-center justify-center transition-all cursor-pointer hover:bg-[#F5F5F5] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white"
className="flex-1 min-h-[48px] px-3.5 py-2.5 rounded-full border border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#475569] font-bold text-[13.5px] leading-tight shadow-sm flex items-center justify-center text-center transition-all cursor-pointer hover:bg-[#F8FAFC] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
>
{contactStatusMutation.isPending ? (
<LoadingThreeDot />
@ -663,9 +668,9 @@ export default function RequestAcceptedClient() {
) : (
<Link
href={profileHref}
className="max-w-[212px] flex-1"
className="flex-1 max-w-[170px]"
>
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C] min-h-[38px] flex items-center justify-center">
<div className="w-full min-h-[48px] px-3.5 py-2.5 rounded-full border border-[#E2E8F0] bg-white/95 backdrop-blur-sm text-[#475569] font-bold text-[14px] leading-tight shadow-sm flex items-center justify-center text-center whitespace-nowrap transition-all hover:bg-[#F8FAFC] active:scale-[0.98]">
{primaryActionText}
</div>
</Link>
@ -679,8 +684,8 @@ export default function RequestAcceptedClient() {
disabled={paymentMutation.isPending}
className={
isFemaleProfile
? "flex-1 h-[44px] rounded-[15px] bg-gradient-to-r from-[#FE6F82] to-[#E03950] text-white font-semibold group-14 flex items-center justify-center shadow-md shadow-[#FE6F82]/30 transition-all cursor-pointer hover:opacity-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none"
: "max-w-[212px] flex-1 appearance-none border-0 bg-transparent p-0 text-left"
? "flex-1 min-h-[48px] px-3.5 py-2.5 rounded-full bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[13.5px] leading-tight flex items-center justify-center text-center whitespace-nowrap shadow-sm transition-all cursor-pointer hover:opacity-95 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
: "flex-1 max-w-[170px] min-h-[48px] appearance-none border-0 bg-transparent p-0 text-center cursor-pointer transition-transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
}
>
{isFemaleProfile ? (
@ -690,9 +695,9 @@ export default function RequestAcceptedClient() {
secondaryActionText
)
) : (
<div className="bg-linear-180 from-[#FE6F82] to-[#E03950] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#fff] shadow-md shadow-[#F2596E]/60 flex items-center justify-center min-h-[38px]">
<div className="w-full h-full min-h-[48px] px-3.5 py-2.5 rounded-full bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[14px] leading-tight flex items-center justify-center text-center whitespace-nowrap shadow-sm hover:opacity-95">
{paymentMutation.isPending ? (
<LoadingThreeDot />
<LoadingThreeDot className="text-white" />
) : (
secondaryActionText
)}
@ -706,8 +711,8 @@ export default function RequestAcceptedClient() {
!isFemaleContactConfirmed &&
!noContactReportedSuccess &&
!(isFemaleProfile && contactStatusMutation.isPending) ? (
<div className="border border-[#F0445B] bg-[#F0445B]/10 rounded-xl mt-4">
<p className="text-[#F0445B] group-12 font-semibold py-2.5 px-3.5 whitespace-pre-line">
<div className="border border-[#FDA4AF]/60 bg-[#FFF1F2]/80 backdrop-blur-sm rounded-[16px] mt-6 p-4 max-w-[360px] shadow-sm">
<p className="text-[#BE123C] text-[12px] font-medium leading-[1.65] whitespace-pre-line text-justify">
{
t[
"Please be informed that from the time of this introduction, you have 48 hours (2 days) to contact the person or their respected family to declare your readiness and begin the acquaintance process. At this stage, merely an initial call to announce your presence is sufficient, and planning for further steps (such as an in-person meeting) depends entirely on your subsequent mutual agreements. Since failing to make contact within the specified time might be considered socially disrespectful, if no action is taken within these 2 days, the introduced match will be removed according to the platform's rules. We also remind you that this issue may lead to restrictions such as delays in future introductions and financial penalties."

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

@ -67,10 +67,15 @@ export default function RequestSentClient() {
],
getAdvisor: t["Get Advisor"],
};
const isFemaleProfile = profile?.gender === "female";
const requestSentCopy = {
title: t["Request Sent"],
description:
t[
description: isFemaleProfile
? t[
"Your request has been sent. Once the gentleman reviews your request, you will be notified."
] ||
"درخواست شما ارسال شد. پس از بررسی درخواست شما توسط آقا، به شما اطلاع‌رسانی خواهد شد."
: t[
"Your request has been sent. Once the lady reviews your request, you will be notified."
],
matchProfile: t["View More Details"],
@ -87,33 +92,41 @@ export default function RequestSentClient() {
>
<PageHeader className="-mx-[6px]" profile={profile} />
<div className="flex flex-1 flex-col justify-between gap-20 pt-[109px]">
<div className="flex flex-1 flex-col justify-between gap-16 pt-8">
<section className="flex flex-col items-center">
<div className="relative isolate flex items-center justify-center">
{/* Illustration */}
<div className="relative mt-6 flex items-center justify-center">
{/* soft glow */}
<div className="absolute h-[115px] w-[115px] rounded-full bg-[#FF5C7D]/10 blur-xl" />
<div className="relative z-10">
<Image
src="/assets/images/Group 15978804fdasf68.svg"
alt="Request sent"
width={131}
height={125}
priority
className="relative z-10"
/>
</div>
</div>
<h1 className="mt-11 group-16 leading-none font-black tracking-[0.02em] text-[#171717] uppercase">
{/* Title */}
<h1 className="mt-8 text-[20px] leading-none font-black tracking-[0.03em] text-[#171717] uppercase">
{requestSentCopy.title}
</h1>
<p className="mt-4 max-w-[315px] group-12 leading-[1.45] font-semibold text-[#777777]">
{/* Description */}
<p className="mt-5 max-w-[330px] text-[15px] leading-[1.6] font-medium text-[#777777]">
{requestSentCopy.description}
</p>
{/* Button */}
<button
type="button"
onClick={openProfile}
className="mt-9 w-full max-w-[212px] cursor-pointer"
className="mt-12 w-full max-w-[300px] cursor-pointer transition-transform active:scale-[0.97]"
>
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C] transition-transform active:scale-[0.98]">
<div className="rounded-full bg-gradient-to-r from-[#FF6687] to-[#FF456C] px-6 py-4 text-[16px] font-bold text-white shadow-sm hover:opacity-95">
{requestSentCopy.matchProfile}
</div>
</button>

30
src/components/Componentes/advisor-actions-card.tsx

@ -1,7 +1,9 @@
"use client";
import Link from "next/link";
import { useMarriageAdvisorsQuery } from "@/hooks/marriage/use-marriage-advisors";
import Button from "./button";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
import NetworkImage from "./network-image";
const FALLBACK_AVATAR = "/assets/images/Avatar Image.png";
@ -33,13 +35,14 @@ export function AdvisorActionsCard({
onGetAdvisor,
className,
}: AdvisorActionsCardProps) {
const { locale } = useI18n();
const { data, isLoading } = useMarriageAdvisorsQuery();
const advisors = data?.results ?? [];
// Derive real avatars from API response (pick first 3 with an avatar).
// Derive real avatars from API response (pick up to 4 with an avatar).
const realAvatars: AdvisorAvatar[] = advisors
.filter((a) => a.avatar_url)
.slice(0, 3)
.slice(0, 4)
.map((a) => ({
id: a.username,
src: a.avatar_url ?? FALLBACK_AVATAR,
@ -64,8 +67,8 @@ export function AdvisorActionsCard({
{description}
</p>
<div className="mt-3.5 flex items-center justify-between gap-3">
<div className="flex items-center pl-0.5">
<div className="mt-3 flex items-center justify-between gap-3">
<div className="flex items-center shrink-0">
{isLoading
? /* ── Shimmer circles while API loads ── */
Array.from({ length: 3 }).map((_, i) => (
@ -96,13 +99,22 @@ export function AdvisorActionsCard({
)}
</div>
<Button
className="w-auto rounded-[9px] border-none bg-[#EBEDF0] bg-none px-4 py-2 text-[12.5px] font-bold text-[#111111]! shadow-none hover:bg-[#E2E4E8] active:scale-[0.98] transition-all"
href={onGetAdvisor ? undefined : getAdvisorHref}
{getAdvisorHref ? (
<Link
href={localizePath(getAdvisorHref, locale)}
className="inline-flex h-[36px] min-w-[105px] px-3.5 shrink-0 items-center justify-center rounded-[9px] border-none bg-[#EBEDF0] text-[12.5px] font-bold text-[#111111] shadow-none hover:bg-[#E2E4E8] active:scale-[0.98] transition-all cursor-pointer whitespace-nowrap"
>
{getAdvisorLabel}
</Link>
) : (
<button
type="button"
onClick={onGetAdvisor}
className="inline-flex h-[36px] min-w-[105px] px-3.5 shrink-0 items-center justify-center rounded-[9px] border-none bg-[#EBEDF0] text-[12.5px] font-bold text-[#111111] shadow-none hover:bg-[#E2E4E8] active:scale-[0.98] transition-all cursor-pointer whitespace-nowrap"
>
{getAdvisorLabel}
</Button>
</button>
)}
</div>
</div>
</section>

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

@ -16,6 +16,12 @@ type InformationSheetPresetIcon =
| "warning"
| "coin"
| "check"
| "diamond"
| "diamond-color"
| "diamond-color.svg"
| "diamond-color.png"
| "/assets/images/diamond-color.png"
| "/assets/images/diamond-color.svg"
| "stash_play-solid.svg"
| "warning.svg"
| "coin.svg"
@ -42,6 +48,7 @@ export type InformationSheetProps = Omit<
closeOnOutside?: boolean;
onClose?: () => void;
isLoading?: boolean;
showCloseButton?: boolean;
};
const DEFAULT_ICON = {
@ -98,6 +105,42 @@ const ICON_PRESETS: Record<
width: 36,
height: 36,
},
diamond: {
src: "/assets/images/diamond-color.svg",
alt: "Subscription",
width: 48,
height: 48,
},
"diamond-color": {
src: "/assets/images/diamond-color.svg",
alt: "Subscription",
width: 48,
height: 48,
},
"diamond-color.svg": {
src: "/assets/images/diamond-color.svg",
alt: "Subscription",
width: 48,
height: 48,
},
"diamond-color.png": {
src: "/assets/images/diamond-color.svg",
alt: "Subscription",
width: 48,
height: 48,
},
"/assets/images/diamond-color.png": {
src: "/assets/images/diamond-color.svg",
alt: "Subscription",
width: 48,
height: 48,
},
"/assets/images/diamond-color.svg": {
src: "/assets/images/diamond-color.svg",
alt: "Subscription",
width: 48,
height: 48,
},
};
function resolveIcon(icon: InformationSheetIcon | null | undefined) {
@ -110,6 +153,16 @@ function resolveIcon(icon: InformationSheetIcon | null | undefined) {
}
if (typeof icon === "string") {
// If diamond PNG was requested, automatically upgrade to high-res vector SVG
if (icon.includes("diamond-color")) {
return {
src: "/assets/images/diamond-color.svg",
alt: "Subscription",
width: 48,
height: 48,
};
}
return (
ICON_PRESETS[icon as InformationSheetPresetIcon] ?? {
src: icon,
@ -137,6 +190,7 @@ export function InformationSheet({
onClose,
className,
isLoading = false,
showCloseButton = true,
...props
}: InformationSheetProps) {
const { locale, dictionary: t } = useI18n();
@ -241,17 +295,68 @@ export function InformationSheet({
<section
{...props}
className={[
"w-full sm:max-w-[375px] rounded-t-[22px] bg-[#F9F8F8] px-4 pt-4 pb-[max(24px,calc(24px+var(--safe-bottom)))] text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform animate-in slide-in-from-bottom",
"relative w-full sm:max-w-[375px] rounded-t-[22px] bg-[#F9F8F8] px-4 pt-4 pb-[max(24px,calc(24px+var(--safe-bottom)))] text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform animate-in slide-in-from-bottom",
isClosing ? "translate-y-full" : "translate-y-0",
className,
]
.filter(Boolean)
.join(" ")}
>
{showCloseButton && (
<button
type="button"
onClick={closeSheet}
className="absolute top-3.5 end-3.5 z-10 flex h-8 w-8 items-center justify-center rounded-full text-[#9CA3AF] hover:bg-black/5 hover:text-[#4B5563] active:scale-95 transition-all cursor-pointer border-none bg-transparent"
aria-label={(t as any)?.["Close"] || "Close"}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
)}
<div className="mx-auto flex flex-col items-center w-full">
{isLoading ? (
<div className="flex h-[280px] min-h-[280px] w-full items-center justify-center">
<LoadingThreeDot className="text-[#FF4E67]" />
<div className="flex flex-col items-center w-full animate-in fade-in duration-200">
{/* Icon Skeleton */}
<LoadingSkeleton className="h-[56px] w-[56px] rounded-full" />
{/* Title Skeleton */}
<LoadingSkeleton className="mt-3.5 h-[22px] w-[65%] rounded-md" />
{/* Description Lines Skeleton */}
<div className="mt-3 flex flex-col items-center gap-2 w-full">
<LoadingSkeleton className="h-[14px] w-[90%] rounded-md" />
<LoadingSkeleton className="h-[14px] w-[80%] rounded-md" />
<LoadingSkeleton className="h-[14px] w-[60%] rounded-md" />
</div>
{/* Middle Box Skeleton (e.g. Plan / Info) */}
<div className="mt-4 w-full rounded-[11px] bg-white p-3 border border-gray-100 flex flex-col items-center gap-2">
<LoadingSkeleton className="h-[12px] w-[35%] rounded-md" />
<LoadingSkeleton className="h-[20px] w-[50%] rounded-md" />
</div>
{/* Subtext Skeleton */}
<div className="mt-3.5 flex flex-col items-center gap-1.5 w-full">
<LoadingSkeleton className="h-[11px] w-[85%] rounded-md" />
<LoadingSkeleton className="h-[11px] w-[70%] rounded-md" />
</div>
{/* Action / Swipe Buttons Skeleton */}
<div className="mt-5 grid grid-cols-[33fr_67fr] w-full gap-3">
<LoadingSkeleton className="h-[52px] w-full rounded-[11px]" />
<LoadingSkeleton className="h-[52px] w-full rounded-[11px]" />
</div>
</div>
) : (
<>

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

@ -135,7 +135,7 @@ export function NavigationButton({
return hasActiveSubscription ? (
<InformationSheet
icon="/assets/images/diamond-color.png"
icon="/assets/images/diamond-color.svg"
title={t["Subscription Status"] || "Subscription Status"}
description={subscriptionDescription}
buttons={t["Got it"] || t["Back"] || "Back"}
@ -201,7 +201,7 @@ export function NavigationButton({
case "subscription":
return hasActiveSubscription ? (
<Image
src="/assets/images/diamond-color.png"
src="/assets/images/diamond-color.svg"
alt=""
aria-hidden="true"
className="size-6 object-contain"
@ -292,7 +292,7 @@ export function NavigationButton({
className="flex w-full items-center gap-3 px-4 py-3 text-start text-sm font-semibold text-gray-700 hover:bg-gray-50 cursor-pointer border-0 bg-transparent"
>
<Image
src="/assets/images/diamond-color.png"
src="/assets/images/diamond-color.svg"
alt=""
width={20}
height={20}

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

@ -6,7 +6,6 @@ import {
NavigationButton,
type NavigationButtonProps,
} from "./navigation-button";
import { hasSupportAccess } from "./support-access";
type PageHeaderProps = {
className?: string;
@ -72,32 +71,19 @@ export function PageHeader({
{isCustomRightButton ? (
<NavigationButton
icon={iconFromProp || "support"}
icon={iconFromProp || "close"}
profile={profile}
{...(iconFromProp === "support" ? { iconLabel: t["Support"] } : {})}
{...rightButtonRest}
/>
) : isMale ? (
<div className="flex items-center gap-2">
<NavigationButton
icon="subscription"
profile={profile}
iconLabel={t["Subscription Status"] || "Subscription"}
/>
<NavigationButton
icon="support"
profile={profile}
iconLabel={t["Support"]}
{...rightButtonRest}
/>
</div>
) : (
<NavigationButton
icon="support"
profile={profile}
iconLabel={t["Support"]}
{...rightButtonRest}
/>
<div className="size-10 shrink-0" />
)}
</header>
);

19
src/components/Componentes/swipe-button.tsx

@ -2,6 +2,7 @@
import { useState } from "react";
import { useI18n } from "@/translations/provider";
import { LoadingSkeleton } from "./loading-skeleton";
import { LoadingThreeDot } from "./loading-three-dot";
type SwipeButtonProps = {
@ -10,6 +11,7 @@ type SwipeButtonProps = {
text: string;
cancelText?: string;
disabled?: boolean;
isLoading?: boolean;
theme?: "default" | "green";
};
@ -19,11 +21,28 @@ export function SwipeButton({
text,
cancelText,
disabled = false,
isLoading = false,
theme = "default",
}: SwipeButtonProps) {
const { dictionary: t } = useI18n();
const [clicked, setClicked] = useState(false);
if (isLoading) {
if (onCancel) {
return (
<div className="flex w-full items-center gap-3">
<LoadingSkeleton className="flex-1 h-[52px] rounded-[11px]" />
<LoadingSkeleton className="flex-1 h-[52px] rounded-[11px]" />
</div>
);
}
return (
<div className="w-full flex">
<LoadingSkeleton className="w-full h-[52px] rounded-[11px]" />
</div>
);
}
const handleClick = () => {
setClicked(true);
onSuccess();

11
src/translations/locales/en.json

@ -248,8 +248,8 @@
"General Health:": "General Health:",
"German": "German",
"Germany": "Germany",
"Get Advisor": "Get Advisor",
"Get an advisor": "Get an advisor",
"Get Advisor": "Talk to Advisor",
"Get an advisor": "Need Marriage Guidance?",
"Glasser 5 Needs Test": "Glasser 5 Needs Test",
"Go back": "Go back",
"Good": "Good",
@ -406,7 +406,7 @@
"Not a good personal fit": "Not a good personal fit",
"Not committed": "Not committed",
"Not important": "Not important",
"Not sure what to do next? Our psychology section is here to guide you at every step.": "Not sure what to do next? Our psychology section is here to guide you at every step.",
"Not sure what to do next? Our psychology section is here to guide you at every step.": "Unsure about your next step? Our expert counselors are here to help you make confident decisions.",
"Nothing is shared without your consent.": "Nothing is shared without your consent.",
"Number of Children": "Number of Children",
"Number of Siblings": "Number of Siblings",
@ -690,10 +690,10 @@
"Very religious and committed": "Very religious and committed",
"View Contact": "View Contact",
"View Contact Details": "View Contact Details",
"View More Details": "View Full Profile & Proceed",
"View More Details": "View Full Profile",
"View Profile": "View Profile",
"View contact number": "View contact number",
"View more details": "View Full Profile & Proceed",
"View more details": "View Full Profile",
"View profile": "View profile",
"Watch Video": "Watch Video",
"We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet",
@ -741,6 +741,7 @@
"Your details are only used for the matching process.": "Your details are only used for the matching process.",
"Your information is kept strictly confidential.": "Your information is kept strictly confidential.",
"Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.",
"Your request has been sent. Once the gentleman reviews your request, you will be notified.": "Your request has been sent. Once the gentleman reviews your request, you will be notified.",
"Your request has been sent. Once the lady reviews your request, you will be notified.": "Your request has been sent. Once the lady reviews your request, you will be notified.",
"Your request was rejected": "Your request was rejected",
"Your request was rejected by the lady. You will be introduced to other candidates in the future.": "Your request was rejected by the lady. You will be introduced to other candidates in the future.",

8
src/translations/locales/fa.json

@ -248,8 +248,8 @@
"General Health:": "سلامت عمومی:",
"German": "آلمانی",
"Germany": "آلمان",
"Get Advisor": "دریافت مشاور",
"Get an advisor": "دریافت مشاور",
"Get Advisor": "گفتگو با مشاور",
"Get an advisor": "نیاز به راهنمایی دارید؟",
"Glasser 5 Needs Test": "تست ۵ نیاز گلاسر",
"Go back": "بازگشت",
"Good": "خوب",
@ -406,7 +406,7 @@
"Not a good personal fit": "تناسب شخصی کافی نبود",
"Not committed": "مقید نیستم",
"Not important": "این معیار برایم اهمیت زیادی ندارد.",
"Not sure what to do next? Our psychology section is here to guide you at every step.": "نمی‌دانید قدم بعدی چیست؟ بخش روانشناسی ما در هر مرحله شما را راهنمایی می‌کند.",
"Not sure what to do next? Our psychology section is here to guide you at every step.": "در تصمیم‌گیری یا ادامه مسیر مردد هستید؟ مشاوران متخصص ما در تمام مراحل آشنایی همراه شما هستند.",
"Nothing is shared without your consent.": "هیچ چیز بدون رضایت شما به اشتراک گذاشته نمی‌شود.",
"Number of Children": "تعداد فرزندان",
"Number of Siblings": "تعداد خواهر و برادر",
@ -740,7 +740,7 @@
"Your Personality Traits": "ویژگی‌های شخصیتی خودتان",
"Your details are only used for the matching process.": "اطلاعات شما فقط برای فرآیند تطبیق‌دهی استفاده می‌شود.",
"Your information is kept strictly confidential.": "اطلاعات شما کاملاً محرمانه نگهداری می‌شود.",
"Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "حفظ حریم خصوصی و امنیت شما بالاترین اولویت ماست. ما متعهد به حفظ امنیت اطلاعات شما و اعطای کنترل کامل به شما در طول فرآیند هستیم.",
"Your request has been sent. Once the gentleman reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط آقا، به شما اطلاع‌رسانی خواهد شد.",
"Your request has been sent. Once the lady reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط خانم، به شما اطلاع‌رسانی خواهد شد.",
"Your request was rejected": "درخواست شما رد شد",
"Your request was rejected by the lady. You will be introduced to other candidates in the future.": "درخواست شما توسط خانم رد شد. به شما مورد های دیگه ای در اینده معرفی خواهد شد.",

Loading…
Cancel
Save