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, useMarriageAdvisorsOverlay,
} from "@/components/Componentes/marriage-advisors-overlay"; } from "@/components/Componentes/marriage-advisors-overlay";
import Button from "@/components/Componentes/button"; import Button from "@/components/Componentes/button";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end"; import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
import { PageBackground } from "@/components/Componentes/page-background"; import { PageBackground } from "@/components/Componentes/page-background";
import PageHeader from "@/components/Componentes/page-header"; import PageHeader from "@/components/Componentes/page-header";
@ -57,18 +56,14 @@ export default function FindingMatchClient() {
} }
}, [profile, locale, router]); }, [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(() => { const isRedirecting = useMemo(() => {
if (!profile) return false; if (!profile) return false;
return getSubmitPath(profile) !== "/finding-match"; return getSubmitPath(profile) !== "/finding-match";
}, [profile]); }, [profile]);
if ((isLoading && !profile) || !profile) {
return <PageLoadingSkeleton />;
}
const copy = { const copy = {
title: t["SEARCH IN PROGRESS"], title: t["SEARCH IN PROGRESS"],
description: description:

4
src/app/layout.tsx

@ -216,6 +216,10 @@ export default async function RootLayout({
if (!config) return; if (!config) return;
configApplied = true; configApplied = true;
window.__HABIB_BOOTSTRAP__ = config; 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 || {}; var safe = config.safeArea || {};
root.style.setProperty('--safe-top', (Number(safe.top) || 0) + 'px'); root.style.setProperty('--safe-top', (Number(safe.top) || 0) + 'px');
root.style.setProperty('--safe-bottom', (Number(safe.bottom) || 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 } = const { isAdvisorOpen, openAdvisors, closeAdvisors } =
useMarriageAdvisorsOverlay(); useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay(); const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay();
const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isError, isLoading } = useMarriageProfileQuery(); const { data: profile, isError, isLoading } = useMarriageProfileQuery();
const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false); const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false);
const [isDeclineConfirmOpen, setIsDeclineConfirmOpen] = useState(false); const [isDeclineConfirmOpen, setIsDeclineConfirmOpen] = useState(false);
@ -594,8 +595,23 @@ export default function NewMatchClient() {
} }
}, [profile, locale, router, isLoading, isError]); }, [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(() => { const isRedirecting = useMemo(() => {
if (!profile) return false; 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 isFemaleProfile = profile?.gender === "female";
const matchHeadingTitle = isFemaleProfile const matchHeadingTitle = isFemaleProfile
? t["New Marriage Proposal"] ? t["New Marriage Proposal"]
@ -868,11 +825,12 @@ export default function NewMatchClient() {
{/* Button */} {/* Button */}
<button <button
type="button" type="button"
disabled={isOpeningProfile && isLoading}
onClick={() => { onClick={() => {
if (isMale && !hasActiveSub) { if (isMale && !hasActiveSub) {
setIsPaymentSheetOpen(true); setIsPaymentSheetOpen(true);
} else { } else {
openProfile();
handleOpenProfile();
} }
}} }}
className=" className="
@ -895,8 +853,14 @@ export default function NewMatchClient() {
border-none 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> </button>
</div> </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 OutcomeSelectionSheet from "@/components/Componentes/outcome-selection-sheet";
import { PageBackground } from "@/components/Componentes/page-background"; import { PageBackground } from "@/components/Componentes/page-background";
import PageHeader from "@/components/Componentes/page-header"; import PageHeader from "@/components/Componentes/page-header";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import SubscriptionRequiredSheet from "@/components/Componentes/subscription-required-sheet"; import SubscriptionRequiredSheet from "@/components/Componentes/subscription-required-sheet";
import SwipeButton from "@/components/Componentes/swipe-button"; import SwipeButton from "@/components/Componentes/swipe-button";
import type { import type {
@ -243,6 +242,7 @@ export default function RequestAcceptedClient() {
const [hasConfirmedFemaleContact, setHasConfirmedFemaleContact] = const [hasConfirmedFemaleContact, setHasConfirmedFemaleContact] =
useState(false); useState(false);
const [showPaymentSuccessToast, setShowPaymentSuccessToast] = useState(false); const [showPaymentSuccessToast, setShowPaymentSuccessToast] = useState(false);
const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isLoading } = useMarriageProfileQuery(); const { data: profile, isLoading } = useMarriageProfileQuery();
const isFemaleProfile = profile?.gender === "female"; const isFemaleProfile = profile?.gender === "female";
@ -258,8 +258,23 @@ export default function RequestAcceptedClient() {
} }
}, [profile, router, locale, noContactReportedSuccess]); }, [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(() => { const isRedirecting = useMemo(() => {
if (!profile) return false; if (!profile) return false;
@ -297,10 +312,6 @@ export default function RequestAcceptedClient() {
enabled: false, enabled: false,
}); });
if ((isLoading && !profile) || !profile) {
return <PageLoadingSkeleton />;
}
const titleText = isFemaleProfile const titleText = isFemaleProfile
? t["Request Approved"] ? t["Request Approved"]
: caseStatus === "payment_done" || caseStatus === "contacted" : caseStatus === "payment_done" || caseStatus === "contacted"
@ -687,10 +698,15 @@ export default function RequestAcceptedClient() {
<> <>
<button <button
type="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]" 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>
<button <button
@ -729,10 +745,15 @@ export default function RequestAcceptedClient() {
) : ( ) : (
<button <button
type="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" 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> </button>
)} )}

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

@ -3,9 +3,9 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; 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 { 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 AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import MarriageAdvisorsOverlay, { import MarriageAdvisorsOverlay, {
useMarriageAdvisorsOverlay, useMarriageAdvisorsOverlay,
@ -33,6 +33,7 @@ export default function RequestSentClient() {
useMarriageAdvisorsOverlay(); useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } = const { isProfileOpen, openProfile, closeProfile } =
useMatchProfileOverlay(); useMatchProfileOverlay();
const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isLoading } = useMarriageProfileQuery({ const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000, refetchInterval: 3000,
}); });
@ -47,17 +48,28 @@ export default function RequestSentClient() {
} }
}, [profile, locale, router]); }, [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(() => { const isRedirecting = useMemo(() => {
if (!profile) return false; if (!profile) return false;
return getSubmitPath(profile) !== "/request-sent"; return getSubmitPath(profile) !== "/request-sent";
}, [profile]); }, [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 = { const copy = {
advisorTitle: t["Get an advisor"], advisorTitle: t["Get an advisor"],
@ -123,11 +135,16 @@ export default function RequestSentClient() {
{/* Button */} {/* Button */}
<button <button
type="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]" 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> </div>
</button> </button>
</section> </section>

3
src/data/countries.ts

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

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

@ -1,6 +1,8 @@
"use client"; "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 { setCachedMarriageEntryPath } from "@/lib/entry-route-cache";
import { import {
getSubmitPath, getSubmitPath,
@ -11,6 +13,27 @@ import type { QueryOptions } from "./options";
import { marriageQueryKeys } from "./query-keys"; import { marriageQueryKeys } from "./query-keys";
import type { MarriageProfileResponse } from "./types"; 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() { export async function getMarriageProfile() {
console.log("🌐 [Profile API] Sending GET /api/marriage/profile/main/ ..."); console.log("🌐 [Profile API] Sending GET /api/marriage/profile/main/ ...");
try { try {
@ -44,9 +67,34 @@ export async function getMarriageProfile() {
export function useMarriageProfileQuery<TData = MarriageProfileResponse>( export function useMarriageProfileQuery<TData = MarriageProfileResponse>(
options?: QueryOptions<MarriageProfileResponse, TData>, 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({ const query = useQuery({
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
initialData: getInitialMarriageProfile as any,
...options, ...options,
queryFn: getMarriageProfile, queryFn: getMarriageProfile,
queryKey: marriageQueryKeys.profile(), queryKey: marriageQueryKeys.profile(),

23
src/lib/auth-bridge.ts

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