From be26287b6c1c3ec60d9ca2c81270c7cf485e9262 Mon Sep 17 00:00:00 2001 From: Ali Alavi Date: Wed, 16 Sep 2026 06:46:46 +0000 Subject: [PATCH] chore(staging): sync marriage frontend codebase completely with master --- auto_deploy_staging.sh | 32 +- docker-compose.staging.yml | 6 +- next.config.ts | 1 + package.json | 2 +- src/app/marriage-advisors/page.tsx | 374 ++++++++- src/app/new-match/profile/page.tsx | 753 +++++++++++++++++- .../Componentes/marriage-advisors-client.tsx | 373 --------- .../Componentes/marriage-advisors-overlay.tsx | 4 +- .../Componentes/match-profile-overlay.tsx | 4 +- .../Componentes/new-match-profile-client.tsx | 752 ----------------- src/translations/locales/en.json | 1 - 11 files changed, 1146 insertions(+), 1156 deletions(-) delete mode 100644 src/components/Componentes/marriage-advisors-client.tsx delete mode 100644 src/components/Componentes/new-match-profile-client.tsx diff --git a/auto_deploy_staging.sh b/auto_deploy_staging.sh index 62e585d..2c1a43d 100755 --- a/auto_deploy_staging.sh +++ b/auto_deploy_staging.sh @@ -16,18 +16,22 @@ fi # Fetch latest master and staging from origin git fetch origin master staging --quiet -# Check 1: Commits ahead on origin/staging (pull staging updates first) +# Check 1: Commits ahead on origin/master +MASTER_COMMITS=$(git rev-list --count HEAD..origin/master 2>/dev/null || echo 0) + +# Check 2: Commits ahead on origin/staging STAGING_COMMITS=$(git rev-list --count HEAD..origin/staging 2>/dev/null || echo 0) -if [ "$STAGING_COMMITS" -gt 0 ]; then - echo "$LOG_TAG 🚀 Detected $STAGING_COMMITS new commit(s) on origin/staging!" - git log --oneline HEAD..origin/staging - echo "$LOG_TAG Fast-forwarding / pulling latest origin/staging..." - git merge origin/staging --ff-only || git pull origin staging --no-edit + +DEPLOYED_COMMIT_FILE="$PROJECT_DIR/.last_deployed_commit" +LAST_DEPLOYED=$(cat "$DEPLOYED_COMMIT_FILE" 2>/dev/null || echo "") +CURRENT_HEAD=$(git rev-parse HEAD) + +NEED_BUILD=0 + +if [ "$CURRENT_HEAD" != "$LAST_DEPLOYED" ]; then NEED_BUILD=1 fi -# Check 2: Commits ahead on origin/master -MASTER_COMMITS=$(git rev-list --count HEAD..origin/master 2>/dev/null || echo 0) if [ "$MASTER_COMMITS" -gt 0 ]; then echo "$LOG_TAG 🚀 Detected $MASTER_COMMITS new commit(s) on origin/master!" git log --oneline HEAD..origin/master @@ -38,13 +42,11 @@ if [ "$MASTER_COMMITS" -gt 0 ]; then NEED_BUILD=1 fi -DEPLOYED_COMMIT_FILE="$PROJECT_DIR/.last_deployed_commit" -LAST_DEPLOYED=$(cat "$DEPLOYED_COMMIT_FILE" 2>/dev/null || echo "") -CURRENT_HEAD=$(git rev-parse HEAD) - -NEED_BUILD=0 - -if [ "$CURRENT_HEAD" != "$LAST_DEPLOYED" ]; then +if [ "$STAGING_COMMITS" -gt 0 ]; then + echo "$LOG_TAG 🚀 Detected $STAGING_COMMITS new commit(s) on origin/staging!" + git log --oneline HEAD..origin/staging + echo "$LOG_TAG Pulling latest origin/staging..." + git merge origin/staging --no-edit || git pull origin staging --no-edit NEED_BUILD=1 fi diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index cbcc255..7efc96d 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -16,7 +16,7 @@ services: environment: - NODE_ENV=production - NEXT_PUBLIC_API_BASE_URL=https://habib.nwhco.ir - - API_BASE_URL=http://najm-backend:8000 + - API_BASE_URL=http://najm_staging_web:8000 - NEXT_PUBLIC_SECURITY_KEY=t5yugymks5458fd4ghfg6h6fg healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/healthz"] @@ -25,9 +25,7 @@ services: retries: 3 start_period: 15s networks: - najm_staging_net: - aliases: - - marriage-staging-web + - najm_staging_net networks: najm_staging_net: diff --git a/next.config.ts b/next.config.ts index e68a3e3..cc3a473 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,6 +6,7 @@ const nextConfig: NextConfig = { poweredByHeader: false, reactStrictMode: true, allowedDevOrigins: ["192.168.1.64"], + turbopack: {}, devIndicators: false, // Compression diff --git a/package.json b/package.json index ce0af22..6bfa70e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev --webpack", - "build": "next build --webpack", + "build": "next build", "start": "next start", "lint": "biome check", "format": "biome format --write", diff --git a/src/app/marriage-advisors/page.tsx b/src/app/marriage-advisors/page.tsx index 3de83df..1c98497 100644 --- a/src/app/marriage-advisors/page.tsx +++ b/src/app/marriage-advisors/page.tsx @@ -1,5 +1,373 @@ -import MarriageAdvisorsClient from "@/components/Componentes/marriage-advisors-client"; +"use client"; -export default function MarriageAdvisorsPage() { - return ; +import { useState } from "react"; +import Image from "next/image"; +import { Ic } from "@/icons"; +import StickyHeader from "@/components/Componentes/sticky-header"; +import NavigationButton from "@/components/Componentes/navigation-button"; +import { PageBackground } from "@/components/Componentes/page-background"; +import SupportSheet from "@/components/Componentes/support-sheet"; +import { useI18n } from "@/translations/provider"; +import { + useMarriageAdvisorsQuery, + type MarriageConsultant, +} from "@/hooks/marriage/use-marriage-advisors"; +import { postActionToFlutter } from "@/lib/webview-actions"; + +const FALLBACK_AVATAR = "/assets/images/Avatar Image.png"; + +type MarriageAdvisorsPageProps = { + onClose?: () => void; +}; + +export default function MarriageAdvisorsPage({ + onClose, +}: MarriageAdvisorsPageProps = {}) { + const { dictionary: t } = useI18n(); + const [isSupportOpen, setIsSupportOpen] = useState(false); + + const { data, isLoading, isError, refetch } = useMarriageAdvisorsQuery(); + const advisors = data?.results ?? []; + + const mainStyle = { + backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, + backgroundColor: "#F5F5F5", + backgroundRepeat: "repeat-y", + backgroundSize: "100% auto", + }; + + // Opens the native Talk consultant page; the Flutter side fetches the + // consultant by username, so only the bare username is needed. Outside the + // WebView (plain browser) postActionToFlutter is a no-op returning false. + const openConsultantPage = (username: string) => { + postActionToFlutter("open_consultant_page", { consultant: username }); + }; + + return ( + <> + +
+ +
+ +

