Browse Source

refactor: update SSR fetch timeouts, improve UI navigation styling, and add logging to profile requests

master
mortezaei 6 days ago
parent
commit
e6eba2d0c6
  1. 128
      src/app/new-match/profile/page.tsx
  2. 235
      src/components/Componentes/navigation-button.tsx

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

@ -252,14 +252,12 @@ function NewMatchProfileSkeleton({
</section>
<div
style={{ paddingBottom: `var(--safe-bottom)` }}
className="shrink-0 z-30 px-[17px]"
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="mx-auto w-full sm:max-w-[375px] rounded-t-[24px] bg-white px-4 py-4 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
<div className="flex gap-3">
<LoadingSkeleton className="w-1/3 h-[46px] rounded-[12px]" />
<LoadingSkeleton className="w-2/3 h-[46px] rounded-[12px]" />
</div>
<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>
@ -286,7 +284,9 @@ type NewMatchProfilePageProps = {
onClose?: () => void;
};
export default function NewMatchProfilePage({ onClose }: NewMatchProfilePageProps = {}) {
export default function NewMatchProfilePage({
onClose,
}: NewMatchProfilePageProps = {}) {
const { dictionary: t, locale } = useI18n();
const router = useRouter();
const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false);
@ -672,70 +672,68 @@ export default function NewMatchProfilePage({ onClose }: NewMatchProfilePageProp
</section>
<div
style={{ paddingBottom: `var(--safe-bottom)` }}
className="shrink-0 z-30 px-[17px]"
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="mx-auto w-full sm:max-w-[375px] rounded-t-[24px] bg-white px-4 py-4 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
{caseStatus === "payment_done" ||
caseStatus === "contacted" ||
caseStatus === "finalized" ||
profile?.status === "matched" ? (
{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"
onClick={() => 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]"
disabled={!isRejectProfileEnabled || isSubmitting}
onClick={() => {
if (isFemaleProfile) {
setIsRejectSheetOpen(true);
} else {
setIsMaleRejectWarningOpen(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.Back}
{t.Reject}
</button>
) : (
<div className="flex gap-3">
<button
type="button"
disabled={!isRejectProfileEnabled || isSubmitting}
onClick={() => {
if (isFemaleProfile) {
setIsRejectSheetOpen(true);
} else {
setIsMaleRejectWarningOpen(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.Reject}
</button>
<button
type="button"
disabled={!isAcceptProfileEnabled || isSubmitting}
onClick={() => {
if (!isAcceptProfileEnabled) {
return;
}
<button
type="button"
disabled={!isAcceptProfileEnabled || isSubmitting}
onClick={() => {
if (!isAcceptProfileEnabled) {
return;
}
if (isFemaleProfile) {
setIsFemaleConsentChecked(false);
}
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>
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>
</>

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

@ -1,27 +1,14 @@
"use client";
import { useQueryClient } from "@tanstack/react-query";
import Image from "next/image";
import { useRouter } from "next/navigation";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { useEffect, useRef, useState } from "react";
import { GoArrowLeft } from "react-icons/go";
import { HiEllipsisVertical } from "react-icons/hi2";
import { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import { useHabcoinInventoryQuery } from "@/hooks/marriage/use-habcoin-inventory";
import {
extractHabcoinPaymentUrl,
useHabcoinPaymentMutation,
} from "@/hooks/marriage/use-habcoin-payment";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
buyHabibCoinPackages,
isInFlutterWebView,
} from "@/lib/webview-actions";
import { isInFlutterWebView } from "@/lib/webview-actions";
import { useI18n } from "@/translations/provider";
import { DiscountWidget } from "./discount-widget";
import type { CheckDiscountResult } from "@/hooks/marriage/use-validate-discount";
import ErrorToast from "./error-toast";
import HelpModal from "./help-modal";
import InformationSheet from "./information-sheet";
import { hasSupportAccess } from "./support-access";
@ -50,6 +37,7 @@ export type NavigationButtonProps = Omit<
helpDescription?: ReactNode;
helpButtonText?: ReactNode;
disableHelpModal?: boolean;
badge?: ReactNode;
profile?: any;
};
@ -63,19 +51,20 @@ export function NavigationButton({
helpDescription,
helpButtonText,
disableHelpModal = false,
badge,
profile: profileProp,
...props
}: NavigationButtonProps) {
const router = useRouter();
const { dictionary: t } = useI18n();
const { dictionary: t, locale } = useI18n();
const isRtl = locale === "fa" || locale === "ar" || locale === "ur";
const [isHelpOpen, setIsHelpOpen] = useState(false);
const [isSupportOpen, setIsSupportOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isSubscriptionInfoOpen, setIsSubscriptionInfoOpen] = useState(false);
const [isSubscriptionLoading, setIsSubscriptionLoading] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Only fetch profile for icons that actually need profile data if not provided via prop.
const needsProfile =
!profileProp &&
(icon === "support" ||
@ -88,29 +77,12 @@ export function NavigationButton({
});
const profile = profileProp !== undefined ? profileProp : queriedProfile;
const refetch = refetchQuery;
const paymentMutation = useHabcoinPaymentMutation();
const queryClient = useQueryClient();
const [toastMessage, setToastMessage] = useState<string | null>(null);
const [toastVariant, setToastVariant] = useState<"error" | "success">(
"error",
);
const [isRenewing, setIsRenewing] = useState(false);
const [isInsufficientCoins, setIsInsufficientCoins] = useState(false);
const [appliedDiscount, setAppliedDiscount] =
useState<CheckDiscountResult | null>(null);
const { data: inventory } = useHabcoinInventoryQuery({
enabled: isSubscriptionInfoOpen,
});
const planPrice = Number(profile?.recommended_plan?.price) || 0;
const finalPrice = appliedDiscount?.valid
? appliedDiscount.discountedPrice
: planPrice;
const coinBalance = inventory?.coin_balance ?? 0;
const hasEnoughCoins =
!isInsufficientCoins &&
(inventory === undefined ? true : coinBalance >= finalPrice);
const isFemale = profile?.gender === "female";
const hasActiveSubscription =
!!profile?.active_subscription &&
profile.active_subscription.is_active !== false &&
profile.active_subscription.is_valid !== false;
const handleOpenSubscriptionModal = () => {
setIsSubscriptionInfoOpen(true);
@ -128,10 +100,7 @@ export function NavigationButton({
return (
<InformationSheet
isLoading={true}
onClose={() => {
setIsSubscriptionInfoOpen(false);
setAppliedDiscount(null);
}}
onClose={() => setIsSubscriptionInfoOpen(false)}
/>
);
}
@ -165,171 +134,29 @@ export function NavigationButton({
}
return hasActiveSubscription ? (
<>
<InformationSheet
icon="/assets/images/diamond-color.png"
title={t["Subscription Status"]}
description={subscriptionDescription}
onClose={() => {
setIsSubscriptionInfoOpen(false);
setAppliedDiscount(null);
}}
buttons={({ close }) => (
<div className="space-y-3">
{profile?.recommended_plan?.id ? (
<DiscountWidget
objectId={profile.recommended_plan.id}
onDiscountApplied={(res) => setAppliedDiscount(res)}
onDiscountCleared={() => setAppliedDiscount(null)}
/>
) : null}
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={close}
>
<div className="inline-flex w-full items-center justify-center rounded-[11px] border border-[#747474] bg-transparent px-2 h-[52px] text-[16px] font-semibold text-[#747474] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">{t["Back"]}</span>
</div>
</button>
{!hasEnoughCoins ? (
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left min-w-0"
onClick={() => buyHabibCoinPackages()}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">
{t["Buy Habib Coins"] || "Buy Habib Coins"}
</span>
</div>
</button>
) : (
<button
type="button"
disabled={!profile?.recommended_plan?.id || isRenewing}
className="appearance-none border-0 bg-transparent p-0 text-left disabled:cursor-not-allowed disabled:opacity-70 min-w-0"
onClick={async () => {
const planId = profile?.recommended_plan?.id;
if (!planId) return;
setIsRenewing(true);
setToastMessage(null);
setToastVariant("error");
setIsInsufficientCoins(false);
try {
const paymentResponse =
await paymentMutation.mutateAsync({
objectId: planId,
discountCode: appliedDiscount?.valid
? appliedDiscount.code
: undefined,
});
const paymentUrl =
extractHabcoinPaymentUrl(paymentResponse);
if (paymentUrl) {
window.location.assign(paymentUrl);
return;
}
// Success! Refetch profile query to update remaining_days in modal.
await queryClient.refetchQueries({
queryKey: marriageQueryKeys.profile(),
});
// Close the modal and show success toast
setIsSubscriptionInfoOpen(false);
setAppliedDiscount(null);
setToastVariant("success");
setToastMessage(
t["Payment successful"] || "Payment successful",
);
} catch (err: any) {
console.error("Renewal payment failed", err);
const errMessage =
err?.response?.data?.error || err?.message || "";
setToastVariant("error");
if (
errMessage.includes("Not enough coins") ||
err?.response?.status === 400
) {
setIsInsufficientCoins(true);
setToastMessage(
t[
"Insufficient coin balance. Please recharge your account."
] ||
"Insufficient coin balance. Please recharge your account.",
);
} else {
setToastMessage(errMessage || "Payment failed");
}
} finally {
setIsRenewing(false);
}
}}
>
<div className="inline-flex w-full items-center justify-center gap-2 rounded-[11px] bg-[#F0445B] px-4 h-[52px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90 min-w-0">
<span className="truncate">
{isRenewing
? t["Renewing..."]
: t["Renew Subscription"]}
</span>
{!isRenewing && (
<span className="inline-flex items-center gap-1.5 rounded-[12px] bg-black/15 px-[9px] py-[4px] text-xs font-semibold leading-none text-white shrink-0 min-w-0 whitespace-nowrap">
<Image
src="/assets/images/Inner Plugdsain Iframe.svg"
alt=""
aria-hidden="true"
width={16}
height={16}
className="shrink-0"
/>
{appliedDiscount?.valid ? (
<>
<span className="line-through text-white/50 text-[11px]">
{planPrice}
</span>
<span className="font-bold text-white text-[12px]">
{finalPrice}
</span>
</>
) : (
<span className="truncate">
{planPrice || "100"}
</span>
)}
</span>
)}
</div>
</button>
)}
</div>
</div>
)}
/>
</>
<InformationSheet
icon="/assets/images/diamond-color.png"
title={t["Subscription Status"] || "Subscription Status"}
description={subscriptionDescription}
buttons={t["Got it"] || t["Back"] || "Back"}
onClose={() => setIsSubscriptionInfoOpen(false)}
/>
) : (
<InformationSheet
icon="/assets/images/icon-park-outline_diamond.svg"
title={t["No Active Subscription"]}
title={t["No Active Subscription"] || "No Active Subscription"}
description={
t[
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you."
]
] ||
"You currently do not have an active subscription. Activation of subscription is only possible when your first case is introduced to you."
}
buttons={t["Got it"]}
buttons={t["Got it"] || t["Back"] || "Back"}
onClose={() => setIsSubscriptionInfoOpen(false)}
/>
);
};
const isFemale = profile?.gender === "female";
const hasActiveSubscription =
!!profile?.active_subscription &&
profile.active_subscription.is_active !== false &&
profile.active_subscription.is_valid !== false;
useEffect(() => {
if (!isDropdownOpen) return;
const handleOutsideClick = (event: MouseEvent) => {
@ -483,14 +310,6 @@ export function NavigationButton({
/>
{renderSubscriptionModal()}
{toastMessage && (
<ErrorToast
message={toastMessage}
variant={toastVariant}
onClose={() => setToastMessage(null)}
/>
)}
</div>
);
}
@ -560,14 +379,6 @@ export function NavigationButton({
/>
{renderSubscriptionModal()}
{toastMessage && (
<ErrorToast
message={toastMessage}
variant={toastVariant}
onClose={() => setToastMessage(null)}
/>
)}
</>
);
}

Loading…
Cancel
Save