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 }) => ( -
- + + +
+
+ + +

+ 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) => ( + + ))} +
+ -
+ + ) : ( +

+ No match summary is available yet. +

)} - /> - ) : null} - -
- -
- -

{t.match.title}

- -
-
- -
-
- - - - - - - - -
-
-
- +
+

+ {t.findingMatch.advisorTitle} +

+

+ {t.findingMatch.advisorDescription} +

+ +
+
+ {advisorAvatars.map((avatar) => ( + + + + ))} + + +7 + +
+ +
-
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 ( +
+

{label}

+

{value}

+
+ ); +} + +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 ? ( + ( +
+ + +
+ )} + onClose={() => setIsRequestSheetOpen(false)} + /> + ) : null} + {isRejectSheetOpen ? ( + ( +
+ + +
+ )} + onClose={() => setIsRejectSheetOpen(false)} + /> + ) : null} + {isDismissReasonSheetOpen ? ( + setIsDismissReasonSheetOpen(false)} + onSubmit={() => { + router.push(localizePath("/candidate-contact", locale)); + }} + /> + ) : null} + +
+ +
+ +

{t.match.title}

+
+
+ + +
+
+ +
+

+ {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) => ( + + ))} +
+
+ +
+
+
+ + +
+
+
+
+ + ); +} 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 ( +