Browse Source

fix: update icon source and add new SVG assets; enhance localization and UI components

- Changed the icon source for the check icon in information-sheet.tsx.
- Added new localization strings for decline and request proceed in dictionaries.ts.
- Introduced new image assets: Group 1597880481.png and Icfdason.svg.
- Created a new page for the new match profile with improved UI and functionality.
- Implemented request and reject profile functionality with corresponding UI sheets.
master
sina_sajjadi 2 months ago
parent
commit
8f051d4fab
  1. 9
      next.config.ts
  2. BIN
      public/assets/images/Group 1597880481.png
  3. 3
      public/assets/images/Icfdason.svg
  4. 3
      public/assets/images/Vectofdasr.svg
  5. 1
      src/app/[lang]/new-match/profile/page.tsx
  6. 4
      src/app/intro/page.tsx
  7. 424
      src/app/new-match/page.tsx
  8. 205
      src/app/new-match/profile/page.tsx
  9. 83
      src/components/ui/dismiss-reason-sheet.tsx
  10. 2
      src/components/ui/information-sheet.tsx
  11. 7
      src/i18n/dictionaries.ts

9
next.config.ts

@ -1,7 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */
images: {
remotePatterns: [
{
protocol: "https",
hostname: "habibapp.com",
},
],
},
}; };
export default nextConfig; export default nextConfig;

BIN
public/assets/images/Group 1597880481.png

After

Width: 90  |  Height: 91  |  Size: 9.3 KiB

3
public/assets/images/Icfdason.svg

@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.44429 7.24086C5.49445 6.19103 6.91857 5.60126 8.40349 5.60126C9.88841 5.60126 11.3125 6.19103 12.3627 7.24086L14.0035 8.88026L15.6443 7.24086C16.1609 6.70601 16.7788 6.27939 17.462 5.9859C18.1452 5.69241 18.8801 5.53792 19.6236 5.53146C20.3672 5.525 21.1046 5.66669 21.7928 5.94826C22.4811 6.22984 23.1063 6.64565 23.6321 7.17145C24.1579 7.69725 24.5737 8.3225 24.8553 9.01072C25.1369 9.69894 25.2786 10.4363 25.2721 11.1799C25.2656 11.9235 25.1112 12.6583 24.8177 13.3415C24.5242 14.0248 24.0975 14.6427 23.5627 15.1593L14.0035 24.7199L4.44429 15.1593C3.39445 14.1091 2.80469 12.685 2.80469 11.2001C2.80469 9.71515 3.39445 8.29102 4.44429 7.24086Z" fill="white"/>
</svg>

3
public/assets/images/Vectofdasr.svg

@ -0,0 +1,3 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 18C0 13.2261 1.89642 8.64773 5.27208 5.27208C8.64773 1.89642 13.2261 0 18 0C22.7739 0 27.3523 1.89642 30.7279 5.27208C34.1036 8.64773 36 13.2261 36 18C36 22.7739 34.1036 27.3523 30.7279 30.7279C27.3523 34.1036 22.7739 36 18 36C13.2261 36 8.64773 34.1036 5.27208 30.7279C1.89642 27.3523 0 22.7739 0 18ZM16.9728 25.704L27.336 12.7488L25.464 11.2512L16.6272 22.2936L10.368 17.0784L8.832 18.9216L16.9728 25.704Z" fill="#F0445B"/>
</svg>

1
src/app/[lang]/new-match/profile/page.tsx

@ -0,0 +1 @@
export { default } from "@/app/new-match/profile/page";

4
src/app/intro/page.tsx

