Browse Source

feat: implement candidate contact flow with status management, outcome sheets, and redirects

front-test-2
ghorbani 3 weeks ago
parent
commit
bcaa4dac4d
  1. 9
      .agents/AGENTS.md
  2. 24
      src/app/candidate-contact/page.tsx
  3. 21
      src/app/finding-match/page.tsx
  4. 16
      src/app/new-match/page.tsx
  5. 20
      src/app/new-match/profile/page.tsx
  6. 30
      src/app/request-accepted/page.tsx
  7. 33
      src/app/request-sent/page.tsx

9
.agents/AGENTS.md

@ -10,3 +10,12 @@ This version has breaking changes — APIs, conventions, and file structure may
- Registration form structure data is kept separately in `src/data/questions/`.
- CRITICAL AUTOMATION RULE: Whenever any new static text, data, field, component label, tooltip, help description, key, or text item is added or updated in the project, its translation key and values MUST automatically be added/updated in ALL 24 locale JSON files (`src/translations/locales/` & `src/data/questions/`).
- QUESTION HELP & TOOLTIPS RULE: All question help texts, tooltips, and modal descriptions must be stored with key-value entries in the respective locale JSON files, ensuring complete synchronization across all supported languages.
# Logic and UI Language Separation Rule (Language-Agnostic Contract)
- **Goal**: Create a language-agnostic contract between frontend and backend for field mappings and logic.
- **Strict Logic Isolation**: Program logic (e.g. conditional visibility, backend validations, validations, and step progressions) MUST NEVER depend on localized UI strings/labels (e.g., Farsi or Arabic text).
- **System Keys Usage**: All business logic and API payloads must rely exclusively on fixed English identifiers called **System Keys** (e.g., `email`, `phone_number`, `marital_status`).
- **Frontend Role**: When submitting data, the frontend must send values associated only with these fixed System Keys. The localized view the user interacts with (Persian, Arabic, Turkish, etc.) is strictly for presentation.
- **Backend Role**: All DB operations, validation rules, condition checks, and calculations must be run exclusively on the System Keys. The backend must never receive, process, or validate display labels.
- **i18n Mapping**: Localization/localization translation is handled solely in the frontend presentation layer via translation keys/files (e.g., `en.json`, `fa.json`, `ar.json`) mapping back to the same System Keys.

24
src/app/candidate-contact/page.tsx