+ {t["Marriage advisors"]} +

+
+
+ + +
+ {isLoading && } + + {isError && ( +
+

+ {t["Failed to load advisors."]} +

+ +
+ )} + + {!isLoading && !isError && advisors.length === 0 && ( +
+

+ {t["No advisors are currently available."]} +

+
+ )} + + {advisors.map((advisor) => ( + openConsultantPage(advisor.username)} + /> + ))} +
+
+ + setIsSupportOpen(false)} + /> + + ); +} + +function AdvisorCard({ + advisor, + onOpen, +}: { + advisor: MarriageConsultant; + onOpen: () => void; +}) { + const { dictionary: t, locale } = useI18n(); + const isRtl = locale === "fa" || locale === "ar" || locale === "ur"; + const unreadCount = advisor.unread_count ?? 0; + const isOnline = advisor.status === "online"; + const rating = Number(advisor.avg_rate ?? 0); + const hasChat = advisor.contact_type?.includes("chat"); + const hasVoice = advisor.contact_type?.includes("voice"); + const hasVideo = advisor.contact_type?.includes("video"); + const hasActiveContact = hasChat || hasVoice || hasVideo; + const topics = advisor.topics?.filter(Boolean) ?? []; + + return ( +
{ + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onOpen(); + } + }} + className="relative w-full max-w-[343px] cursor-pointer transition-transform active:scale-[0.98] outline-none" + > + {/* Outer Card Container */} +
0 + ? "border border-[#ECA533]" + : "border border-[#EBEBEB]" + }`} + > +
+ {/* Header Row: Avatar + Info + Rating */} +
+ {/* Avatar with Status Dot */} +
+ {advisor.avatar_url ? ( + // eslint-disable-next-line @next/next/no-img-element + {advisor.fullname { + event.currentTarget.src = FALLBACK_AVATAR; + }} + /> + ) : ( +
+ + + +
+ )} + + {/* Status indicator dot badge (top-start) */} +
+
+
+
+
+
+ + {/* Consultant Info */} +
+
+ {advisor.is_ai && ( + + AI + + )} +

+ {advisor.fullname ?? advisor.username} +

+ {rating > 0 && ( +
+ + + + + {rating.toFixed(1)} + +
+ )} +
+

+ {advisor.slogan ?? ""} +

+
+
+ + {/* Contact Chips */} + {hasActiveContact && ( +
+ {hasChat && ( +
+ + + + {t["Text Message"] || "Text Message"} +
+ )} + + {hasVoice && ( +
+ + + + {t["Voice Call"] || "Voice Call"} +
+ )} + + {hasVideo && ( +
+ + + + {t["Video Call"] || "Video Call"} +
+ )} +
+ )} + + {/* Topics Badges */} + {topics.length > 0 && ( + <> +
+
+ {topics.map((topic) => ( + + {topic} + + ))} +
+ + )} +
+
+ + {/* Floating Unread Badge at Top Corner */} + {unreadCount > 0 && ( +
+ {unreadCount} + + {t["New Message"] || "New Message"} + +
+ )} +
+ ); +} + +function AdvisorsSkeleton() { + return ( + <> + {[0, 1, 2].map((index) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} + + ); } diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index f942805..e201c7b 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -1,5 +1,752 @@ -import NewMatchProfileClient from "@/components/Componentes/new-match-profile-client"; +"use client"; -export default function NewMatchProfilePage() { - return ; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; +import Button from "@/components/Componentes/button"; +import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet"; +import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet"; +import InformationSheet from "@/components/Componentes/information-sheet"; +import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; +import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot"; +import NavigationButton from "@/components/Componentes/navigation-button"; +import { PageBackground } from "@/components/Componentes/page-background"; +import StickyHeader from "@/components/Componentes/sticky-header"; +import type { + MarriageCaseStatus, + MarriageField, + MarriageFieldValue, + MarriageGender, + MarriagePhoneFieldValue, + MarriageProfileResponse, +} from "@/hooks/marriage/types"; +import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; +import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; +import { getSubmitPath } from "@/lib/get-submit-path"; +import { markMatchStarted } from "@/lib/match-start-grace"; +import { localizePath } from "@/translations/config"; +import { useI18n } from "@/translations/provider"; + +import { + formatFieldLabel, + formatFieldValue, + formatOptionValue, + getOrderedSummaryFields, + isMarriagePhoneFieldValue, + titleFromKey, +} from "@/lib/marriage-field-formatter"; + +function isImageField(field: MarriageField) { + return /(avatar|image|photo|picture|portrait|upload)/i.test( + `${field.key} ${field.label}`, + ); +} + +function isFirstNameField(field: MarriageField) { + const key = (field.key || "").toLowerCase(); + return ( + key === "first_name" || + key === "personal_identity.first_name" || + key.endsWith(".first_name") || + key.endsWith("_first_name") || + key === "full_name" || + key === "fullname" || + key === "q1_full_name" || + key.endsWith(".full_name") || + key.endsWith("_full_name") + ); +} + +function canAcceptProfile( + gender: MarriageGender | null | undefined, + status: MarriageCaseStatus | null | undefined, +) { + if (!gender || !status) { + return false; + } + + if (gender === "female") { + return status === "introduced" || status === "male_accepted"; + } + + return status === "introduced"; +} + +function MatchField({ + field, + dictionary, +}: { + field: MarriageField; + dictionary?: Record; +}) { + const value = formatOptionValue(field.value, dictionary); + + if (!value || isImageField(field)) { + return null; + } + + const label = formatFieldLabel(field, dictionary); + + return ( +
+

+ {label} +

+

+ {value} +

+
+ ); +} + +function MatchPublicProfileFields({ + publicInfo, + dictionary, +}: { + publicInfo: MarriageField[] | null | undefined; + dictionary?: Record; +}) { + const visibleFields = useMemo(() => { + if (!publicInfo) return []; + return publicInfo.filter((field) => { + if ( + field.value === null || + field.value === "" || + isImageField(field) || + isFirstNameField(field) + ) { + return false; + } + if ((field as any).private === true) { + return false; + } + return true; + }); + }, [publicInfo]); + + if (!visibleFields.length) { + return ( +
+

+ {dictionary?.["No public information is available to display."] || + "No public information is available to display."} +

+
+ ); + } + + return ( +
+ {visibleFields.map((field) => ( + + ))} +
+ ); +} + +function NewMatchProfileSkeleton({ + hideBackButton = false, + onClose, +}: { + hideBackButton?: boolean; + onClose?: () => void; +}) { + const { dictionary: t } = useI18n(); + + return ( + <> + +
+ +
+ {!hideBackButton ? ( + + ) : ( +
+ )} +

+ {t["More detail"]} +

+
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ + +
+
+
+ + ); +} + +function formatBoldText(text: string) { + if (!text) return ""; + const parts = text.split(/\*\*([^*]+)\*\*/g); + return parts.map((part, index) => { + if (index % 2 === 1) { + return ( + + {part} + + ); + } + return part; + }); +} + +type NewMatchProfilePageProps = { + onClose?: () => void; + profile?: MarriageProfileResponse; +}; + +export default function NewMatchProfilePage({ + onClose, + profile: profileProp, +}: NewMatchProfilePageProps = {}) { + const { dictionary: t, locale } = useI18n(); + const router = useRouter(); + const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false); + const [isFemaleConsentChecked, setIsFemaleConsentChecked] = useState(false); + const [isDeclineSheetOpen, setIsDeclineSheetOpen] = useState(false); + const [isMaleDeclineWarningOpen, setIsMaleDeclineWarningOpen] = + useState(false); + const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = + useState(false); + const { + data: queriedProfile, + isLoading: isQueryLoading, + refetch: refetchProfile, + } = useMarriageProfileQuery({ + enabled: !profileProp, + }); + + const profile = profileProp ?? queriedProfile; + const isLoading = !profileProp && isQueryLoading; + + useEffect(() => { + // If rendered as an overlay (onClose is passed), NEVER redirect away from current page + if (!profile || Boolean(onClose)) { + return; + } + const targetPath = getSubmitPath(profile); + const caseStatus = profile?.active_case?.status; + const isProfileMatched = profile?.status === "matched"; + const isViewingAllowed = + caseStatus === "female_accepted" || + caseStatus === "payment_pending" || + caseStatus === "payment_done" || + caseStatus === "contacted" || + caseStatus === "finalized" || + isProfileMatched; + + if ( + targetPath !== "/new-match" && + targetPath !== "/request-sent" && + targetPath !== "/request-accepted" && + !isViewingAllowed + ) { + router.replace(localizePath(targetPath, locale)); + } + }, [profile, locale, router, onClose]); + + const caseId = profile?.active_case?.case_id; + const caseStatus = profile?.active_case?.status; + const isFemaleProfile = profile?.gender === "female"; + const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { + onSuccess: async (data, variables) => { + onClose?.(); + if (variables.action === "accept") { + const nextPath = + profile?.gender === "female" + ? "/request-accepted" + : "/request-sent"; + router.replace(localizePath(nextPath, locale)); + return; + } + + router.replace(localizePath("/finding-match", locale)); + }, + }); + + const candidateName = useMemo(() => { + const publicInfo = profile?.match_summary?.public_info ?? []; + const firstNameIdx = publicInfo.findIndex( + (f) => + f.key === "personal_identity.first_name" || + f.key?.endsWith(".first_name") || + f.key === "first_name", + ); + const lastNameIdx = publicInfo.findIndex( + (f) => + f.key === "personal_identity.last_name" || + f.key?.endsWith(".last_name") || + f.key === "last_name", + ); + + if (firstNameIdx !== -1 && publicInfo[firstNameIdx].value) { + const firstName = formatFieldValue(publicInfo[firstNameIdx].value); + if (lastNameIdx !== -1 && publicInfo[lastNameIdx].value) { + const lastName = formatFieldValue(publicInfo[lastNameIdx].value); + return `${firstName} ${lastName}`.trim(); + } + return firstName || ""; + } + + const nameField = publicInfo.find( + (f) => + f.key === "q1_full_name" || + f.key.endsWith(".full_name") || + f.key.toLowerCase().includes("fullname") || + f.key.toLowerCase().includes("display_name"), + ); + const formatted = formatFieldValue(nameField?.value ?? null); + if (formatted) return formatted; + + const firstVal = publicInfo.find((f) => Boolean(f.value))?.value ?? null; + return ( + formatFieldValue(firstVal) || + (profile?.match_summary?.id + ? `Profile #${profile.match_summary.id}` + : "Profile") + ); + }, [profile?.match_summary]); + + const isCandidateFemale = + profile?.match_summary?.gender === "female" || profile?.gender === "male"; + const avatarSrc = isCandidateFemale + ? "/assets/images/female_avatar.svg" + : "/assets/images/Group 1597880481.png"; + + const isRedirecting = useMemo(() => { + if (!profile) return false; + const targetPath = getSubmitPath(profile); + const caseStatus = profile?.active_case?.status; + const isProfileMatched = profile?.status === "matched"; + const isViewingAllowed = + caseStatus === "payment_done" || + caseStatus === "contacted" || + caseStatus === "finalized" || + isProfileMatched; + + return ( + targetPath !== "/new-match" && + targetPath !== "/request-sent" && + !isViewingAllowed + ); + }, [profile]); + + const isSubmitting = respondMutation.isPending; + + if ((isLoading && !profile) || !profile) { + return ( + + ); + } + const isAcceptProfileEnabled = + Boolean(caseId) && + !isSubmitting && + canAcceptProfile(profile?.gender, caseStatus); + const isDeclineProfileEnabled = isAcceptProfileEnabled; + + const nameParts = candidateName.trim().split(/\s+/); + const _firstName = nameParts[0] || ""; + const _lastName = nameParts.slice(1).join(" ") || ""; + + const mainClass = "-mx-[17px] flex h-dvh flex-col overflow-hidden"; + + const mainStyle = { + backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, + backgroundColor: "#F5F5F5", + backgroundRepeat: "repeat-y", + backgroundSize: "100% auto", + }; + + return ( + <> + + {isRequestSheetOpen ? ( + isFemaleProfile ? ( + ( +
+ + +
+ + +
+
+ )} + onClose={() => { + setIsFemaleConsentChecked(false); + setIsRequestSheetOpen(false); + }} + /> + ) : ( + ( +
+ + +
+ )} + onClose={() => setIsRequestSheetOpen(false)} + /> + ) + ) : null} + {isDeclineSheetOpen ? ( + ( +
+ + +
+ )} + onClose={() => setIsDeclineSheetOpen(false)} + /> + ) : null} + {isMaleDeclineWarningOpen ? ( + ( +
+ + +
+ )} + onClose={() => setIsMaleDeclineWarningOpen(false)} + /> + ) : null} + {isDismissReasonSheetOpen ? ( + setIsDismissReasonSheetOpen(false)} + onSubmit={async (reason) => { + if (!caseId) { + return; + } + + await respondMutation.mutateAsync({ + action: "reject", + custom_note: reason, + }); + }} + /> + ) : null} + +
+ +
+ +