@ -17,7 +17,9 @@ export default function Intro() {
? "/questions-list" ? "/questions-list"
: profile?.status === "waiting" : profile?.status === "waiting"
? "/finding-match" ? "/finding-match"
: "/slider";
: profile?.status === "in_case" || profile?.status === "matched"
? "/new-match"
: "/slider";
const submitHref = localizePath(submitPath, locale); const submitHref = localizePath(submitPath, locale);
return ( return (

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

@ -1,191 +1,295 @@
"use client"; "use client";
import Image from "next/image"; 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 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"; 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; label: string;
value: 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 (
<div className="space-y-2.5">
<p className="text-[15px] leading-5 font-semibold text-[#1A1A1A]">
{label}
{hint ? (
<span className="ml-1.5 text-[12px] font-normal text-[#7A7A7A]">
{hint}
</span>
) : null}
</p>
<div
aria-label={`${label}: ${value}`}
className="rounded-[15px] border border-white/90 bg-[#F7F1F1]/95 px-4 py-[17px] text-[15px] text-[#2C2C2C] shadow-[0_8px_24px_rgba(224,57,80,0.05)]"
role="note"
>
{value}
</div>
</div>
);
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<number>,
) {
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<number>();
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 ( return (
<div className="space-y-2.5">
<p className="text-[15px] leading-5 font-semibold text-[#1A1A1A]">
{label}
{hint ? (
<span className="ml-1.5 text-[12px] font-normal text-[#7A7A7A]">
{hint}
</span>
) : null}
</p>
<div className="grid grid-cols-2 gap-3">
{values.map((value) => (
<div
key={`${label}-${value}`}
aria-label={`${label}: ${value}`}
className="rounded-[15px] border border-white/90 bg-[#F7F1F1]/95 px-4 py-[17px] text-[15px] text-[#2C2C2C] shadow-[0_8px_24px_rgba(224,57,80,0.05)]"
role="note"
>
{value}
</div>
))}
</div>
</div>
<p className="break-words text-[10px] leading-[1.85] font-medium text-white">
<span>{field.label}: </span>
<span>{field.value}</span>
</p>
); );
} }
export default function NewMatchPage() { 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 ( return (
<> <>
{isAcceptSheetOpen ? (
<InformationSheet
icon="check"
title={t.match.acceptProfile}
description={t.match.acceptDescription}
onClose={() => {
setIsAcceptSheetOpen(false);
if (shouldNavigateToRequestAccepted) {
setShouldNavigateToRequestAccepted(false);
router.push(localizePath("/request-accepted", locale));
}
}}
buttons={({ close }) => (
<div className="flex w-full gap-3">
<Button
className="border border-[#D7D7D7] bg-transparent text-[#6B6B6B]"
onClick={close}
variant="outlined"
>
{t.common.cancel}
</Button>
<PageBackground />
<main className="-mx-[17px] flex min-h-screen flex-col px-[5px] pt-5 pb-5 text-center">
<section className="flex flex-col items-center">
<div
aria-hidden="true"
className="relative flex h-[60px] w-[60px] items-center justify-center rounded-full bg-[#FF4E67] shadow-[0_12px_28px_rgba(240,68,91,0.22)]"
>
<span className="absolute top-3 right-3 h-2 w-2 rounded-full border-2 border-[#FFD54B] border-b-0 border-l-0" />
<span className="absolute top-4 right-2 h-3 w-3 rounded-full border-2 border-[#FFD54B] border-b-0 border-l-0" />
<FaBell className="size-8 -rotate-12 text-[#FFD84A] drop-shadow-[0_2px_0_rgba(151,92,0,0.45)]" />
</div>
<h1 className="mt-[15px] text-[17px] leading-none font-black tracking-[0.02em] text-[#111111] uppercase">
You have a new match!
</h1>
<p className="mt-[11px] max-w-[322px] text-[12px] leading-[1.35] font-semibold text-[#7C7C7C]">
A matching profile has been found. Information is provided by the
girl&apos;s family or introducers. If you approve, we&apos;ll share
your profile with her family
</p>
</section>
<section className="mt-[36px] rounded-[11px] bg-[linear-gradient(180deg,#F0445B_0%,#F4556E_100%)] px-[17px] pt-[18px] pb-[17px] text-white shadow-[0_18px_38px_rgba(240,68,91,0.25)]">
{isLoading ? (
<p className="py-8 text-[13px] font-semibold">
Loading match summary...
</p>
) : isError ? (
<p className="py-8 text-[13px] font-semibold">
Unable to load match summary.
</p>
) : matchSummary ? (
<>
<h2 className="break-words text-[13px] leading-[1.4] font-bold">
<span>Name: </span>
<span>{matchDisplay.name}</span>
</h2>
<div className="mt-[3px] min-h-[68px]">
{matchDisplay.occupation ? (
<FieldLine field={matchDisplay.occupation} />
) : null}
{pairedFields.length ? (
<p className="break-words text-[10px] leading-[1.85] font-medium text-white">
{pairedFields.map((field, index) => (
<span key={field.id}>
{index > 0 ? <span> | </span> : null}
<span>
{field.label}: {field.value}
</span>
</span>
))}
</p>
) : null}
{matchDisplay.maritalStatus ? (
<FieldLine field={matchDisplay.maritalStatus} />
) : null}
{matchDisplay.cityPreference ? (
<FieldLine field={matchDisplay.cityPreference} />
) : null}
{matchDisplay.extraFields.map((field) => (
<FieldLine key={field.id} field={field} />
))}
</div>
<Button <Button
className="border-none bg-[#21C17D] bg-none from-[#21C17D] to-[#21C17D] shadow-none"
onClick={() => {
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
</Button> </Button>
</div>
</>
) : (
<p className="py-8 text-[13px] font-semibold">
No match summary is available yet.
</p>
)} )}
/>
) : null}
<main className="-mx-[17px] flex min-h-screen flex-col overflow-hidden">
<StickyHeader className="rounded-b-[32px] px-[17px] pt-7 pb-6">
<div className="flex items-center justify-between">
<NavigationButton
variant="transparent"
icon="close"
iconLabel={t.match.goBack}
/>
<h1 className="text-[26px] text-white">{t.match.title}</h1>
<NavigationButton
variant="transparent"
icon="info"
iconLabel={t.common.support}
/>
</div>
</StickyHeader>
<section className="px-[17px] pt-5 pb-36">
<div className="space-y-5">
<MatchField label={t.match.fields.birthYear} value="2024/12/04" />
<MatchField
label={t.match.fields.nationality}
value={t.match.values.iranian}
/>
<MatchSplitField
label={t.match.fields.residence}
values={[t.match.values.iran, t.match.values.tehran]}
/>
<MatchSplitField
label={t.match.fields.futureResidence}
values={[t.match.values.iran, t.match.values.tehran]}
/>
<MatchField
label={t.match.fields.religion}
value={t.match.values.muslim}
/>
<MatchField
label={t.match.fields.countryCity}
hint={t.match.fields.currentlyLivingIn}
value={`${t.match.values.iran} / ${t.match.values.tehran}`}
/>
<MatchField
label={t.match.fields.education}
value={t.match.values.education}
/>
<MatchField
label={t.match.fields.occupation}
value={t.match.values.occupation}
/>
</div>
</section> </section>
<section className="fixed right-0 bottom-0 left-0 z-20 mx-auto flex max-w-[375px] justify-evenly rounded-t-[30px] bg-white px-[17px] pt-6 pb-8 shadow-[0_-18px_50px_rgba(15,23,42,0.08)]">
<div className="rounded-full bg-white p-3.5 shadow-md">
<IoClose size={28} />
<section className="mt-[29px] rounded-[12px] border border-white/80 bg-white/78 px-3 py-3.5 text-left shadow-[0_18px_45px_rgba(15,23,42,0.06)] backdrop-blur-sm">
<h2 className="text-[16px] leading-none font-bold text-[#1C1C1C]">
{t.findingMatch.advisorTitle}
</h2>
<p className="mt-2 max-w-[280px] text-[11px] leading-[1.45] font-semibold text-[#8A8A8A]">
{t.findingMatch.advisorDescription}
</p>
<div className="mt-4 flex items-center justify-between gap-3">
<div className="flex items-center pl-1">
{advisorAvatars.map((avatar) => (
<span
key={avatar.id}
className="-ml-1.5 flex h-[30px] w-[30px] overflow-hidden rounded-full border-2 border-white bg-[#E7E7E7] first:ml-0"
>
<Image
src={avatar.src}
alt=""
width={30}
height={30}
className="h-full w-full object-cover"
/>
</span>
))}
<span className="-ml-1.5 flex h-[30px] w-[30px] items-center justify-center rounded-full border-2 border-white bg-[#EDEEF1] text-[12px] font-semibold text-[#1C1C1C]">
+7
</span>
</div>
<Button
className="w-auto rounded-[9px] border-none bg-[#EBEDF0] bg-none px-5 py-[13px] text-[#111111]! shadow-none"
href="/questions-list"
>
{t.findingMatch.getAdvisor}
</Button>
</div> </div>
<button
aria-label={t.match.acceptProfile}
className="rounded-full bg-[#F0445B] p-3.5 shadow-md"
onClick={() => setIsAcceptSheetOpen(true)}
type="button"
>
<Image
src={"/assets/images/Icon.svg"}
width={28}
height={28}
alt="heart"
/>
</button>
</section> </section>
</main> </main>
</> </>

205
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 (
<div className="mb-3.5 space-y-2 border-b border-[#000000]/10 pb-2.5">
<p className="text-xs font-semibold text-[#978787]">{label}</p>
<p className="font-semibold text-[#111111]">{value}</p>
</div>
);
}
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 (
<>
<PageBackground />
{isRequestSheetOpen ? (
<InformationSheet
icon="check"
title="Request to Proceed"
description="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."
buttons={({ close }) => (
<div className="grid w-full grid-cols-2 gap-3">
<Button variant="outlined" className="py-[18px]" onClick={close}>
{t.common.cancel}
</Button>
<Button
className="py-[18px]"
onClick={() => {
close();
router.push(localizePath("/request-accepted", locale));
}}
>
{t.common.confirm}
</Button>
</div>
)}
onClose={() => setIsRequestSheetOpen(false)}
/>
) : null}
{isRejectSheetOpen ? (
<InformationSheet
icon="warning"
title="Reject Profile"
description="Are you sure you've fully reviewed the profile and want to reject this profile?"
buttons={({ close }) => (
<div className="grid w-full grid-cols-2 gap-3">
<Button variant="outlined" className="py-[18px]" onClick={close}>
{t.common.cancel}
</Button>
<Button
className="py-[18px]"
onClick={() => {
close();
setIsDismissReasonSheetOpen(true);
}}
>
Reject
</Button>
</div>
)}
onClose={() => setIsRejectSheetOpen(false)}
/>
) : null}
{isDismissReasonSheetOpen ? (
<DismissReasonSheet
onClose={() => setIsDismissReasonSheetOpen(false)}
onSubmit={() => {
router.push(localizePath("/candidate-contact", locale));
}}
/>
) : null}
<main className="-mx-[17px] flex min-h-screen flex-col bg-[linear-gradient(180deg,rgba(255,197,196,0.2)_0%,rgba(251,237,237,0.7)_100%)] pb-10">
<StickyHeader className="rounded-b-[32px] px-[17px] pb-6 pt-7">
<div className="flex items-center justify-between">
<NavigationButton
variant="transparent"
icon="close"
iconLabel={t.match.goBack}
/>
<h1 className="text-[26px] text-white">{t.match.title}</h1>
<div className="w-[39px]" />
</div>
</StickyHeader>
<section className="px-[17px] pb-32 pt-5">
<div>
<Image
src={"/assets/images/Group 1597880481.png"}
alt=""
width={90}
height={90}
className="rounded-full"
/>
<div className="relative inline-block mt-2">
<p
className="text-[25px] leading-none text-white/80"
style={{ letterSpacing: "-2.3px", fontWeight: "1000" }}
>
{profile?.match_summary?.public_info.find(
(field) => field.key === "q1_full_name",
)?.value || "Name not available"}
</p>
<p className="absolute inset-0 whitespace-nowrap text-[22px] font-bold leading-none text-[#F0445B]">
{profile?.match_summary?.public_info.find(
(field) => field.key === "q1_full_name",
)?.value || "Name not available"}
</p>
</div>
</div>
<div className="mt-6">
{profile?.match_summary?.public_info.map((field) => (
<MatchField key={field.key} field={field} />
))}
</div>
</section>
<div className="fixed inset-x-0 bottom-0 z-30 px-[17px]">
<div className="mx-auto max-w-[375px] rounded-t-[24px] bg-white px-4 py-4 shadow-[0_12px_30px_rgba(0,0,0,0.14)]">
<div className="flex gap-3">
<button
type="button"
onClick={() => 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
</button>
<button
type="button"
onClick={() => 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)]"
>
<Image
src="/assets/images/Icfdason.svg"
alt=""
width={28}
height={28}
/>
<span>{t.match.acceptProfile}</span>
</button>
</div>
</div>
</div>
</main>
</>
);
}

83
src/components/ui/dismiss-reason-sheet.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import type { HTMLAttributes } from "react"; import type { HTMLAttributes } from "react";
import { useEffect, useState } from "react";
import { useEffect, useId, useState } from "react";
import Button from "@/components/ui/button"; import Button from "@/components/ui/button";
import { useI18n } from "@/i18n/provider"; import { useI18n } from "@/i18n/provider";
@ -24,9 +24,12 @@ export function DismissReasonSheet({
...props ...props
}: DismissReasonSheetProps) { }: DismissReasonSheetProps) {
const { dictionary: t } = useI18n(); const { dictionary: t } = useI18n();
const options = t.sheets.callOptions;
const groupId = useId();
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true); const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
const [selectedReason, setSelectedReason] = useState(options[0]);
const [reasonText, setReasonText] = useState(""); const [reasonText, setReasonText] = useState("");
const closeSheet = () => { const closeSheet = () => {
@ -128,26 +131,86 @@ export function DismissReasonSheet({
{t.sheets.dismissReasons} {t.sheets.dismissReasons}
</h2> </h2>
<div
className="mt-5 w-full"
role="radiogroup"
aria-labelledby={groupId}
>
<span id={groupId} className="sr-only">
{t.sheets.dismissReasons}
</span>
<div className="flex flex-col gap-[14px]">
{options.map((option) => {
const checked = selectedReason === option;
const showTextArea = option === options[options.length - 1];
return (
<button
key={option}
type="button"
className={[
"flex w-full items-start gap-3 rounded-[14px] bg-[#ECECEC] px-[14px] py-[18px] text-left",
checked ? "text-[#171717]" : "text-[#7B7B7B]",
]
.filter(Boolean)
.join(" ")}
onClick={() => setSelectedReason(option)}
>
<span
aria-hidden="true"
className={[
"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border",
checked ? "border-[#F0445B]" : "border-[#9E9E9E]",
]
.filter(Boolean)
.join(" ")}
>
{checked ? (
<span className="h-3 w-3 rounded-full bg-[#F0445B]" />
) : null}
</span>
<span className="flex min-w-0 flex-1 flex-col">
<span className="text-[16px] leading-[1.2] font-semibold">
{option}
</span>
{checked && showTextArea ? (
<textarea
className="mt-4 min-h-[143px] w-full resize-none rounded-[16px] border border-[#454545] bg-transparent px-3 py-3 text-[16px] text-[#171717] outline-none placeholder:text-[#AAAAAA]"
placeholder={t.sheets.dismissPlaceholder}
value={reasonText}
onChange={(event) =>
setReasonText(event.target.value)
}
onClick={(event) => event.stopPropagation()}
/>
) : null}
</span>
</button>
);
})}
</div>
</div>
<p className="mt-4 w-full text-left text-[14px] leading-[1.45] text-[#2C2C2C]"> <p className="mt-4 w-full text-left text-[14px] leading-[1.45] text-[#2C2C2C]">
{t.sheets.dismissDescription} {t.sheets.dismissDescription}
</p> </p>
<textarea
className="mt-4 min-h-[356px] w-full resize-none rounded-[16px] border border-[#454545] bg-transparent px-3 py-3 text-[16px] text-[#171717] outline-none placeholder:text-[#AAAAAA]"
placeholder={t.sheets.dismissPlaceholder}
value={reasonText}
onChange={(event) => setReasonText(event.target.value)}
/>
<div className="mt-[14px] w-full"> <div className="mt-[14px] w-full">
<Button <Button
className="rounded-[14px] py-[18px] shadow-none" className="rounded-[14px] py-[18px] shadow-none"
onClick={() => { onClick={() => {
onSubmit?.(reasonText);
onSubmit?.(
reasonText
? `${selectedReason}\n${reasonText}`
: selectedReason,
);
closeSheet(); closeSheet();
}} }}
> >
{t.common.submit}
{t.common.decline}
</Button> </Button>
</div> </div>
</div> </div>

2
src/components/ui/information-sheet.tsx

@ -83,7 +83,7 @@ const ICON_PRESETS: Record<
height: 50, height: 50,
}, },
check: { check: {
src: "/assets/images/Vectorcheck.svg",
src: "/assets/images/Vectofdasr.svg",
alt: "Check", alt: "Check",
width: 36, width: 36,
height: 36, height: 36,

7
src/i18n/dictionaries.ts

@ -1,10 +1,11 @@
import type { Locale } from "@/i18n/config";
import type { Locale } from "@/i18n/config";
export const dictionaries = { export const dictionaries = {
en: { en: {
common: { common: {
appName: "Habib Marriage", appName: "Habib Marriage",
submit: "Submit", submit: "Submit",
decline: "Decline",
continue: "Continue", continue: "Continue",
cancel: "Cancel", cancel: "Cancel",
confirm: "Confirm", confirm: "Confirm",
@ -46,6 +47,9 @@ export const dictionaries = {
title: "New Match", title: "New Match",
goBack: "Go back", goBack: "Go back",
acceptProfile: "Accept Profile", 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: acceptDescription:
"Are you sure you've fully reviewed the profile and are ready to proceed?", "Are you sure you've fully reviewed the profile and are ready to proceed?",
fields: { fields: {
@ -119,6 +123,7 @@ export const dictionaries = {
common: { common: {
appName: "ازدواج حبیب", appName: "ازدواج حبیب",
submit: "ثبت", submit: "ثبت",
decline: "رد",
continue: "ادامه", continue: "ادامه",
cancel: "لغو", cancel: "لغو",
confirm: "تایید", confirm: "تایید",

Loading…
Cancel
Save