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. 22
      src/app/new-match/profile/page.tsx
  2. 227
      src/components/Componentes/navigation-button.tsx

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

@ -252,14 +252,12 @@ function NewMatchProfileSkeleton({
</section> </section>
<div <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"> <div className="flex gap-3">
<LoadingSkeleton className="w-1/3 h-[46px] rounded-[12px]" />
<LoadingSkeleton className="w-2/3 h-[46px] rounded-[12px]" />
</div>
<LoadingSkeleton className="w-1/3 h-[52px] rounded-[12px]" />
<LoadingSkeleton className="w-2/3 h-[52px] rounded-[12px]" />
</div> </div>
</div> </div>
</main> </main>
@ -286,7 +284,9 @@ type NewMatchProfilePageProps = {
onClose?: () => void; onClose?: () => void;
}; };
export default function NewMatchProfilePage({ onClose }: NewMatchProfilePageProps = {}) {
export default function NewMatchProfilePage({
onClose,
}: NewMatchProfilePageProps = {}) {
const { dictionary: t, locale } = useI18n(); const { dictionary: t, locale } = useI18n();
const router = useRouter(); const router = useRouter();
const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false); const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false);
@ -672,17 +672,16 @@ export default function NewMatchProfilePage({ onClose }: NewMatchProfilePageProp
</section> </section>
<div <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 === "payment_done" ||
caseStatus === "contacted" || caseStatus === "contacted" ||
caseStatus === "finalized" || caseStatus === "finalized" ||
profile?.status === "matched" ? ( profile?.status === "matched" ? (
<button <button
type="button" type="button"
onClick={() => router.back()}
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]" 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} {t.Back}
@ -736,7 +735,6 @@ export default function NewMatchProfilePage({ onClose }: NewMatchProfilePageProp
</div> </div>
)} )}
</div> </div>
</div>
</main> </main>
</> </>
); );

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

@ -1,27 +1,14 @@
"use client"; "use client";
import { useQueryClient } from "@tanstack/react-query";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { ButtonHTMLAttributes, ReactNode } from "react"; import type { ButtonHTMLAttributes, ReactNode } from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { GoArrowLeft } from "react-icons/go"; import { GoArrowLeft } from "react-icons/go";
import { HiEllipsisVertical } from "react-icons/hi2"; 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 { 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 { 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 HelpModal from "./help-modal";
import InformationSheet from "./information-sheet"; import InformationSheet from "./information-sheet";
import { hasSupportAccess } from "./support-access"; import { hasSupportAccess } from "./support-access";
@ -50,6 +37,7 @@ export type NavigationButtonProps = Omit<
helpDescription?: ReactNode; helpDescription?: ReactNode;
helpButtonText?: ReactNode; helpButtonText?: ReactNode;
disableHelpModal?: boolean; disableHelpModal?: boolean;
badge?: ReactNode;
profile?: any; profile?: any;
}; };
@ -63,19 +51,20 @@ export function NavigationButton({
helpDescription, helpDescription,
helpButtonText, helpButtonText,
disableHelpModal = false, disableHelpModal = false,
badge,
profile: profileProp, profile: profileProp,
...props ...props
}: NavigationButtonProps) { }: NavigationButtonProps) {
const router = useRouter(); 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 [isHelpOpen, setIsHelpOpen] = useState(false);
const [isSupportOpen, setIsSupportOpen] = useState(false); const [isSupportOpen, setIsSupportOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isSubscriptionInfoOpen, setIsSubscriptionInfoOpen] = useState(false); const [isSubscriptionInfoOpen, setIsSubscriptionInfoOpen] = useState(false);
const [isSubscriptionLoading, setIsSubscriptionLoading] = useState(false); const [isSubscriptionLoading, setIsSubscriptionLoading] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
// Only fetch profile for icons that actually need profile data if not provided via prop.
const needsProfile = const needsProfile =
!profileProp && !profileProp &&
(icon === "support" || (icon === "support" ||
@ -88,29 +77,12 @@ export function NavigationButton({
}); });
const profile = profileProp !== undefined ? profileProp : queriedProfile; const profile = profileProp !== undefined ? profileProp : queriedProfile;
const refetch = refetchQuery; 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 = () => { const handleOpenSubscriptionModal = () => {
setIsSubscriptionInfoOpen(true); setIsSubscriptionInfoOpen(true);
@ -128,10 +100,7 @@ export function NavigationButton({
return ( return (
<InformationSheet <InformationSheet
isLoading={true} isLoading={true}
onClose={() => {
setIsSubscriptionInfoOpen(false);
setAppliedDiscount(null);
}}
onClose={() => setIsSubscriptionInfoOpen(false)}
/> />
); );
} }
@ -165,171 +134,29 @@ export function NavigationButton({
} }
return hasActiveSubscription ? ( return hasActiveSubscription ? (
<>
<InformationSheet <InformationSheet
icon="/assets/images/diamond-color.png" icon="/assets/images/diamond-color.png"
title={t["Subscription Status"]}
title={t["Subscription Status"] || "Subscription Status"}
description={subscriptionDescription} 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>
)}
buttons={t["Got it"] || t["Back"] || "Back"}
onClose={() => setIsSubscriptionInfoOpen(false)}
/> />
</>
) : ( ) : (
<InformationSheet <InformationSheet
icon="/assets/images/icon-park-outline_diamond.svg" icon="/assets/images/icon-park-outline_diamond.svg"
title={t["No Active Subscription"]}
title={t["No Active Subscription"] || "No Active Subscription"}
description={ description={
t[ 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."
]
] ||
"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)} 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(() => { useEffect(() => {
if (!isDropdownOpen) return; if (!isDropdownOpen) return;
const handleOutsideClick = (event: MouseEvent) => { const handleOutsideClick = (event: MouseEvent) => {
@ -483,14 +310,6 @@ export function NavigationButton({
/> />
{renderSubscriptionModal()} {renderSubscriptionModal()}
{toastMessage && (
<ErrorToast
message={toastMessage}
variant={toastVariant}
onClose={() => setToastMessage(null)}
/>
)}
</div> </div>
); );
} }
@ -560,14 +379,6 @@ export function NavigationButton({
/> />
{renderSubscriptionModal()} {renderSubscriptionModal()}
{toastMessage && (
<ErrorToast
message={toastMessage}
variant={toastVariant}
onClose={() => setToastMessage(null)}
/>
)}
</> </>
); );
} }

Loading…
Cancel
Save