+ {t["More detail"]} +

+
+
+ + +
+
+ {/* Frame 2095586585 - Avatar Section */} +
+ {/* Avatar Group */} +
+ +
+ + {/* Frame 2095586663 - Names Section */} +
+ + {candidateName} + +
+
+
+ + +
+ +
+ {(Boolean(onClose) && !isAcceptProfileEnabled) || + caseStatus === "female_accepted" || + caseStatus === "payment_pending" || + caseStatus === "payment_done" || + caseStatus === "contacted" || + caseStatus === "finalized" || + profile?.status === "matched" ? ( + + ) : ( +
+ + +
+ )} +
+
+ + ); } diff --git a/src/components/Componentes/marriage-advisors-client.tsx b/src/components/Componentes/marriage-advisors-client.tsx deleted file mode 100644 index ef95114..0000000 --- a/src/components/Componentes/marriage-advisors-client.tsx +++ /dev/null @@ -1,373 +0,0 @@ -"use client"; - -import { useState } from "react"; -import Image from "next/image"; -import { Ic } from "@/icons"; -import StickyHeader from "@/components/Componentes/sticky-header"; -import NavigationButton from "@/components/Componentes/navigation-button"; -import { PageBackground } from "@/components/Componentes/page-background"; -import SupportSheet from "@/components/Componentes/support-sheet"; -import { useI18n } from "@/translations/provider"; -import { - useMarriageAdvisorsQuery, - type MarriageConsultant, -} from "@/hooks/marriage/use-marriage-advisors"; -import { postActionToFlutter } from "@/lib/webview-actions"; - -const FALLBACK_AVATAR = "/assets/images/Avatar Image.png"; - -type MarriageAdvisorsClientProps = { - onClose?: () => void; -}; - -export default function MarriageAdvisorsClient({ - onClose, -}: MarriageAdvisorsClientProps = {}) { - const { dictionary: t } = useI18n(); - const [isSupportOpen, setIsSupportOpen] = useState(false); - - const { data, isLoading, isError, refetch } = useMarriageAdvisorsQuery(); - const advisors = data?.results ?? []; - - const mainStyle = { - backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, - backgroundColor: "#F5F5F5", - backgroundRepeat: "repeat-y", - backgroundSize: "100% auto", - }; - - // Opens the native Talk consultant page; the Flutter side fetches the - // consultant by username, so only the bare username is needed. Outside the - // WebView (plain browser) postActionToFlutter is a no-op returning false. - const openConsultantPage = (username: string) => { - postActionToFlutter("open_consultant_page", { consultant: username }); - }; - - return ( - <> - -
- -
- -

- {t["Marriage advisors"]} -

-
-
- - -
- {isLoading && } - - {isError && ( -
-

- {t["Failed to load advisors."]} -

- -
- )} - - {!isLoading && !isError && advisors.length === 0 && ( -
-

- {t["No advisors are currently available."]} -

-
- )} - - {advisors.map((advisor) => ( - openConsultantPage(advisor.username)} - /> - ))} -
-
- - setIsSupportOpen(false)} - /> - - ); -} - -function AdvisorCard({ - advisor, - onOpen, -}: { - advisor: MarriageConsultant; - onOpen: () => void; -}) { - const { dictionary: t, locale } = useI18n(); - const isRtl = locale === "fa" || locale === "ar" || locale === "ur"; - const unreadCount = advisor.unread_count ?? 0; - const isOnline = advisor.status === "online"; - const rating = Number(advisor.avg_rate ?? 0); - const hasChat = advisor.contact_type?.includes("chat"); - const hasVoice = advisor.contact_type?.includes("voice"); - const hasVideo = advisor.contact_type?.includes("video"); - const hasActiveContact = hasChat || hasVoice || hasVideo; - const topics = advisor.topics?.filter(Boolean) ?? []; - - return ( -
{ - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - onOpen(); - } - }} - className="relative w-full max-w-[343px] cursor-pointer transition-transform active:scale-[0.98] outline-none" - > - {/* Outer Card Container */} -
0 - ? "border border-[#ECA533]" - : "border border-[#EBEBEB]" - }`} - > -
- {/* Header Row: Avatar + Info + Rating */} -
- {/* Avatar with Status Dot */} -
- {advisor.avatar_url ? ( - // eslint-disable-next-line @next/next/no-img-element - {advisor.fullname { - event.currentTarget.src = FALLBACK_AVATAR; - }} - /> - ) : ( -
- - - -
- )} - - {/* Status indicator dot badge (top-start) */} -
-
-
-
-
-
- - {/* Consultant Info */} -
-
- {advisor.is_ai && ( - - AI - - )} -

- {advisor.fullname ?? advisor.username} -

- {rating > 0 && ( -
- - - - - {rating.toFixed(1)} - -
- )} -
-

- {advisor.slogan ?? ""} -

-
-
- - {/* Contact Chips */} - {hasActiveContact && ( -
- {hasChat && ( -
- - - - {t["Text Message"] || "Text Message"} -
- )} - - {hasVoice && ( -
- - - - {t["Voice Call"] || "Voice Call"} -
- )} - - {hasVideo && ( -
- - - - {t["Video Call"] || "Video Call"} -
- )} -
- )} - - {/* Topics Badges */} - {topics.length > 0 && ( - <> -
-
- {topics.map((topic) => ( - - {topic} - - ))} -
- - )} -
-
- - {/* Floating Unread Badge at Top Corner */} - {unreadCount > 0 && ( -
- {unreadCount} - - {t["New Message"] || "New Message"} - -
- )} -
- ); -} - -function AdvisorsSkeleton() { - return ( - <> - {[0, 1, 2].map((index) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} - - ); -} diff --git a/src/components/Componentes/marriage-advisors-overlay.tsx b/src/components/Componentes/marriage-advisors-overlay.tsx index 11b81a1..54e4753 100644 --- a/src/components/Componentes/marriage-advisors-overlay.tsx +++ b/src/components/Componentes/marriage-advisors-overlay.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import MarriageAdvisorsClient from "@/components/Componentes/marriage-advisors-client"; +import MarriageAdvisorsPage from "@/app/marriage-advisors/page"; import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; @@ -69,7 +69,7 @@ export function MarriageAdvisorsOverlay({ }: MarriageAdvisorsOverlayProps) { return ( - + ); } diff --git a/src/components/Componentes/match-profile-overlay.tsx b/src/components/Componentes/match-profile-overlay.tsx index 9c29fec..d940ee2 100644 --- a/src/components/Componentes/match-profile-overlay.tsx +++ b/src/components/Componentes/match-profile-overlay.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import NewMatchProfileClient from "@/components/Componentes/new-match-profile-client"; +import NewMatchProfilePage from "@/app/new-match/profile/page"; import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import type { MarriageProfileResponse } from "@/hooks/marriage/types"; @@ -72,7 +72,7 @@ export function MatchProfileOverlay({ }: MatchProfileOverlayProps) { return ( - + ); } diff --git a/src/components/Componentes/new-match-profile-client.tsx b/src/components/Componentes/new-match-profile-client.tsx deleted file mode 100644 index 776706c..0000000 --- a/src/components/Componentes/new-match-profile-client.tsx +++ /dev/null @@ -1,752 +0,0 @@ -"use client"; - -import Image from "next/image"; -import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; -import Button from "@/components/Componentes/button"; -import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet"; -import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet"; -import InformationSheet from "@/components/Componentes/information-sheet"; -import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; -import { LoadingThreeDot } from "@/components/Componentes/loading-three-dot"; -import NavigationButton from "@/components/Componentes/navigation-button"; -import { PageBackground } from "@/components/Componentes/page-background"; -import StickyHeader from "@/components/Componentes/sticky-header"; -import type { - MarriageCaseStatus, - MarriageField, - MarriageFieldValue, - MarriageGender, - MarriagePhoneFieldValue, - MarriageProfileResponse, -} from "@/hooks/marriage/types"; -import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; -import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; -import { getSubmitPath } from "@/lib/get-submit-path"; -import { markMatchStarted } from "@/lib/match-start-grace"; -import { localizePath } from "@/translations/config"; -import { useI18n } from "@/translations/provider"; - -import { - formatFieldLabel, - formatFieldValue, - formatOptionValue, - getOrderedSummaryFields, - isMarriagePhoneFieldValue, - titleFromKey, -} from "@/lib/marriage-field-formatter"; - -function isImageField(field: MarriageField) { - return /(avatar|image|photo|picture|portrait|upload)/i.test( - `${field.key} ${field.label}`, - ); -} - -function isFirstNameField(field: MarriageField) { - const key = (field.key || "").toLowerCase(); - return ( - key === "first_name" || - key === "personal_identity.first_name" || - key.endsWith(".first_name") || - key.endsWith("_first_name") || - key === "full_name" || - key === "fullname" || - key === "q1_full_name" || - key.endsWith(".full_name") || - key.endsWith("_full_name") - ); -} - -function canAcceptProfile( - gender: MarriageGender | null | undefined, - status: MarriageCaseStatus | null | undefined, -) { - if (!gender || !status) { - return false; - } - - if (gender === "female") { - return status === "introduced" || status === "male_accepted"; - } - - return status === "introduced"; -} - -function MatchField({ - field, - dictionary, -}: { - field: MarriageField; - dictionary?: Record; -}) { - const value = formatOptionValue(field.value, dictionary); - - if (!value || isImageField(field)) { - return null; - } - - const label = formatFieldLabel(field, dictionary); - - return ( -
-

- {label} -

-

- {value} -

-
- ); -} - -function MatchPublicProfileFields({ - publicInfo, - dictionary, -}: { - publicInfo: MarriageField[] | null | undefined; - dictionary?: Record; -}) { - const visibleFields = useMemo(() => { - if (!publicInfo) return []; - return publicInfo.filter((field) => { - if ( - field.value === null || - field.value === "" || - isImageField(field) || - isFirstNameField(field) - ) { - return false; - } - if ((field as any).private === true) { - return false; - } - return true; - }); - }, [publicInfo]); - - if (!visibleFields.length) { - return ( -
-

- {dictionary?.["No public information is available to display."] || - "No public information is available to display."} -

-
- ); - } - - return ( -
- {visibleFields.map((field) => ( - - ))} -
- ); -} - -function NewMatchProfileSkeleton({ - hideBackButton = false, - onClose, -}: { - hideBackButton?: boolean; - onClose?: () => void; -}) { - const { dictionary: t } = useI18n(); - - return ( - <> - -
- -
- {!hideBackButton ? ( - - ) : ( -
- )} -

- {t["More detail"]} -

-
-
- - -
-
- - -
- -
- - -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
-
-
- -
-
- - -
-
-
- - ); -} - -function formatBoldText(text: string) { - if (!text) return ""; - const parts = text.split(/\*\*([^*]+)\*\*/g); - return parts.map((part, index) => { - if (index % 2 === 1) { - return ( - - {part} - - ); - } - return part; - }); -} - -type NewMatchProfileClientProps = { - onClose?: () => void; - profile?: MarriageProfileResponse; -}; - -export default function NewMatchProfileClient({ - onClose, - profile: profileProp, -}: NewMatchProfileClientProps = {}) { - const { dictionary: t, locale } = useI18n(); - const router = useRouter(); - const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false); - const [isFemaleConsentChecked, setIsFemaleConsentChecked] = useState(false); - const [isDeclineSheetOpen, setIsDeclineSheetOpen] = useState(false); - const [isMaleDeclineWarningOpen, setIsMaleDeclineWarningOpen] = - useState(false); - const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = - useState(false); - const { - data: queriedProfile, - isLoading: isQueryLoading, - refetch: refetchProfile, - } = useMarriageProfileQuery({ - enabled: !profileProp, - }); - - const profile = profileProp ?? queriedProfile; - const isLoading = !profileProp && isQueryLoading; - - useEffect(() => { - // If rendered as an overlay (onClose is passed), NEVER redirect away from current page - if (!profile || Boolean(onClose)) { - return; - } - const targetPath = getSubmitPath(profile); - const caseStatus = profile?.active_case?.status; - const isProfileMatched = profile?.status === "matched"; - const isViewingAllowed = - caseStatus === "female_accepted" || - caseStatus === "payment_pending" || - caseStatus === "payment_done" || - caseStatus === "contacted" || - caseStatus === "finalized" || - isProfileMatched; - - if ( - targetPath !== "/new-match" && - targetPath !== "/request-sent" && - targetPath !== "/request-accepted" && - !isViewingAllowed - ) { - router.replace(localizePath(targetPath, locale)); - } - }, [profile, locale, router, onClose]); - - const caseId = profile?.active_case?.case_id; - const caseStatus = profile?.active_case?.status; - const isFemaleProfile = profile?.gender === "female"; - const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { - onSuccess: async (data, variables) => { - onClose?.(); - if (variables.action === "accept") { - const nextPath = - profile?.gender === "female" - ? "/request-accepted" - : "/request-sent"; - router.replace(localizePath(nextPath, locale)); - return; - } - - router.replace(localizePath("/finding-match", locale)); - }, - }); - - const candidateName = useMemo(() => { - const publicInfo = profile?.match_summary?.public_info ?? []; - const firstNameIdx = publicInfo.findIndex( - (f) => - f.key === "personal_identity.first_name" || - f.key?.endsWith(".first_name") || - f.key === "first_name", - ); - const lastNameIdx = publicInfo.findIndex( - (f) => - f.key === "personal_identity.last_name" || - f.key?.endsWith(".last_name") || - f.key === "last_name", - ); - - if (firstNameIdx !== -1 && publicInfo[firstNameIdx].value) { - const firstName = formatFieldValue(publicInfo[firstNameIdx].value); - if (lastNameIdx !== -1 && publicInfo[lastNameIdx].value) { - const lastName = formatFieldValue(publicInfo[lastNameIdx].value); - return `${firstName} ${lastName}`.trim(); - } - return firstName || ""; - } - - const nameField = publicInfo.find( - (f) => - f.key === "q1_full_name" || - f.key.endsWith(".full_name") || - f.key.toLowerCase().includes("fullname") || - f.key.toLowerCase().includes("display_name"), - ); - const formatted = formatFieldValue(nameField?.value ?? null); - if (formatted) return formatted; - - const firstVal = publicInfo.find((f) => Boolean(f.value))?.value ?? null; - return ( - formatFieldValue(firstVal) || - (profile?.match_summary?.id - ? `Profile #${profile.match_summary.id}` - : "Profile") - ); - }, [profile?.match_summary]); - - const isCandidateFemale = - profile?.match_summary?.gender === "female" || profile?.gender === "male"; - const avatarSrc = isCandidateFemale - ? "/assets/images/female_avatar.svg" - : "/assets/images/Group 1597880481.png"; - - const isRedirecting = useMemo(() => { - if (!profile) return false; - const targetPath = getSubmitPath(profile); - const caseStatus = profile?.active_case?.status; - const isProfileMatched = profile?.status === "matched"; - const isViewingAllowed = - caseStatus === "payment_done" || - caseStatus === "contacted" || - caseStatus === "finalized" || - isProfileMatched; - - return ( - targetPath !== "/new-match" && - targetPath !== "/request-sent" && - !isViewingAllowed - ); - }, [profile]); - - const isSubmitting = respondMutation.isPending; - - if ((isLoading && !profile) || !profile) { - return ( - - ); - } - const isAcceptProfileEnabled = - Boolean(caseId) && - !isSubmitting && - canAcceptProfile(profile?.gender, caseStatus); - const isDeclineProfileEnabled = isAcceptProfileEnabled; - - const nameParts = candidateName.trim().split(/\s+/); - const _firstName = nameParts[0] || ""; - const _lastName = nameParts.slice(1).join(" ") || ""; - - const mainClass = "-mx-[17px] flex h-dvh flex-col overflow-hidden"; - - const mainStyle = { - backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, - backgroundColor: "#F5F5F5", - backgroundRepeat: "repeat-y", - backgroundSize: "100% auto", - }; - - return ( - <> - - {isRequestSheetOpen ? ( - isFemaleProfile ? ( - ( -
- - -
- - -
-
- )} - onClose={() => { - setIsFemaleConsentChecked(false); - setIsRequestSheetOpen(false); - }} - /> - ) : ( - ( -
- - -
- )} - onClose={() => setIsRequestSheetOpen(false)} - /> - ) - ) : null} - {isDeclineSheetOpen ? ( - ( -
- - -
- )} - onClose={() => setIsDeclineSheetOpen(false)} - /> - ) : null} - {isMaleDeclineWarningOpen ? ( - ( -
- - -
- )} - onClose={() => setIsMaleDeclineWarningOpen(false)} - /> - ) : null} - {isDismissReasonSheetOpen ? ( - setIsDismissReasonSheetOpen(false)} - onSubmit={async (reason) => { - if (!caseId) { - return; - } - - await respondMutation.mutateAsync({ - action: "reject", - custom_note: reason, - }); - }} - /> - ) : null} - -
- -
- -

