- {isLoading ? (
-
- Loading match summary...
-
- ) : isError ? (
-
- Unable to load match summary.
+
+
+
+ {t.common.appName}
+
+
+
+
+
+
+
+
+ YOU HAVE A NEW MATCH!
+
+ A matching profile has been found. Information is provided by the
+ girl's family or introducers. If you approve, we'll
+ share your profile with her family
- ) : matchSummary ? (
- <>
-
- Name:
- {matchDisplay.name}
-
-
-
- {matchDisplay.occupation ? (
-
- ) : null}
-
- {pairedFields.length ? (
-
- {pairedFields.map((field, index) => (
-
- {index > 0 ? | : null}
-
- {field.label}: {field.value}
-
-
+
+
+
+ {isLoading ? (
+
+ Loading match summary...
+
+ ) : isError ? (
+
+ Unable to load match summary.
+
+ ) : matchSummary ? (
+ <>
+
+ Name:
+ {matchDisplay.name}
+
+
+
+ {matchDisplay.occupation ? (
+
+ ) : null}
+
+ {pairedFields.length ? (
+
+ {pairedFields.map((field, index) => (
+
+ {index > 0 ? | : null}
+
+ {field.label}: {field.value}
+
+
+ ))}
+
+ ) : null}
+
+ {matchDisplay.maritalStatus ? (
+
+ ) : null}
+ {matchDisplay.cityPreference ? (
+
+ ) : null}
+ {matchDisplay.extraFields.map((field) => (
+
))}
-
- ) : null}
-
- {matchDisplay.maritalStatus ? (
-
- ) : null}
- {matchDisplay.cityPreference ? (
-
- ) : null}
- {matchDisplay.extraFields.map((field) => (
-
- ))}
+
+
+
+ View Profile
+
+ >
+ ) : (
+
+ No match summary is available yet.
+
+ )}
+
+
+
+
+
+
+
+
+ Profile locked
+
+
-
-
- View Profile
-
- >
- ) : (
-
- No match summary is available yet.
-
- )}
-
-
-
-
- {t.findingMatch.advisorTitle}
-
-
- {t.findingMatch.advisorDescription}
-
-
-
-
-
-
-
-
-
-
- Profile locked
-
-
diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx
index e83e737..6bca6ae 100644
--- a/src/app/new-match/profile/page.tsx
+++ b/src/app/new-match/profile/page.tsx
@@ -5,13 +5,16 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import Button from "@/components/ui/button";
import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet";
+import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import InformationSheet from "@/components/ui/information-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
-import {
- type MarriageField,
- type MarriageFieldValue,
+import type {
+ MarriageCaseStatus,
+ MarriageField,
+ MarriageFieldValue,
+ MarriageGender,
} from "@/hooks/marriage/types";
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
@@ -45,6 +48,21 @@ function isImageField(field: MarriageField) {
);
}
+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 }: { field: MarriageField }) {
const value = formatFieldValue(field.value);
@@ -66,12 +84,15 @@ export default function NewMatchProfilePage() {
const { dictionary: t, locale } = useI18n();
const router = useRouter();
const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false);
+ const [isFemaleConsentChecked, setIsFemaleConsentChecked] = useState(false);
const [isRejectSheetOpen, setIsRejectSheetOpen] = useState(false);
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] =
useState(false);
const { data: profile } = useMarriageProfileQuery();
const caseId = profile?.active_case?.case_id;
- const isMaleAccepted = profile?.active_case?.status === "male_accepted";
+ const caseStatus = profile?.active_case?.status;
+ const isFemaleProfile = profile?.gender === "female";
+ const isMaleAccepted = caseStatus === "male_accepted";
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", {
onSuccess: async (_, variables) => {
if (variables.action === "accept") {
@@ -84,42 +105,114 @@ export default function NewMatchProfilePage() {
});
const isSubmitting = respondMutation.isPending;
+ const isAcceptProfileEnabled =
+ Boolean(caseId) &&
+ !isSubmitting &&
+ canAcceptProfile(profile?.gender, caseStatus);
return (
<>
{isRequestSheetOpen ? (
-
(
-
-
- {t.common.cancel}
-
-
{
- close();
- if (!caseId) {
- return;
- }
+ isFemaleProfile ? (
+ (
+
+
setIsFemaleConsentChecked((value) => !value)}
+ className="flex w-full items-start gap-4 rounded-[15px] border-2 border-white bg-white/50 px-5 py-5 text-left shadow-[0_8px_24px_rgba(0,0,0,0.05)]"
+ >
+
+ {isFemaleConsentChecked ? (
+
+ ) : null}
+
+
+ I confirm that the female candidate and her family have
+ reviewed this profile and tentatively agree to further
+ communication
+
+
- if (isMaleAccepted) {
- return;
- }
+
+
+ {t.common.cancel}
+
+ {
+ close();
+ if (!isAcceptProfileEnabled) {
+ return;
+ }
- await respondMutation.mutateAsync({ action: "accept" });
- }}
- >
- {t.common.confirm}
-
-
- )}
- onClose={() => setIsRequestSheetOpen(false)}
- />
+ await respondMutation.mutateAsync({ action: "accept" });
+ }}
+ >
+ {t.common.confirm}
+
+
+
+ )}
+ onClose={() => {
+ setIsFemaleConsentChecked(false);
+ setIsRequestSheetOpen(false);
+ }}
+ />
+ ) : (
+ (
+
+
+ {t.common.cancel}
+
+ {
+ close();
+ if (!isAcceptProfileEnabled) {
+ return;
+ }
+
+ await respondMutation.mutateAsync({ action: "accept" });
+ }}
+ >
+ {t.common.confirm}
+
+
+ )}
+ onClose={() => setIsRequestSheetOpen(false)}
+ />
+ )
) : null}
{isRejectSheetOpen ? (
{
- if (isMaleAccepted) {
+ if (!isAcceptProfileEnabled) {
return;
}
+ if (isFemaleProfile) {
+ setIsFemaleConsentChecked(false);
+ }
+
setIsRequestSheetOpen(true);
}}
className="inline-flex w-2/3 whitespace-nowrap items-center justify-center gap-1 rounded-[12px] bg-[#F0445B] px-4 py-[13px] text-[16px] font-semibold text-white shadow-[0_8px_16px_rgba(240,68,91,0.24)] disabled:cursor-not-allowed disabled:opacity-50"
diff --git a/src/app/questions-list/[slug]/page.tsx b/src/app/questions-list/[slug]/page.tsx
index d2c909a..2d40cf3 100644
--- a/src/app/questions-list/[slug]/page.tsx
+++ b/src/app/questions-list/[slug]/page.tsx
@@ -7,6 +7,7 @@ import QuestionDropdown from "@/components/questions/question-dropdown";
import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button";
import QuestionFile from "@/components/questions/question-file";
import QuestionNumber from "@/components/questions/question-number";
+import QuestionPhone from "@/components/questions/question-phone";
import QuestionPhoto from "@/components/questions/question-photo";
import QuestionRadio from "@/components/questions/question-radio";
import QuestionSectionFlow from "@/components/questions/question-section-flow";
@@ -59,6 +60,10 @@ function renderQuestion(question: QuestionField, questionIndex: number) {
return (
);
+ case "phone":
+ return (
+
+ );
case "photo":
return (
diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx
index 6b86d8f..dafc4de 100644
--- a/src/app/questions-list/page.tsx
+++ b/src/app/questions-list/page.tsx
@@ -3,13 +3,17 @@
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
+import { IoClose } from "react-icons/io5";
import QuestionCard from "@/components/questions/question-card";
import RequiredStepsCard from "@/components/questions/required-steps-card";
import Button from "@/components/ui/button";
import InformationSheet from "@/components/ui/information-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
-import { getQuestionListItems } from "@/data/question-data";
+import {
+ getQuestionListItems,
+ type QuestionListItem,
+} from "@/data/question-data";
import { useStartMarriageMatchMutation } from "@/hooks/marriage/use-match-start";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections";
@@ -28,6 +32,9 @@ export default function QuestionsListPage() {
},
});
const [isOptionalInfoSheetOpen, setIsOptionalInfoSheetOpen] = useState(false);
+ const [selectedSection, setSelectedSection] = useState(
+ null,
+ );
const questionListItems = getQuestionListItems(locale);
const allRequiredSectionsCompleted = useMemo(() => {
if (!sections?.length) {
@@ -106,6 +113,33 @@ export default function QuestionsListPage() {
)}
/>
) : null}
+ {selectedSection ? (
+ (
+
+
+ {selectedSection.title}
+
+
+
+
+
+ )}
+ description={
+
+ {selectedSection.summary}
+
+ }
+ onClose={() => setSelectedSection(null)}
+ className="text-left"
+ />
+ ) : null}
@@ -139,6 +173,7 @@ export default function QuestionsListPage() {
key={item.slug}
item={item}
progress={sectionProgressBySlug.get(item.slug) ?? null}
+ onInfoClick={(section) => setSelectedSection(section)}
/>
))}
diff --git a/src/app/request-accepted/page.tsx b/src/app/request-accepted/page.tsx
index ca1f897..31d3b63 100644
--- a/src/app/request-accepted/page.tsx
+++ b/src/app/request-accepted/page.tsx
@@ -4,9 +4,14 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
+import { FiCopy, FiPhone } from "react-icons/fi";
+import CallResultSheet from "@/components/ui/call-result-sheet";
+import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import SubscriptionRequiredSheet from "@/components/ui/subscription-required-sheet";
import { PageBackground } from "@/components/utils/page-background";
+import type { MarriageField } from "@/hooks/marriage/types";
+import { useMarriageContactInfoQuery } from "@/hooks/marriage/use-contact-info";
import {
extractHabcoinPaymentUrl,
useHabcoinPaymentMutation,
@@ -15,14 +20,160 @@ import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
+type ContactInfoPhoneItem = {
+ key: string;
+ label: string;
+ phoneNumber: string;
+};
+
+function sanitizePhoneNumber(value: MarriageField["value"]) {
+ if (value === null || value === "") {
+ return null;
+ }
+
+ const trimmedValue = String(value).trim();
+
+ if (!trimmedValue) {
+ return null;
+ }
+
+ const digits = trimmedValue.replace(/\D/g, "");
+
+ if (!digits) {
+ return null;
+ }
+
+ return trimmedValue.startsWith("+") ? `+${digits}` : digits;
+}
+
+function titleFromKey(key: string) {
+ return key
+ .replace(/^q\d+[_-]?/i, "")
+ .replace(/[_-]+/g, " ")
+ .replace(/\s+/g, " ")
+ .trim()
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}
+
+function formatContactLabel(field: MarriageField) {
+ const resolvedLabel = (field.label || titleFromKey(field.key)).trim();
+
+ return resolvedLabel.replace(/\s+Number$/i, "").trim();
+}
+
+function getContactInfoPhoneItems(fields: MarriageField[] | null | undefined) {
+ return (fields ?? []).flatMap((field) => {
+ if (field.type.toLowerCase() !== "phone") {
+ return [];
+ }
+
+ const phoneNumber = sanitizePhoneNumber(field.value);
+
+ if (!phoneNumber) {
+ return [];
+ }
+
+ return [
+ {
+ key: field.key,
+ label: formatContactLabel(field),
+ phoneNumber,
+ },
+ ];
+ });
+}
+
+function ContactInfoPhoneCard({ item }: { item: ContactInfoPhoneItem }) {
+ return (
+
+
+
+ {item.phoneNumber}
+
+
+ {item.label}
+
+
+
+
+
{
+ void navigator.clipboard
+ .writeText(item.phoneNumber)
+ .catch(() => {});
+ }}
+ className="inline-flex p-3 items-center justify-center rounded-[10px] bg-[#F0445B] text-white"
+ >
+
+
+
+
+
+
+
+
+ );
+}
+
export default function RequestAcceptedPage() {
const { locale } = useI18n();
const router = useRouter();
+ const [isCallResultSheetOpen, setIsCallResultSheetOpen] = useState(false);
+ const [isContactInfoSheetOpen, setIsContactInfoSheetOpen] = useState(false);
const [isSubscriptionSheetOpen, setIsSubscriptionSheetOpen] = useState(false);
const profileHref = localizePath("/new-match/profile", locale);
const { data: profile } = useMarriageProfileQuery();
+ const isFemaleProfile = profile?.gender === "female";
+ const caseId = profile?.active_case?.case_id;
+ const caseStatus = profile?.active_case?.status;
const recommendedPlanId = profile?.recommended_plan?.id;
const paymentMutation = useHabcoinPaymentMutation();
+ const contactInfoQuery = useMarriageContactInfoQuery(caseId, {
+ enabled: false,
+ });
+ const titleText = isFemaleProfile
+ ? "The selected candidate will contact your family shortly."
+ : "REQUEST ACCEPTED";
+ const primaryActionText = isFemaleProfile
+ ? "No contact yet?"
+ : "Match Profile";
+ const secondaryActionText = isFemaleProfile ? "Contacted" : "View Contact";
+ const contactInfoPhoneItems = getContactInfoPhoneItems(
+ contactInfoQuery.data?.contact_info,
+ );
+
+ const handleSecondaryAction = async () => {
+ if (isFemaleProfile) {
+ setIsCallResultSheetOpen(true);
+ return;
+ }
+
+ if (caseStatus === "female_accepted" || caseStatus === "payment_pending") {
+ setIsCallResultSheetOpen(true);
+ return;
+ }
+
+ if (caseStatus === "payment_done") {
+ if (!caseId) {
+ return;
+ }
+
+ if (!contactInfoQuery.data) {
+ await contactInfoQuery.refetch();
+ }
+
+ setIsContactInfoSheetOpen(true);
+ }
+ };
const handlePayment = async () => {
if (!recommendedPlanId || paymentMutation.isPending) {
@@ -49,6 +200,31 @@ export default function RequestAcceptedPage() {
<>
+ {isCallResultSheetOpen ? (
+ setIsCallResultSheetOpen(false)} />
+ ) : null}
+
+ {isContactInfoSheetOpen ? (
+
+ {contactInfoPhoneItems.map((item) => (
+
+ ))}
+
+ ) : (
+
+ Contact information is not available yet.
+
+ )
+ }
+ onClose={() => setIsContactInfoSheetOpen(false)}
+ />
+ ) : null}
+
{isSubscriptionSheetOpen ? (
setIsSubscriptionSheetOpen(false)}
@@ -78,7 +254,7 @@ export default function RequestAcceptedPage() {
- REQUEST ACCEPTED
+ {titleText}
@@ -89,25 +265,44 @@ export default function RequestAcceptedPage() {
- Match Profile
+ {primaryActionText}
setIsSubscriptionSheetOpen(true)}
+ onClick={() => {
+ if (isFemaleProfile) {
+ setIsCallResultSheetOpen(true);
+ return;
+ }
+
+ if (
+ caseStatus === "female_accepted" ||
+ caseStatus === "payment_pending"
+ ) {
+ setIsCallResultSheetOpen(true);
+ return;
+ }
+
+ if (caseStatus === "payment_done") {
+ void handleSecondaryAction();
+ return;
+ }
+
+ setIsSubscriptionSheetOpen(true);
+ }}
className="max-w-[212px] appearance-none border-0 bg-transparent p-0 text-left"
>
- View Contact
+ {secondaryActionText}
- Please note: if you do not make contact within 2 days, a penalty
- may apply
+ If they don’t contact you within 2 days, please inform us.
diff --git a/src/app/request-sent/page.tsx b/src/app/request-sent/page.tsx
index ec6e7a3..71ae3fc 100644
--- a/src/app/request-sent/page.tsx
+++ b/src/app/request-sent/page.tsx
@@ -1,17 +1,21 @@
"use client";
import Image from "next/image";
-import { useRouter } from "next/navigation";
-import Button from "@/components/ui/button";
+import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
-import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
import Link from "next/link";
+const advisorAvatars = [
+ { id: "advisor-primary", src: "/assets/images/Avatar Image.png" },
+ { id: "advisor-secondary", src: "/assets/images/Ellipse 370.png" },
+ { id: "advisor-tertiary", src: "/assets/images/Avatar Image.png" },
+];
+
export default function RequestSentPage() {
- const { locale } = useI18n();
- const router = useRouter();
+ const { dictionary: t } = useI18n();
+ const copy = t.findingMatch;
return (
<>
@@ -53,6 +57,14 @@ export default function RequestSentPage() {
+
diff --git a/src/app/slider/page.tsx b/src/app/terms/page.tsx
similarity index 68%
rename from src/app/slider/page.tsx
rename to src/app/terms/page.tsx
index bc51f22..1bd8c75 100644
--- a/src/app/slider/page.tsx
+++ b/src/app/terms/page.tsx
@@ -1,5 +1,5 @@
import SliderPage from "@/components/sliders/slider-page";
-export default function SliderRoute() {
+export default function TermsRoute() {
return
;
}
diff --git a/src/components/dev/locator-paths.ts b/src/components/dev/locator-paths.ts
index c854c92..d6bf28a 100644
--- a/src/components/dev/locator-paths.ts
+++ b/src/components/dev/locator-paths.ts
@@ -4,7 +4,7 @@ export const LOCATORS = {
appQuestionsListPage: "D:/sajjadi/marriage/src/app/questions-list/page.tsx",
appQuestionDetailPage:
"D:/sajjadi/marriage/src/app/questions-list/[slug]/page.tsx",
- appSliderPage: "D:/sajjadi/marriage/src/app/slider/page.tsx",
+ appTermsPage: "D:/sajjadi/marriage/src/app/terms/page.tsx",
bookingTermsCard:
"D:/sajjadi/marriage/src/components/questions/booking-terms-card.tsx",
questionCard:
diff --git a/src/components/questions/question-card.tsx b/src/components/questions/question-card.tsx
index 9d3c784..bc9e2f6 100644
--- a/src/components/questions/question-card.tsx
+++ b/src/components/questions/question-card.tsx
@@ -11,15 +11,25 @@ import {
import type { QuestionCardIcon, QuestionListItem } from "@/data/question-data";
import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
+import Image from "next/image";
type QuestionCardProps = {
item: QuestionListItem;
progress?: number | null;
+ onInfoClick?: (item: QuestionListItem) => void;
};
const RADIUS = 8;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
+const iconAssetMap: Partial
> = {
+ profile: "/assets/images/mingcute_user-info-fill.svg",
+ education: "/assets/images/boxicons_education-filled.svg",
+ details: "/assets/images/Grfdasfoup.svg",
+ checklist: "/assets/images/noun-test-4525471 1.svg",
+ contact: "/assets/images/solar_user-id-bold.svg",
+};
+
const iconMap: Record = {
profile: IoPerson,
education: IoSchool,
@@ -31,6 +41,7 @@ const iconMap: Record = {
export function QuestionCard({
item,
progress = item.progress,
+ onInfoClick,
}: QuestionCardProps) {
const { dictionary: t, locale } = useI18n();
const hasProgress = typeof progress === "number" && Number.isFinite(progress);
@@ -39,6 +50,7 @@ export function QuestionCard({
: 0;
const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE;
const CardIcon = iconMap[item.icon];
+ const iconAsset = iconAssetMap[item.icon];
return (
-
-
+
+
+ {iconAsset ? (
+
+ ) : (
+
+ )}
+
@@ -74,41 +98,59 @@ export function QuestionCard({
{item.showInfoBadge ? (
-
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ onInfoClick?.(item);
+ }}
+ className="flex h-[17px] w-[17px] items-center justify-center rounded-[6px] bg-[#747474] text-white"
+ >
-
+
) : (
)}
-
-
-
-
+ ) : (
+
+
+
+
+ )}
{hasProgress ? `${normalizedProgress}%` : "..."}
diff --git a/src/components/questions/question-phone.tsx b/src/components/questions/question-phone.tsx
new file mode 100644
index 0000000..9b71738
--- /dev/null
+++ b/src/components/questions/question-phone.tsx
@@ -0,0 +1,160 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import type { QuestionField } from "@/data/question-data";
+import { useQuestionAnswer } from "./question-answer-storage";
+import QuestionTitle from "./question-title";
+
+type QuestionPhoneProps = {
+ question: QuestionField;
+ questionIndex: number;
+ countryCode?: string;
+};
+
+type PhoneValueParts = {
+ codeValue: string;
+ phoneValue: string;
+};
+
+function readPhoneValue(value: unknown, fallbackCode: string): PhoneValueParts {
+ if (value === null) {
+ return {
+ codeValue: "",
+ phoneValue: "",
+ };
+ }
+
+ if (typeof value !== "string") {
+ return {
+ codeValue: fallbackCode,
+ phoneValue: "",
+ };
+ }
+
+ if (value.length === 0) {
+ return {
+ codeValue: "",
+ phoneValue: "",
+ };
+ }
+
+ const separatorIndex = value.indexOf(" ");
+
+ if (separatorIndex >= 0) {
+ return {
+ codeValue: value.slice(0, separatorIndex),
+ phoneValue: value.slice(separatorIndex + 1),
+ };
+ }
+
+ if (value.startsWith("+")) {
+ return {
+ codeValue: value,
+ phoneValue: "",
+ };
+ }
+
+ return {
+ codeValue: "",
+ phoneValue: value,
+ };
+}
+
+function writePhoneValue(codeValue: string, phoneValue: string) {
+ if (!codeValue && !phoneValue) {
+ return null;
+ }
+
+ if (codeValue && phoneValue) {
+ return `${codeValue} ${phoneValue}`;
+ }
+
+ if (codeValue) {
+ return codeValue.startsWith("+") ? codeValue : `${codeValue} `;
+ }
+
+ return phoneValue;
+}
+
+export function QuestionPhone({
+ question,
+ questionIndex,
+ countryCode = "+98",
+}: QuestionPhoneProps) {
+ const { setValue, value } = useQuestionAnswer(question, questionIndex);
+ const defaultCodeValue = countryCode.trim() || "+98";
+ const initialValue = readPhoneValue(value, defaultCodeValue);
+ const [codeValue, setCodeValue] = useState(initialValue.codeValue);
+ const [phoneValue, setPhoneValue] = useState(initialValue.phoneValue);
+ const lastCommittedValueRef = useRef(value);
+
+ useEffect(() => {
+ if (value === lastCommittedValueRef.current) {
+ return;
+ }
+
+ const nextValue = readPhoneValue(value, defaultCodeValue);
+
+ setCodeValue(nextValue.codeValue);
+ setPhoneValue(nextValue.phoneValue);
+ lastCommittedValueRef.current = value;
+ }, [defaultCodeValue, value]);
+
+ const updateStoredValue = (nextCodeValue: string, nextPhoneValue: string) => {
+ const nextValue = writePhoneValue(nextCodeValue, nextPhoneValue);
+
+ lastCommittedValueRef.current = nextValue;
+ setValue(nextValue);
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+export default QuestionPhone;
diff --git a/src/components/sliders/slider-slide-five.tsx b/src/components/sliders/slider-slide-five.tsx
index a41da30..25afd0c 100644
--- a/src/components/sliders/slider-slide-five.tsx
+++ b/src/components/sliders/slider-slide-five.tsx
@@ -22,13 +22,13 @@ export function SliderSlideFive({ index }: SliderSlideProps) {
-
+
Your privacy and safety are our top priorities. We are committed to
keeping your information secure and giving you full control
throughout the process.
-
+
{NOTICE_ITEMS.map((item) => (
{item}
))}
diff --git a/src/components/ui/advisor-actions-card.tsx b/src/components/ui/advisor-actions-card.tsx
new file mode 100644
index 0000000..c9b1f62
--- /dev/null
+++ b/src/components/ui/advisor-actions-card.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import Image from "next/image";
+import Button from "@/components/ui/button";
+
+export type AdvisorAvatar = {
+ id: string;
+ src: string;
+};
+
+type AdvisorActionsCardProps = {
+ title: string;
+ description: string;
+ avatars: AdvisorAvatar[];
+ extraCount: number;
+ getAdvisorLabel: string;
+ getAdvisorHref: string;
+ className?: string;
+};
+
+export function AdvisorActionsCard({
+ title,
+ description,
+ avatars,
+ extraCount,
+ getAdvisorLabel,
+ getAdvisorHref,
+ className,
+}: AdvisorActionsCardProps) {
+ return (
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+ {avatars.map((avatar) => (
+
+
+
+ ))}
+
+ +{extraCount}
+
+
+
+
+ {getAdvisorLabel}
+
+
+
+
+ );
+}
+
+export default AdvisorActionsCard;
diff --git a/src/components/ui/call-result-sheet.tsx b/src/components/ui/call-result-sheet.tsx
index e88c3ec..9606639 100644
--- a/src/components/ui/call-result-sheet.tsx
+++ b/src/components/ui/call-result-sheet.tsx
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from "react";
import { useEffect, useId, useState } from "react";
-import Button from "@/components/ui/button";
import { useI18n } from "@/i18n/provider";
const EXIT_ANIMATION_MS = 220;
@@ -192,16 +191,29 @@ export function CallResultSheet({
-
-
+
+
+ {t.common.cancel}
+
+
+
+ {
onSubmit?.(selectedReason);
closeSheet();
}}
>
- {t.common.submit}
-
+
+ {t.common.submit}
+
+
diff --git a/src/components/ui/female-consent-sheet.tsx b/src/components/ui/female-consent-sheet.tsx
new file mode 100644
index 0000000..1a562b8
--- /dev/null
+++ b/src/components/ui/female-consent-sheet.tsx
@@ -0,0 +1,159 @@
+"use client";
+
+import type { HTMLAttributes, ReactNode } from "react";
+import { useEffect, useState } from "react";
+import { useI18n } from "@/i18n/provider";
+import Image from "next/image";
+
+const EXIT_ANIMATION_MS = 220;
+
+export type FemaleConsentSheetProps = Omit<
+ HTMLAttributes
,
+ "children" | "title"
+> & {
+ title: ReactNode;
+ description?: ReactNode;
+ buttons?: ReactNode | ((controls: { close: () => void }) => ReactNode);
+ closeOnOutside?: boolean;
+ onClose?: () => void;
+};
+
+export function FemaleConsentSheet({
+ title,
+ description,
+ buttons,
+ closeOnOutside = true,
+ onClose,
+ className,
+ ...props
+}: FemaleConsentSheetProps) {
+ const { dictionary: t } = useI18n();
+ const [isVisible, setIsVisible] = useState(true);
+ const [isEntering, setIsEntering] = useState(true);
+ const [isClosing, setIsClosing] = useState(false);
+
+ const closeSheet = () => {
+ if (isClosing) {
+ return;
+ }
+
+ setIsClosing(true);
+ };
+
+ const controls = { close: closeSheet };
+ const resolvedButtons =
+ typeof buttons === "function" ? buttons(controls) : buttons;
+
+ useEffect(() => {
+ const frameId = window.requestAnimationFrame(() => {
+ setIsEntering(false);
+ });
+
+ return () => {
+ window.cancelAnimationFrame(frameId);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!isVisible) {
+ return;
+ }
+
+ const previousBodyOverflow = document.body.style.overflow;
+ const previousHtmlOverflow = document.documentElement.style.overflow;
+
+ document.body.style.overflow = "hidden";
+ document.documentElement.style.overflow = "hidden";
+
+ return () => {
+ document.body.style.overflow = previousBodyOverflow;
+ document.documentElement.style.overflow = previousHtmlOverflow;
+ };
+ }, [isVisible]);
+
+ useEffect(() => {
+ if (!isClosing) {
+ return;
+ }
+
+ const timeoutId = window.setTimeout(() => {
+ setIsVisible(false);
+ onClose?.();
+ }, EXIT_ANIMATION_MS);
+
+ return () => {
+ window.clearTimeout(timeoutId);
+ };
+ }, [isClosing, onClose]);
+
+ if (!isVisible) {
+ return null;
+ }
+
+ return (
+ {
+ if (closeOnOutside && event.target === event.currentTarget) {
+ closeSheet();
+ }
+ }}
+ onKeyDown={(event) => {
+ if (
+ closeOnOutside &&
+ event.target === event.currentTarget &&
+ (event.key === "Escape" || event.key === "Enter" || event.key === " ")
+ ) {
+ event.preventDefault();
+ closeSheet();
+ }
+ }}
+ >
+
+
+
+ {title}
+
+
+
+
+
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+ {resolvedButtons ? {resolvedButtons}
: null}
+
+
+ );
+}
+
+export default FemaleConsentSheet;
diff --git a/src/components/ui/information-sheet.tsx b/src/components/ui/information-sheet.tsx
index 8b9af6a..0acd3d5 100644
--- a/src/components/ui/information-sheet.tsx
+++ b/src/components/ui/information-sheet.tsx
@@ -238,7 +238,7 @@ export function InformationSheet({