diff --git a/next.config.ts b/next.config.ts
index e9ffa30..64aa975 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,14 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "habibapp.com",
+ },
+ ],
+ },
};
export default nextConfig;
diff --git a/public/assets/images/Group 1597880481.png b/public/assets/images/Group 1597880481.png
new file mode 100644
index 0000000..0c3f4b6
Binary files /dev/null and b/public/assets/images/Group 1597880481.png differ
diff --git a/public/assets/images/Icfdason.svg b/public/assets/images/Icfdason.svg
new file mode 100644
index 0000000..e8a187c
--- /dev/null
+++ b/public/assets/images/Icfdason.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/assets/images/Vectofdasr.svg b/public/assets/images/Vectofdasr.svg
new file mode 100644
index 0000000..42ece4b
--- /dev/null
+++ b/public/assets/images/Vectofdasr.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/app/[lang]/new-match/profile/page.tsx b/src/app/[lang]/new-match/profile/page.tsx
new file mode 100644
index 0000000..5b7a853
--- /dev/null
+++ b/src/app/[lang]/new-match/profile/page.tsx
@@ -0,0 +1 @@
+export { default } from "@/app/new-match/profile/page";
diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx
index 495c275..40eb395 100644
--- a/src/app/intro/page.tsx
+++ b/src/app/intro/page.tsx
@@ -17,7 +17,9 @@ export default function Intro() {
? "/questions-list"
: profile?.status === "waiting"
? "/finding-match"
- : "/slider";
+ : profile?.status === "in_case" || profile?.status === "matched"
+ ? "/new-match"
+ : "/slider";
const submitHref = localizePath(submitPath, locale);
return (
diff --git a/src/app/new-match/page.tsx b/src/app/new-match/page.tsx
index 9de6abf..86ff49e 100644
--- a/src/app/new-match/page.tsx
+++ b/src/app/new-match/page.tsx
@@ -1,191 +1,295 @@
"use client";
import Image from "next/image";
-import { useRouter } from "next/navigation";
-import { useState } from "react";
-import { IoClose } from "react-icons/io5";
+import { useMemo } from "react";
+import { FaBell } from "react-icons/fa6";
import Button from "@/components/ui/button";
-import InformationSheet from "@/components/ui/information-sheet";
-import NavigationButton from "@/components/ui/navigation-button";
-import StickyHeader from "@/components/ui/sticky-header";
-import { localizePath } from "@/i18n/config";
+import { PageBackground } from "@/components/utils/page-background";
+import type {
+ MarriageField,
+ MarriageFieldValue,
+ MarriageMatchSummary,
+} from "@/hooks/marriage/types";
+import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { useI18n } from "@/i18n/provider";
-type MatchFieldProps = {
+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" },
+];
+
+const fieldCandidates = {
+ name: ["name", "full_name", "fullname", "first_name", "display_name"],
+ occupation: ["occupation", "job", "profession", "career", "work"],
+ age: ["age"],
+ city: ["city", "current_city", "residence_city", "location", "residence"],
+ maritalStatus: ["marital_status", "maritalstatus", "relationship_status"],
+ cityPreference: [
+ "city_preference",
+ "citypreference",
+ "preferred_city",
+ "preferred_location",
+ "future_residence",
+ ],
+} as const;
+
+type DisplayField = {
+ id: string;
label: string;
value: string;
- hint?: string;
};
-type MatchSplitFieldProps = {
- label: string;
- hint?: string;
- values: [string, string];
-};
+function normalizeFieldName(value: string) {
+ return value
+ .toLowerCase()
+ .replace(/^q\d+[_-]?/, "")
+ .replace(/[^a-z0-9]/g, "");
+}
-function MatchField({ label, value, hint }: MatchFieldProps) {
- return (
-
-
- {label}
- {hint ? (
-
- {hint}
-
- ) : null}
-
-
- {value}
-
-
- );
+function formatFieldValue(value: MarriageFieldValue) {
+ if (value === null || value === "") {
+ return null;
+ }
+
+ if (typeof value === "boolean") {
+ return value ? "Yes" : "No";
+ }
+
+ return String(value);
+}
+
+function titleFromKey(key: string) {
+ return key
+ .replace(/^q\d+[_-]?/i, "")
+ .replace(/[_-]+/g, " ")
+ .replace(/\s+/g, " ")
+ .trim()
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
}
-function MatchSplitField({ label, hint, values }: MatchSplitFieldProps) {
+function toDisplayField(field: MarriageField): DisplayField | null {
+ const value = formatFieldValue(field.value);
+
+ if (!value) {
+ return null;
+ }
+
+ return {
+ id: field.key || field.label || value,
+ label: field.label || titleFromKey(field.key),
+ value,
+ };
+}
+
+function pickField(
+ fields: MarriageField[],
+ candidates: readonly string[],
+ usedIndexes: Set,
+) {
+ const candidateSet = new Set(candidates.map(normalizeFieldName));
+ for (const [fieldIndex, field] of fields.entries()) {
+ if (usedIndexes.has(fieldIndex)) {
+ continue;
+ }
+
+ const displayField = toDisplayField(field);
+
+ if (
+ displayField &&
+ [field.key, field.label].some((value) =>
+ candidateSet.has(normalizeFieldName(value)),
+ )
+ ) {
+ usedIndexes.add(fieldIndex);
+ return displayField;
+ }
+ }
+
+ return null;
+}
+
+function useMatchSummaryDisplay(matchSummary: MarriageMatchSummary | null) {
+ return useMemo(() => {
+ const fields = matchSummary?.public_info ?? [];
+ const usedIndexes = new Set();
+ const name = pickField(fields, fieldCandidates.name, usedIndexes);
+ const occupation = pickField(
+ fields,
+ fieldCandidates.occupation,
+ usedIndexes,
+ );
+ const age = pickField(fields, fieldCandidates.age, usedIndexes);
+ const city = pickField(fields, fieldCandidates.city, usedIndexes);
+ const maritalStatus = pickField(
+ fields,
+ fieldCandidates.maritalStatus,
+ usedIndexes,
+ );
+ const cityPreference = pickField(
+ fields,
+ fieldCandidates.cityPreference,
+ usedIndexes,
+ );
+ const extraFields = fields
+ .filter((_, index) => !usedIndexes.has(index))
+ .map(toDisplayField)
+ .filter((field): field is DisplayField => Boolean(field))
+ .slice(0, 4);
+
+ return {
+ age,
+ city,
+ cityPreference,
+ extraFields,
+ maritalStatus,
+ name:
+ name?.value ??
+ (matchSummary?.id ? `Profile #${matchSummary.id}` : null),
+ occupation,
+ };
+ }, [matchSummary]);
+}
+
+function FieldLine({ field }: { field: DisplayField }) {
return (
-
-
- {label}
- {hint ? (
-
- {hint}
-
- ) : null}
-
-
- {values.map((value) => (
-
- {value}
-
- ))}
-
-
+
+ {field.label}:
+ {field.value}
+
);
}
export default function NewMatchPage() {
- const { dictionary: t, locale } = useI18n();
- const [isAcceptSheetOpen, setIsAcceptSheetOpen] = useState(false);
- const [shouldNavigateToRequestAccepted, setShouldNavigateToRequestAccepted] =
- useState(false);
- const router = useRouter();
+ const { dictionary: t } = useI18n();
+ const { data: profile, isError, isLoading } = useMarriageProfileQuery();
+ const matchSummary = profile?.match_summary ?? null;
+ const matchDisplay = useMatchSummaryDisplay(matchSummary);
+ const pairedFields = [matchDisplay.age, matchDisplay.city].filter(
+ (field): field is DisplayField => Boolean(field),
+ );
return (
<>
- {isAcceptSheetOpen ? (
- {
- setIsAcceptSheetOpen(false);
-
- if (shouldNavigateToRequestAccepted) {
- setShouldNavigateToRequestAccepted(false);
- router.push(localizePath("/request-accepted", locale));
- }
- }}
- buttons={({ close }) => (
-
-
- {t.common.cancel}
-
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+ {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) => (
+
+ ))}
+
+
{
- setShouldNavigateToRequestAccepted(true);
- close();
- }}
+ className="mt-[15px] rounded-[7px] border-none bg-white bg-none py-[12px] text-[#F0445B]! shadow-none"
+ href="/new-match/profile"
>
- {t.common.confirm}
+ View Profile
-
+ >
+ ) : (
+
+ No match summary is available yet.
+
)}
- />
- ) : null}
-
-
-
-
-
-
{t.match.title}
-
-
-
-
-
-
-
-
+
+
+ {t.findingMatch.advisorTitle}
+
+
+ {t.findingMatch.advisorDescription}
+
+
+
+
+ {advisorAvatars.map((avatar) => (
+
+
+
+ ))}
+
+ +7
+
+
+
+
+ {t.findingMatch.getAdvisor}
+
- setIsAcceptSheetOpen(true)}
- type="button"
- >
-
-
>
diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx
new file mode 100644
index 0000000..c41cad5
--- /dev/null
+++ b/src/app/new-match/profile/page.tsx
@@ -0,0 +1,205 @@
+"use client";
+
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import Button from "@/components/ui/button";
+import DismissReasonSheet from "@/components/ui/dismiss-reason-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, MarriageFieldValue } from "@/hooks/marriage/types";
+import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
+import { localizePath } from "@/i18n/config";
+import { useI18n } from "@/i18n/provider";
+
+function formatFieldValue(value: MarriageFieldValue) {
+ if (value === null || value === "") {
+ return null;
+ }
+
+ if (typeof value === "boolean") {
+ return value ? "Yes" : "No";
+ }
+
+ return String(value);
+}
+
+function titleFromKey(key: string) {
+ return key
+ .replace(/^q\d+[_-]?/i, "")
+ .replace(/[_-]+/g, " ")
+ .replace(/\s+/g, " ")
+ .trim()
+ .replace(/\b\w/g, (letter) => letter.toUpperCase());
+}
+
+function isImageField(field: MarriageField) {
+ return /(avatar|image|photo|picture|portrait|upload)/i.test(
+ `${field.key} ${field.label}`,
+ );
+}
+
+function MatchField({ field }: { field: MarriageField }) {
+ const value = formatFieldValue(field.value);
+
+ if (!value || isImageField(field)) {
+ return null;
+ }
+
+ const label = field.label || titleFromKey(field.key);
+
+ return (
+
+ );
+}
+
+export default function NewMatchProfilePage() {
+ const { dictionary: t, locale } = useI18n();
+ const router = useRouter();
+ const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false);
+ const [isRejectSheetOpen, setIsRejectSheetOpen] = useState(false);
+ const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] =
+ useState(false);
+ const { data: profile } = useMarriageProfileQuery();
+
+ return (
+ <>
+
+ {isRequestSheetOpen ? (
+
(
+
+
+ {t.common.cancel}
+
+ {
+ close();
+ router.push(localizePath("/request-accepted", locale));
+ }}
+ >
+ {t.common.confirm}
+
+
+ )}
+ onClose={() => setIsRequestSheetOpen(false)}
+ />
+ ) : null}
+ {isRejectSheetOpen ? (
+ (
+
+
+ {t.common.cancel}
+
+ {
+ close();
+ setIsDismissReasonSheetOpen(true);
+ }}
+ >
+ Reject
+
+
+ )}
+ onClose={() => setIsRejectSheetOpen(false)}
+ />
+ ) : null}
+ {isDismissReasonSheetOpen ? (
+ setIsDismissReasonSheetOpen(false)}
+ onSubmit={() => {
+ router.push(localizePath("/candidate-contact", locale));
+ }}
+ />
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+ {profile?.match_summary?.public_info.find(
+ (field) => field.key === "q1_full_name",
+ )?.value || "Name not available"}
+
+
+ {profile?.match_summary?.public_info.find(
+ (field) => field.key === "q1_full_name",
+ )?.value || "Name not available"}
+
+
+
+
+
+ {profile?.match_summary?.public_info.map((field) => (
+
+ ))}
+
+
+
+
+
+
+ setIsRejectSheetOpen(true)}
+ className="inline-flex w-1/3 items-center justify-center rounded-[12px] border border-[#BFBFBF] bg-white px-4 py-[13px] text-[16px] font-semibold text-[#9A9A9A]"
+ >
+ Reject
+
+ 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)]"
+ >
+
+ {t.match.acceptProfile}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/ui/dismiss-reason-sheet.tsx b/src/components/ui/dismiss-reason-sheet.tsx
index 9e0a22c..90bf4d7 100644
--- a/src/components/ui/dismiss-reason-sheet.tsx
+++ b/src/components/ui/dismiss-reason-sheet.tsx
@@ -1,7 +1,7 @@
"use client";
import type { HTMLAttributes } from "react";
-import { useEffect, useState } from "react";
+import { useEffect, useId, useState } from "react";
import Button from "@/components/ui/button";
import { useI18n } from "@/i18n/provider";
@@ -24,9 +24,12 @@ export function DismissReasonSheet({
...props
}: DismissReasonSheetProps) {
const { dictionary: t } = useI18n();
+ const options = t.sheets.callOptions;
+ const groupId = useId();
const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false);
+ const [selectedReason, setSelectedReason] = useState(options[0]);
const [reasonText, setReasonText] = useState("");
const closeSheet = () => {
@@ -128,26 +131,86 @@ export function DismissReasonSheet({
{t.sheets.dismissReasons}
+
+
+ {t.sheets.dismissReasons}
+
+
+
+ {options.map((option) => {
+ const checked = selectedReason === option;
+ const showTextArea = option === options[options.length - 1];
+
+ return (
+ setSelectedReason(option)}
+ >
+
+ {checked ? (
+
+ ) : null}
+
+
+
+
+ {option}
+
+
+ {checked && showTextArea ? (
+
+
+ );
+ })}
+
+
+
{t.sheets.dismissDescription}
-
diff --git a/src/components/ui/information-sheet.tsx b/src/components/ui/information-sheet.tsx
index bee27c7..8b9af6a 100644
--- a/src/components/ui/information-sheet.tsx
+++ b/src/components/ui/information-sheet.tsx
@@ -83,7 +83,7 @@ const ICON_PRESETS: Record<
height: 50,
},
check: {
- src: "/assets/images/Vectorcheck.svg",
+ src: "/assets/images/Vectofdasr.svg",
alt: "Check",
width: 36,
height: 36,
diff --git a/src/i18n/dictionaries.ts b/src/i18n/dictionaries.ts
index f2b9058..2a227b2 100644
--- a/src/i18n/dictionaries.ts
+++ b/src/i18n/dictionaries.ts
@@ -1,10 +1,11 @@
-import type { Locale } from "@/i18n/config";
+import type { Locale } from "@/i18n/config";
export const dictionaries = {
en: {
common: {
appName: "Habib Marriage",
submit: "Submit",
+ decline: "Decline",
continue: "Continue",
cancel: "Cancel",
confirm: "Confirm",
@@ -46,6 +47,9 @@ export const dictionaries = {
title: "New Match",
goBack: "Go back",
acceptProfile: "Accept Profile",
+ requestProceedTitle: "Request to Proceed",
+ requestProceedDescription:
+ "With your approval, we will approach their family on your behalf to propose marriage. After their family agrees, you will be introduced to each other for further acquaintance.",
acceptDescription:
"Are you sure you've fully reviewed the profile and are ready to proceed?",
fields: {
@@ -119,6 +123,7 @@ export const dictionaries = {
common: {
appName: "ازدواج حبیب",
submit: "ثبت",
+ decline: "رد",
continue: "ادامه",
cancel: "لغو",
confirm: "تایید",