- {t["More detail"]} -

-
-
- - -
-
- {/* Frame 2095586585 - Avatar Section */} -
- {/* Avatar Group */} -
- -
- - {/* Frame 2095586663 - Names Section */} -
- - {candidateName} - -
-
-
- - -
- -
- {(Boolean(onClose) && !isAcceptProfileEnabled) || - caseStatus === "female_accepted" || - caseStatus === "payment_pending" || - caseStatus === "payment_done" || - caseStatus === "contacted" || - caseStatus === "finalized" || - profile?.status === "matched" ? ( - - ) : ( -
- - -
- )} -
-
- - ); -} diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index 1ef1223..44a3210 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -832,7 +832,6 @@ "Subscription required": "Subscription required", "To view profiles, you need a subscription. Each subscription includes up to {count} matches.": "To view profiles, you need a subscription. Each subscription includes up to {count} matches.", "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users": "To view profiles, a subscription is required. This helps cover our support and matching services and applies only to male users.", - "To view profiles, a subscription is required. This helps cover our advisory and matching services and applies only to male users": "To view profiles, a subscription is required. This helps cover our advisory and matching services and applies only to male users.", "{count} case introductions remaining of your subscription.": "{count} case introductions remaining of your subscription.", "{days} days and {count} case introductions remaining of your subscription.": "{days} days and {count} case introductions remaining of your subscription.", "You do not have enough Habib Coins": "You do not have enough Habib Coins",