Browse Source

fix ssr loading

master
mortezaei 3 weeks ago
parent
commit
8830dee187
  1. 9
      src/app/finding-match/finding-match-client.tsx
  2. 4
      src/app/layout.tsx
  3. 92
      src/app/new-match/new-match-client.tsx
  4. 43
      src/app/request-accepted/request-accepted-client.tsx
  5. 37
      src/app/request-sent/request-sent-client.tsx
  6. 3
      src/data/countries.ts
  7. 50
      src/hooks/marriage/use-profile-main.ts
  8. 23
      src/lib/auth-bridge.ts

9
src/app/finding-match/finding-match-client.tsx

@ -11,7 +11,6 @@ import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay";
import Button from "@/components/Componentes/button";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
import { PageBackground } from "@/components/Componentes/page-background";
import PageHeader from "@/components/Componentes/page-header";
@ -57,18 +56,14 @@ export default function FindingMatchClient() {
}
}, [profile, locale, router]);
// Signal Flutter to lift its loading cover once the profile is available.
useHabibWebReady(!!profile && !isLoading);
// Signal Flutter to lift its loading cover immediately so page renders with 0ms latency.
useHabibWebReady(true);
const isRedirecting = useMemo(() => {
if (!profile) return false;
return getSubmitPath(profile) !== "/finding-match";
}, [profile]);
if ((isLoading && !profile) || !profile) {
return <PageLoadingSkeleton />;
}
const copy = {
title: t["SEARCH IN PROGRESS"],
description:

4
src/app/layout.tsx

@ -216,6 +216,10 @@ export default async function RootLayout({
if (!config) return;
configApplied = true;
window.__HABIB_BOOTSTRAP__ = config;
if (config.marriage) {
window.__HABIB_MARRIAGE_INITIAL_DATA__ = config.marriage;
window.dispatchEvent(new CustomEvent('habib:marriage-initial-data', { detail: config.marriage }));
}
var safe = config.safeArea || {};
root.style.setProperty('--safe-top', (Number(safe.top) || 0) + 'px');
root.style.setProperty('--safe-bottom', (Number(safe.bottom) || 0) + 'px');

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

@ -488,6 +488,7 @@ export default function NewMatchClient() {
const { isAdvisorOpen, openAdvisors, closeAdvisors } =
useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay();
const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isError, isLoading } = useMarriageProfileQuery();
const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false);
const [isDeclineConfirmOpen, setIsDeclineConfirmOpen] = useState(false);
@ -594,8 +595,23 @@ export default function NewMatchClient() {
}
}, [profile, locale, router, isLoading, isError]);
// Signal Flutter to lift its loading cover once the profile is available.
useHabibWebReady(!!profile && !isLoading);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
const handleOpenProfile = () => {
if (profile) {
openProfile();
} else {
setIsOpeningProfile(true);
}
};
useEffect(() => {
if (isOpeningProfile && profile) {
setIsOpeningProfile(false);
openProfile();
}
}, [isOpeningProfile, profile, openProfile]);
const isRedirecting = useMemo(() => {
if (!profile) return false;
@ -625,65 +641,6 @@ export default function NewMatchClient() {
}
}
if ((isLoading && !profile) || !profile) {
return (
<>
<PageBackground />
<PageHeader profile={profile} sticky />
<main
style={{
paddingBottom: `${20 + bottom}px`,
}}
className="min-h-0 flex-1 flex flex-col gap-3.5 pt-4 pb-1 text-center"
>
{/* 1. Header Section Skeleton */}
<section className="flex flex-col items-center shrink-0">
<LoadingSkeleton className="h-[56px] w-[56px] rounded-full" />
<LoadingSkeleton className="h-6 w-[200px] mt-2.5" />
<LoadingSkeleton className="h-3.5 w-[260px] mt-1.5" />
</section>
{/* 2. Match Card Skeleton */}
<section className="w-full rounded-[16px] border border-white/80 bg-white px-4 py-4 shadow-none flex flex-col items-center shrink-0">
{/* Name line */}
<LoadingSkeleton className="h-4 w-[130px]" />
{/* Subtitle / Details lines */}
<div className="mt-2.5 w-full flex flex-col items-center gap-2 min-h-[40px]">
<LoadingSkeleton className="h-3 w-[180px]" />
<LoadingSkeleton className="h-3 w-[150px]" />
</div>
{/* Button skeleton */}
<LoadingSkeleton className="mt-3.5 h-[40px] w-full rounded-[10px]" />
</section>
{/* 3. Advisor Card Skeleton */}
<section className="w-full shrink-0">
<div className="rounded-[14px] border border-white/80 bg-white/75 px-4 py-3.5 text-left shadow-none backdrop-blur-sm">
<LoadingSkeleton className="h-4 w-[120px]" />
<div className="mt-1.5 space-y-1">
<LoadingSkeleton className="h-2.5 w-[240px]" />
<LoadingSkeleton className="h-2.5 w-[160px]" />
</div>
<div className="mt-3.5 flex items-center justify-between gap-3">
<div className="flex items-center pl-1">
<LoadingSkeleton className="h-[28px] w-[28px] rounded-full border-2 border-white" />
<LoadingSkeleton className="-ml-1.5 h-[28px] w-[28px] rounded-full border-2 border-white" />
<LoadingSkeleton className="-ml-1.5 h-[28px] w-[28px] rounded-full border-2 border-white" />
</div>
<LoadingSkeleton className="h-[36px] w-[100px] rounded-[9px]" />
</div>
</div>
</section>
</main>
</>
);
}
const isFemaleProfile = profile?.gender === "female";
const matchHeadingTitle = isFemaleProfile
? t["New Marriage Proposal"]
@ -868,11 +825,12 @@ export default function NewMatchClient() {
{/* Button */}
<button
type="button"
disabled={isOpeningProfile && isLoading}
onClick={() => {
if (isMale && !hasActiveSub) {
setIsPaymentSheetOpen(true);
} else {
openProfile();
handleOpenProfile();
}
}}
className="
@ -895,8 +853,14 @@ export default function NewMatchClient() {
border-none
"
>
<Ic name="eye" className="size-5 text-white" />
<span>{t["View more details"]}</span>
{isOpeningProfile && isLoading ? (
<LoadingThreeDot />
) : (
<>
<Ic name="eye" className="size-5 text-white" />
<span>{t["View more details"]}</span>
</>
)}
</button>
</div>
</>

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

@ -20,7 +20,6 @@ import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot";
import OutcomeSelectionSheet from "@/components/Componentes/outcome-selection-sheet";
import { PageBackground } from "@/components/Componentes/page-background";
import PageHeader from "@/components/Componentes/page-header";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import SubscriptionRequiredSheet from "@/components/Componentes/subscription-required-sheet";
import SwipeButton from "@/components/Componentes/swipe-button";
import type {
@ -243,6 +242,7 @@ export default function RequestAcceptedClient() {
const [hasConfirmedFemaleContact, setHasConfirmedFemaleContact] =
useState(false);
const [showPaymentSuccessToast, setShowPaymentSuccessToast] = useState(false);
const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isLoading } = useMarriageProfileQuery();
const isFemaleProfile = profile?.gender === "female";
@ -258,8 +258,23 @@ export default function RequestAcceptedClient() {
}
}, [profile, router, locale, noContactReportedSuccess]);
// Signal Flutter to lift its loading cover once the profile is available.
useHabibWebReady(!!profile && !isLoading);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
const handleOpenProfile = () => {
if (profile) {
openProfile();
} else {
setIsOpeningProfile(true);
}
};
useEffect(() => {
if (isOpeningProfile && profile) {
setIsOpeningProfile(false);
openProfile();
}
}, [isOpeningProfile, profile, openProfile]);
const isRedirecting = useMemo(() => {
if (!profile) return false;
@ -297,10 +312,6 @@ export default function RequestAcceptedClient() {
enabled: false,
});
if ((isLoading && !profile) || !profile) {
return <PageLoadingSkeleton />;
}
const titleText = isFemaleProfile
? t["Request Approved"]
: caseStatus === "payment_done" || caseStatus === "contacted"
@ -687,10 +698,15 @@ export default function RequestAcceptedClient() {
<>
<button
type="button"
onClick={() => openProfile()}
disabled={isOpeningProfile && isLoading}
onClick={handleOpenProfile}
className="flex-1 h-[50px] px-3 rounded-[14px] 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"]}
{isOpeningProfile && isLoading ? (
<LoadingThreeDot />
) : (
t["View Profile"]
)}
</button>
<button
@ -729,10 +745,15 @@ export default function RequestAcceptedClient() {
) : (
<button
type="button"
onClick={() => openProfile()}
disabled={isOpeningProfile && isLoading}
onClick={handleOpenProfile}
className="flex-1 min-h-[48px] px-3.5 py-2.5 rounded-[14px] 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] cursor-pointer"
>
{primaryActionText}
{isOpeningProfile && isLoading ? (
<LoadingThreeDot />
) : (
primaryActionText
)}
</button>
)}

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

@ -3,9 +3,9 @@
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import { useHabibWebReady } from "@/hooks/use-habib-web-ready";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay,
@ -33,6 +33,7 @@ export default function RequestSentClient() {
useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } =
useMatchProfileOverlay();
const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000,
});
@ -47,17 +48,28 @@ export default function RequestSentClient() {
}
}, [profile, locale, router]);
// Signal Flutter to lift its loading cover once the profile is available.
useHabibWebReady(!!profile && !isLoading);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
const isRedirecting = useMemo(() => {
if (!profile) return false;
return getSubmitPath(profile) !== "/request-sent";
}, [profile]);
if ((isLoading && !profile) || !profile) {
return <PageLoadingSkeleton />;
}
const handleOpenProfile = () => {
if (profile) {
openProfile();
} else {
setIsOpeningProfile(true);
}
};
useEffect(() => {
if (isOpeningProfile && profile) {
setIsOpeningProfile(false);
openProfile();
}
}, [isOpeningProfile, profile, openProfile]);
const copy = {
advisorTitle: t["Get an advisor"],
@ -123,11 +135,16 @@ export default function RequestSentClient() {
{/* Button */}
<button
type="button"
onClick={openProfile}
disabled={isOpeningProfile && isLoading}
onClick={handleOpenProfile}
className="mt-12 w-full max-w-[300px] cursor-pointer transition-transform active:scale-[0.97]"
>
<div className="rounded-[14px] 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 className="flex items-center justify-center min-h-[54px] rounded-[14px] bg-gradient-to-r from-[#FF6687] to-[#FF456C] px-6 py-4 text-[16px] font-bold text-white shadow-sm hover:opacity-95">
{isOpeningProfile && isLoading ? (
<LoadingThreeDot />
) : (
requestSentCopy.matchProfile
)}
</div>
</button>
</section>

3
src/data/countries.ts

@ -5110,6 +5110,7 @@ export function resolveCountryName(
if (!trimmed) return "";
const isFa = String(locale || "en").toLowerCase().startsWith("fa");
const iso = getCountryIsoCode(trimmed);
// 1. If Persian locale, prioritize curated COUNTRIES_FA
if (isFa) {
@ -5136,7 +5137,7 @@ export function resolveCountryName(
// 2. Dynamic Intl.DisplayNames for the requested locale
if (iso) {
try {
const displayNames = new Intl.DisplayNames([isFa ? "fa" : "en"], {
const displayNames = new Intl.DisplayNames([locale], {
type: "region",
});
const name = displayNames.of(iso);

50
src/hooks/marriage/use-profile-main.ts

@ -1,6 +1,8 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { authBridge } from "@/lib/auth-bridge";
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache";
import {
getSubmitPath,
@ -11,6 +13,27 @@ import type { QueryOptions } from "./options";
import { marriageQueryKeys } from "./query-keys";
import type { MarriageProfileResponse } from "./types";
export function getInitialMarriageProfile(): MarriageProfileResponse | undefined {
const data = authBridge.getMarriageData();
if (!data) return undefined;
return {
id: data.id ?? 0,
status: data.status,
gender: data.gender,
age: data.age ?? null,
is_registering_for_self: data.is_registering_for_self ?? null,
can_edit_profile: data.can_edit_profile ?? true,
can_message_expert: data.can_message_expert ?? true,
active_subscription: data.active_subscription ?? null,
is_ready_for_match: data.is_ready_for_match ?? true,
active_case: data.active_case ?? null,
needs_subscription: data.needs_subscription ?? false,
recommended_plan: data.recommended_plan ?? null,
match_summary: data.match_summary ?? null,
} as MarriageProfileResponse;
}
export async function getMarriageProfile() {
console.log("🌐 [Profile API] Sending GET /api/marriage/profile/main/ ...");
try {
@ -44,9 +67,34 @@ export async function getMarriageProfile() {
export function useMarriageProfileQuery<TData = MarriageProfileResponse>(
options?: QueryOptions<MarriageProfileResponse, TData>,
) {
const queryClient = useQueryClient();
useEffect(() => {
const handleInitialData = (e: Event) => {
const customEvent = e as CustomEvent;
const initial = getInitialMarriageProfile();
if (initial) {
queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => {
if (!old) return initial;
return {
...old,
...initial,
match_summary: initial.match_summary ?? old.match_summary,
};
});
}
};
window.addEventListener("habib:marriage-initial-data", handleInitialData);
return () => {
window.removeEventListener("habib:marriage-initial-data", handleInitialData);
};
}, [queryClient]);
const query = useQuery({
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
initialData: getInitialMarriageProfile as any,
...options,
queryFn: getMarriageProfile,
queryKey: marriageQueryKeys.profile(),

23
src/lib/auth-bridge.ts

@ -23,6 +23,7 @@ function postFlutterMessage(payload: Record<string, unknown>) {
class AuthBridge {
private token: string | null = null;
private coins = 0;
private marriageData: Record<string, any> | null = null;
private isReady = false;
private loginRequested = false;
private readyCallbacks: Array<() => void> = [];
@ -33,6 +34,10 @@ class AuthBridge {
this.init();
}
public getMarriageData(): Record<string, any> | null {
return this.marriageData;
}
private syncFromStorage() {
if (typeof window === "undefined") {
return false;
@ -41,8 +46,15 @@ class AuthBridge {
const win = window as Window & {
HABIB_TOKEN?: string;
HABIB_COINS?: number;
HABIB_MARRIAGE?: Record<string, any>;
__HABIB_MARRIAGE_INITIAL_DATA__?: Record<string, any>;
};
if (win.HABIB_MARRIAGE || win.__HABIB_MARRIAGE_INITIAL_DATA__) {
this.marriageData =
win.HABIB_MARRIAGE ?? win.__HABIB_MARRIAGE_INITIAL_DATA__ ?? null;
}
const token =
win.HABIB_TOKEN ??
getClientCookie(TOKEN_COOKIE_NAME) ??
@ -88,6 +100,17 @@ class AuthBridge {
(event) => {
if (event.action === "login") {
this.handleLoginResponse(event.success);
} else if (
event.action === "initial_config" &&
event.data &&
(event.data as any).marriage
) {
this.marriageData = (event.data as any).marriage;
window.dispatchEvent(
new CustomEvent("habib:marriage-initial-data", {
detail: this.marriageData,
}),
);
}
},
);

Loading…
Cancel
Save