@ -2,7 +2,7 @@
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { DotsLoader } from "@/components/Componentes/button";
import CallResultSheet from "@/components/Componentes/call-result-sheet";
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
@ -48,6 +48,11 @@ export default function CandidateContactPage() {
}
}, [profile, router, locale]);
const isRedirecting = useMemo(() => {
if (!profile) return false;
return getSubmitPath(profile) !== "/candidate-contact";
}, [profile]);
const caseId = profile?.active_case?.case_id;
const caseStatus = profile?.active_case?.status;
const isFemale = profile?.gender === "female";
@ -69,7 +74,7 @@ export default function CandidateContactPage() {
});
};
if (isProfileLoading) {
if (isProfileLoading || isRedirecting) {
return (
<>
<PageBackground />
@ -237,7 +242,7 @@ export default function CandidateContactPage() {
)}
</section>
<div className="space-y-4">
<div className="space-y-4 pb-20">
{/* Advisor section */}
<AdvisorActionsCard
title={t.findingMatch.advisorTitle}
@ -249,18 +254,23 @@ export default function CandidateContactPage() {
/>
{/* Profile Locked banner */}
<section className="flex flex-col items-center text-center">
<div className="flex items-center justify-center py-[17px] rounded-[15px] gap-2 w-full bg-[#E0E0E0]">
<section className="pointer-events-none fixed bottom-0 left-1/2 z-20 w-full sm:max-w-[375px] -translate-x-1/2 bg-[#F5F5F5]">
<div
style={{ paddingBottom: "calc(20px + var(--safe-bottom))" }}
className="pointer-events-auto px-[17px] pt-3"
>
<div className="flex items-center justify-center w-full gap-1 rounded-[11px] bg-[#DBDBDB] py-3.5">
<Image
src="/assets/images/material-symbols_lock.svg"
width={16}
height={16}
width={24}
height={24}
alt="lock"
/>
<h2 className="group-14 leading-none font-semibold text-[#747474]">
{t.requestSent.profileLocked}
</h2>
</div>
</div>
</section>
</div>
</div>

21
src/app/finding-match/page.tsx

@ -3,7 +3,8 @@
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { DotsLoader } from "@/components/Componentes/button";
import { FaLock, FaPen } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import PageHeader from "@/components/Componentes/page-header";
@ -22,7 +23,7 @@ const advisorAvatars = [
export default function FindingMatchPage() {
const router = useRouter();
const { dictionary: t, locale } = useI18n();
const { data: profile } = useMarriageProfileQuery({
const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000,
});
@ -36,6 +37,22 @@ export default function FindingMatchPage() {
}
}, [profile, locale, router]);
const isRedirecting = useMemo(() => {
if (!profile) return false;
return getSubmitPath(profile) !== "/finding-match";
}, [profile]);
if (isLoading || isRedirecting) {
return (
<>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(16px,calc(var(--safe-top)+8px))] items-center justify-center">
<DotsLoader />
</main>
</>
);
}
const copy = t.findingMatch;
const matchImageSrc = "/assets/images/Group 1597880466.svg";

16
src/app/new-match/page.tsx

@ -271,6 +271,22 @@ export default function NewMatchPage() {
}
}, [profile, locale, router]);
const isRedirecting = useMemo(() => {
if (!profile) return false;
return getSubmitPath(profile) !== "/new-match";
}, [profile]);
if (isLoading || isRedirecting) {
return (
<>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(16px,calc(var(--safe-top)+8px))] items-center justify-center">
<DotsLoader />
</main>
</>
);
}
const matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary);
const pairedFields = [matchDisplay.age, matchDisplay.city].filter(

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

@ -341,7 +341,25 @@ export default function NewMatchProfilePage() {
? "/assets/images/female_avatar.svg"
: "/assets/images/Group 1597880481.png";
if (isLoading || !profile) {
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]);
if (isLoading || !profile || isRedirecting) {
return <NewMatchProfileSkeleton />;
}

30
src/app/request-accepted/page.tsx

@ -3,7 +3,7 @@
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import { DotsLoader } from "@/components/Componentes/button";
import CallResultSheet from "@/components/Componentes/call-result-sheet";
@ -177,7 +177,7 @@ export default function RequestAcceptedPage() {
const [isSubscriptionSheetOpen, setIsSubscriptionSheetOpen] = useState(false);
const [isOutcomeSheetOpen, setIsOutcomeSheetOpen] = useState(false);
const profileHref = localizePath("/new-match/profile", locale);
const { data: profile } = useMarriageProfileQuery({
const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000,
});
@ -191,6 +191,11 @@ export default function RequestAcceptedPage() {
}
}, [profile, router, locale]);
const isRedirecting = useMemo(() => {
if (!profile) return false;
return getSubmitPath(profile) !== "/request-accepted";
}, [profile]);
const isFemaleProfile = profile?.gender === "female";
const caseId = profile?.active_case?.case_id;
const caseStatus = profile?.active_case?.status;
@ -208,6 +213,18 @@ export default function RequestAcceptedPage() {
const contactInfoQuery = useMarriageContactInfoQuery(caseId, {
enabled: false,
});
if (isLoading || isRedirecting) {
return (
<>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(16px,calc(var(--safe-top)+8px))] items-center justify-center">
<DotsLoader />
</main>
</>
);
}
const titleText = isFemaleProfile
? t.requestAccepted.titleFemale
: caseStatus === "payment_done" || caseStatus === "contacted"
@ -494,7 +511,7 @@ export default function RequestAcceptedPage() {
)}
</section>
<div className="space-y-8">
<div className="space-y-8 pb-20">
{/* Advisor section */}
<AdvisorActionsCard
title={t.findingMatch.advisorTitle}
@ -505,7 +522,11 @@ export default function RequestAcceptedPage() {
getAdvisorHref="/questions-list"
/>
<section className="flex flex-col items-center text-center">
<section className="pointer-events-none fixed bottom-0 left-1/2 z-20 w-full sm:max-w-[375px] -translate-x-1/2 bg-[#F5F5F5]">
<div
style={{ paddingBottom: "calc(20px + var(--safe-bottom))" }}
className="pointer-events-auto px-[17px] pt-3"
>
<div className="flex items-center justify-center w-full gap-1 rounded-[11px] bg-[#DBDBDB] py-3.5">
<Image
src="/assets/images/material-symbols_lock.svg"
@ -518,6 +539,7 @@ export default function RequestAcceptedPage() {
{t.requestAccepted.profileLocked}
</h2>
</div>
</div>
</section>
</div>
</div>

33
src/app/request-sent/page.tsx

@ -3,7 +3,8 @@
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useEffect, useMemo } from "react";
import { DotsLoader } from "@/components/Componentes/button";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background";
@ -21,7 +22,7 @@ const advisorAvatars = [
export default function RequestSentPage() {
const router = useRouter();
const { dictionary: t, locale } = useI18n();
const { data: profile } = useMarriageProfileQuery({
const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000,
});
@ -35,6 +36,22 @@ export default function RequestSentPage() {
}
}, [profile, locale, router]);
const isRedirecting = useMemo(() => {
if (!profile) return false;
return getSubmitPath(profile) !== "/request-sent";
}, [profile]);
if (isLoading || isRedirecting) {
return (
<>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-[max(16px,calc(var(--safe-top)+8px))] items-center justify-center">
<DotsLoader />
</main>
</>
);
}
const copy = t.findingMatch;
const requestSentCopy = (t as any).requestSent ?? {
title: "درخواست ارسال شد",
@ -85,7 +102,7 @@ export default function RequestSentPage() {
</Link>
</section>
<div className="space-y-8">
<div className="space-y-8 pb-20">
<AdvisorActionsCard
title={copy.advisorTitle}
description={copy.advisorDescription}
@ -95,19 +112,23 @@ export default function RequestSentPage() {
getAdvisorHref="/questions-list"
/>
<section className="flex flex-col items-center text-center">
<div className="flex items-center justify-center py-3.5 rounded-[11px] gap-1 w-full bg-[#DBDBDB]">
<section className="pointer-events-none fixed bottom-0 left-1/2 z-20 w-full sm:max-w-[375px] -translate-x-1/2 bg-[#F5F5F5]">
<div
style={{ paddingBottom: "calc(20px + var(--safe-bottom))" }}
className="pointer-events-auto px-[17px] pt-3"
>
<div className="flex items-center justify-center w-full gap-1 rounded-[11px] bg-[#DBDBDB] py-3.5">
<Image
src={"/assets/images/material-symbols_lock.svg"}
width={24}
height={24}
alt="lock"
/>
<h2 className="group-14 leading-none font-semibold text-[#747474]">
{requestSentCopy.profileLocked}
</h2>
</div>
</div>
</section>
</div>
</div>

Loading…
Cancel
Save