Browse Source

feat: implement dynamic question components, slider functionality, and registration flow pages

front-test-2
ghorbani 4 weeks ago
parent
commit
130f518c45
  1. 8
      src/app/finding-match/page.tsx
  2. 33
      src/app/globals.css
  3. 18
      src/app/intro/page.tsx
  4. 2
      src/app/questions-list/[slug]/question-detail-client.tsx
  5. 6
      src/app/questions-list/page.tsx
  6. 20
      src/app/request-accepted/page.tsx
  7. 10
      src/app/request-sent/page.tsx
  8. 2
      src/components/questions/question-button.tsx
  9. 10
      src/components/questions/question-card.tsx
  10. 2
      src/components/questions/question-checkbox.tsx
  11. 10
      src/components/questions/question-date.tsx
  12. 10
      src/components/questions/question-dropdown.tsx
  13. 233
      src/components/questions/question-file.tsx
  14. 6
      src/components/questions/question-phone.tsx
  15. 231
      src/components/questions/question-photo.tsx
  16. 4
      src/components/questions/question-progress-tracker.tsx
  17. 2
      src/components/questions/question-radio.tsx
  18. 4
      src/components/questions/question-slider.tsx
  19. 4
      src/components/questions/question-snap-list.tsx
  20. 10
      src/components/questions/question-text.tsx
  21. 8
      src/components/questions/question-title.tsx
  22. 6
      src/components/questions/required-steps-card.tsx
  23. 10
      src/components/sliders/slider-slide-four.tsx
  24. 24
      src/components/sliders/slider-slide-one.tsx
  25. 8
      src/components/sliders/slider-slide-three.tsx
  26. 10
      src/components/sliders/slider-slide-two.tsx
  27. 6
      src/components/ui/advisor-actions-card.tsx
  28. 6
      src/components/ui/button.tsx
  29. 10
      src/components/ui/info-progress-card.tsx
  30. 4
      src/components/ui/information-sheet.tsx
  31. 9
      src/components/ui/sticky-header.tsx
  32. 1
      src/hooks/marriage/use-upload-tmp-media.ts
  33. 100
      src/i18n/locales/en/questions.json
  34. 100
      src/i18n/locales/fa/questions.json

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

@ -35,7 +35,7 @@ export default function FindingMatchPage() {
> >
<header className="-mx-[6px] flex items-center justify-between"> <header className="-mx-[6px] flex items-center justify-between">
<NavigationButton icon="back" /> <NavigationButton icon="back" />
<h1 className="font-faminela">{t.common.appName}</h1>
<h1 className="font-faminela group-16">{t.common.appName}</h1>
<NavigationButton icon="subscription" iconLabel="Subscribe" /> <NavigationButton icon="subscription" iconLabel="Subscribe" />
</header> </header>
@ -51,11 +51,11 @@ export default function FindingMatchPage() {
/> />
</div> </div>
<h1 className="mt-5 text-[22px] font-bold leading-none tracking-[0.02em] text-[#171717] uppercase">
<h1 className="mt-5 group-16 font-bold leading-none tracking-[0.02em] text-[#171717] uppercase">
{copy.title} {copy.title}
</h1> </h1>
<p className="mt-3 max-w-[320px] text-[14px] leading-[1.35] font-semibold text-[#747474]">
<p className="mt-3 max-w-[320px] group-12 leading-[1.35] font-semibold text-[#747474]">
{copy.description} {copy.description}
</p> </p>
</section> </section>
@ -73,7 +73,7 @@ export default function FindingMatchPage() {
className="inline-flex mt-3.5 w-full cursor-pointer items-center justify-center rounded-[9px] border-none bg-[#2B2C31] bg-none px-4 py-[17px] text-center text-white shadow-none transition-opacity" className="inline-flex mt-3.5 w-full cursor-pointer items-center justify-center rounded-[9px] border-none bg-[#2B2C31] bg-none px-4 py-[17px] text-center text-white shadow-none transition-opacity"
href={localizePath("/questions-list", locale)} href={localizePath("/questions-list", locale)}
> >
<span className="flex min-w-0 items-center justify-center gap-2 text-center text-[16px] leading-none font-semibold">
<span className="flex min-w-0 items-center justify-center gap-2 text-center group-16 leading-none font-semibold">
<FaPen aria-hidden="true" className="h-3.5 w-3.5 shrink-0" /> <FaPen aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span>{copy.editProfile}</span> <span>{copy.editProfile}</span>
</span> </span>

33
src/app/globals.css

@ -18,6 +18,39 @@
--font-faminela: var(--font-faminela-local); --font-faminela: var(--font-faminela-local);
} }
/* Typography Configuration - Centralized Design System Scale */
@utility group-10 {
font-size: 10px;
}
@utility group-12 {
font-size: 12px;
}
@utility group-14 {
font-size: 14px;
}
@utility group-16 {
font-size: 16px;
}
.group-10 {
font-size: 10px;
}
.group-12 {
font-size: 12px;
}
.group-14 {
font-size: 14px;
}
.group-16 {
font-size: 16px;
}
html { html {
min-height: 100%; min-height: 100%;
} }

18
src/app/intro/page.tsx

@ -144,7 +144,7 @@ export default function Intro() {
)} )}
<header className="flex items-center justify-between"> <header className="flex items-center justify-between">
<NavigationButton icon="back" /> <NavigationButton icon="back" />
<h1 className="font-faminela">{t.common.appName}</h1>
<h1 className="font-faminela group-16">{t.common.appName}</h1>
<NavigationButton <NavigationButton
icon="support" icon="support"
iconLabel={t.common.support} iconLabel={t.common.support}
@ -159,10 +159,10 @@ export default function Intro() {
width={168} width={168}
height={155} height={155}
/> />
<h2 className="text-[#475569] font-bold mt-5 text-center">
<h2 className="group-16 text-[#475569] font-bold mt-5 text-center">
{t.intro.title} {t.intro.title}
</h2> </h2>
<p className=" text-center mt-2 text-xs text-[#4D4D4D]">
<p className="text-center mt-2 group-12 text-[#4D4D4D]">
{t.intro.description} {t.intro.description}
</p> </p>
</div> </div>
@ -175,8 +175,8 @@ export default function Intro() {
height={37} height={37}
/> />
<div className="flex flex-col items-start justify-start -mb-2"> <div className="flex flex-col items-start justify-start -mb-2">
<p className="text-[#36363C] text-lg font-bold leading-3">120</p>
<p className="text-[#36363C] text-[10px] font-semibold">
<p className="text-[#36363C] group-16 font-bold leading-3">120</p>
<p className="text-[#36363C] group-10 font-semibold">
{t.intro.userProfile} {t.intro.userProfile}
</p> </p>
</div> </div>
@ -189,8 +189,8 @@ export default function Intro() {
height={37} height={37}
/> />
<div className="flex flex-col items-start justify-start -mb-2"> <div className="flex flex-col items-start justify-start -mb-2">
<p className="text-[#36363C] text-lg font-bold leading-3">14</p>
<p className="text-[#36363C] text-[10px] font-semibold">
<p className="text-[#36363C] group-16 font-bold leading-3">14</p>
<p className="text-[#36363C] group-10 font-semibold">
{t.intro.matches} {t.intro.matches}
</p> </p>
</div> </div>
@ -203,8 +203,8 @@ export default function Intro() {
height={37} height={37}
/> />
<div className="flex flex-col items-start justify-start -mb-2"> <div className="flex flex-col items-start justify-start -mb-2">
<p className="text-[#36363C] text-lg font-bold leading-3">14</p>
<p className="text-[#36363C] text-[10px] font-semibold">
<p className="text-[#36363C] group-16 font-bold leading-3">14</p>
<p className="text-[#36363C] group-10 font-semibold">
{t.intro.marriage} {t.intro.marriage}
</p> </p>
</div> </div>

2
src/app/questions-list/[slug]/question-detail-client.tsx

@ -452,7 +452,7 @@ export default function QuestionDetailClient({
icon="close" icon="close"
iconLabel={closeLabel} iconLabel={closeLabel}
/> />
<h1 className="min-w-0 flex-1 text-center font-semibold text-white">
<h1 className="min-w-0 flex-1 text-center font-semibold text-white truncate">
{item.title} {item.title}
</h1> </h1>
<NavigationButton <NavigationButton

6
src/app/questions-list/page.tsx

@ -152,7 +152,7 @@ export default function QuestionsListPage() {
icon={null} icon={null}
title={({ close }) => ( title={({ close }) => (
<span className="flex w-full items-start justify-between gap-3 text-left"> <span className="flex w-full items-start justify-between gap-3 text-left">
<span className="text-[13px] leading-5 font-bold tracking-normal text-[#8B8B8B]">
<span className="group-14 leading-5 font-bold tracking-normal text-[#8B8B8B]">
{selectedSection.title} {selectedSection.title}
</span> </span>
<button <button
@ -166,7 +166,7 @@ export default function QuestionsListPage() {
</span> </span>
)} )}
description={ description={
<p className="px-0.5 text-sm leading-[1.45] text-[#2D2D2D]">
<p className="px-0.5 group-14 leading-[1.45] text-[#2D2D2D]">
{selectedSection.summary} {selectedSection.summary}
</p> </p>
} }
@ -190,7 +190,7 @@ export default function QuestionsListPage() {
icon="back" icon="back"
iconLabel={t.questions.closeQuestionsList} iconLabel={t.questions.closeQuestionsList}
/> />
<h1 className="text-[13px] font-semibold text-[#151515]">
<h1 className="group-16 font-semibold text-[#151515]">
{t.questions.profileRegistration} {t.questions.profileRegistration}
</h1> </h1>
<NavigationButton <NavigationButton

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

@ -110,11 +110,11 @@ function ContactInfoPhoneCard({ item }: { item: ContactInfoPhoneItem }) {
<div className="min-w-0 flex-1 text-left"> <div className="min-w-0 flex-1 text-left">
<p <p
dir="ltr" dir="ltr"
className="text-lg leading-none font-semibold tracking-[0.01em] text-[#4D4D4D] tabular-nums"
className="group-16 leading-none font-semibold tracking-[0.01em] text-[#4D4D4D] tabular-nums"
> >
{item.phoneNumber} {item.phoneNumber}
</p> </p>
<p className="mt-2 text-[10px] leading-none font-semibold text-[#F0445B]">
<p className="mt-2 group-10 leading-none font-semibold text-[#F0445B]">
{item.label} {item.label}
</p> </p>
</div> </div>
@ -237,7 +237,7 @@ export default function RequestAcceptedPage() {
))} ))}
</div> </div>
) : ( ) : (
<div className="rounded-[12px] bg-[#ECECEC] px-4 py-3 text-sm font-semibold text-[#555]">
<div className="rounded-[12px] bg-[#ECECEC] px-4 py-3 group-12 font-semibold text-[#555]">
Contact information is not available yet. Contact information is not available yet.
</div> </div>
) )
@ -260,7 +260,7 @@ export default function RequestAcceptedPage() {
> >
<header className="-mx-[6px] flex items-center justify-between"> <header className="-mx-[6px] flex items-center justify-between">
<NavigationButton icon="back" /> <NavigationButton icon="back" />
<h1 className="font-faminela">Habib Marriage</h1>
<h1 className="font-faminela group-16">Habib Marriage</h1>
<NavigationButton icon="support" iconLabel="Support" /> <NavigationButton icon="support" iconLabel="Support" />
</header> </header>
@ -277,18 +277,18 @@ export default function RequestAcceptedPage() {
/> />
</div> </div>
<h1 className="mt-11 text-[22px] leading-none font-black tracking-[0.02em] text-[#171717] uppercase">
<h1 className="mt-11 group-16 leading-none font-black tracking-[0.02em] text-[#171717] uppercase">
{titleText} {titleText}
</h1> </h1>
<p className="mt-4 max-w-[315px] text-[16px] leading-[1.45] font-semibold text-[#777777]">
<p className="mt-4 max-w-[315px] group-12 leading-[1.45] font-semibold text-[#777777]">
You can now view their family&apos;s contact details and arrange You can now view their family&apos;s contact details and arrange
further steps. further steps.
</p> </p>
<div className="flex mt-9 w-full justify-center gap-4"> <div className="flex mt-9 w-full justify-center gap-4">
<Link href={profileHref} className="max-w-[212px]"> <Link href={profileHref} className="max-w-[212px]">
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow text-sm text-center font-semibold text-[#36363C]">
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C]">
{primaryActionText} {primaryActionText}
</div> </div>
</Link> </Link>
@ -318,14 +318,14 @@ export default function RequestAcceptedPage() {
}} }}
className="max-w-[212px] appearance-none border-0 bg-transparent p-0 text-left" className="max-w-[212px] appearance-none border-0 bg-transparent p-0 text-left"
> >
<div className="bg-linear-180 from-[#FE6F82] to-[#E03950] px-4 py-2 rounded-[15px] shadow text-sm text-center font-semibold text-[#fff] shadow-md shadow-[#F2596E]/60">
<div className="bg-linear-180 from-[#FE6F82] to-[#E03950] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#fff] shadow-md shadow-[#F2596E]/60">
{secondaryActionText} {secondaryActionText}
</div> </div>
</button> </button>
</div> </div>
<div className="border border-[#F0445B] bg-[#F0445B]/10 rounded-xl mt-4"> <div className="border border-[#F0445B] bg-[#F0445B]/10 rounded-xl mt-4">
<p className="text-[#F0445B] text-xs font-semibold py-2.5 px-3.5">
<p className="text-[#F0445B] group-12 font-semibold py-2.5 px-3.5">
If they dont contact you within 2 days, please inform us. If they dont contact you within 2 days, please inform us.
</p> </p>
</div> </div>
@ -341,7 +341,7 @@ export default function RequestAcceptedPage() {
alt="lock" alt="lock"
/> />
<h2 className="text-[17px] leading-none font-semibold text-[#747474]">
<h2 className="group-14 leading-none font-semibold text-[#747474]">
Profile is locked Profile is locked
</h2> </h2>
</div> </div>

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

@ -27,7 +27,7 @@ export default function RequestSentPage() {
> >
<header className="-mx-[6px] flex items-center justify-between"> <header className="-mx-[6px] flex items-center justify-between">
<NavigationButton icon="back" /> <NavigationButton icon="back" />
<h1 className="font-faminela">Habib Marriage</h1>
<h1 className="font-faminela group-16">Habib Marriage</h1>
<NavigationButton icon="support" iconLabel="Support" /> <NavigationButton icon="support" iconLabel="Support" />
</header> </header>
@ -44,16 +44,16 @@ export default function RequestSentPage() {
/> />
</div> </div>
<h1 className="mt-11 text-[22px] leading-none font-black tracking-[0.02em] text-[#171717] uppercase">
<h1 className="mt-11 group-16 leading-none font-black tracking-[0.02em] text-[#171717] uppercase">
Request Sent Request Sent
</h1> </h1>
<p className="mt-4 max-w-[315px] text-[16px] leading-[1.45] font-semibold text-[#777777]">
<p className="mt-4 max-w-[315px] group-12 leading-[1.45] font-semibold text-[#777777]">
We will propose marriage on your behalf. If they accept, their contact details will be shared with you. We will propose marriage on your behalf. If they accept, their contact details will be shared with you.
</p> </p>
<Link href={"/new-match/profile"} className="mt-9 w-full max-w-[212px]"> <Link href={"/new-match/profile"} className="mt-9 w-full max-w-[212px]">
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow text-sm text-center font-semibold text-[#36363C]">
<div className="bg-[#F5F5F5] px-4 py-2 rounded-[15px] shadow group-14 text-center font-semibold text-[#36363C]">
Match Profile Match Profile
</div> </div>
</Link > </Link >
@ -78,7 +78,7 @@ export default function RequestSentPage() {
alt="lock" alt="lock"
/> />
<h2 className="text-[17px] leading-none font-semibold text-[#747474]">
<h2 className="group-14 leading-none font-semibold text-[#747474]">
Profile is locked Profile is locked
</h2> </h2>
</div> </div>

2
src/components/questions/question-button.tsx

@ -42,7 +42,7 @@ export function QuestionButton({
type="button" type="button"
disabled={disabled} disabled={disabled}
onClick={() => setValue(true)} onClick={() => setValue(true)}
className="min-h-[50px] rounded-[15px] bg-[#181818] px-5 py-3 text-[15px] font-semibold text-white"
className="min-h-[50px] rounded-[15px] bg-[#181818] px-5 py-3 group-14 font-semibold text-white"
> >
{question.extras.options[0] || {question.extras.options[0] ||
question.extras.placeHolder || question.extras.placeHolder ||

10
src/components/questions/question-card.tsx

@ -82,16 +82,16 @@ export function QuestionCard({
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex min-w-0 items-baseline gap-1"> <div className="flex min-w-0 items-baseline gap-1">
<h2 className="truncate text-[12px] leading-none font-bold text-[#1B1B1B]">
<h2 className="truncate group-14 leading-none font-bold text-[#1B1B1B]">
{item.title} {item.title}
</h2> </h2>
{item.required ? ( {item.required ? (
<span className="shrink-0 text-[9px] leading-none font-semibold text-[#FF5B73]">
<span className="shrink-0 group-10 leading-none font-semibold text-[#FF5B73]">
({t.common.required}) ({t.common.required})
</span> </span>
) : null} ) : null}
</div> </div>
<p className="mt-1.5 text-[10px] leading-none font-medium text-[#7A7A7A]">
<p className="mt-1.5 group-10 leading-none font-medium text-[#7A7A7A]">
{t.common.estimateTime}: {item.estimate} {t.common.estimateTime}: {item.estimate}
</p> </p>
</div> </div>
@ -151,7 +151,7 @@ export function QuestionCard({
/> />
</svg> </svg>
)} )}
<span className="text-[10px] leading-none font-semibold text-[#1F1F1F]">
<span className="group-10 leading-none font-semibold text-[#1F1F1F]">
{hasProgress ? `${normalizedProgress}%` : "..."} {hasProgress ? `${normalizedProgress}%` : "..."}
</span> </span>
</div> </div>
@ -159,7 +159,7 @@ export function QuestionCard({
</div> </div>
{item.note ? ( {item.note ? (
<p className="mt-2 rounded-full bg-[#FFF1F4] px-3 py-1.5 text-center text-[9px] leading-none font-medium text-[#F35A74]">
<p className="mt-2 rounded-full bg-[#FFF1F4] px-3 py-1.5 text-center group-10 leading-none font-medium text-[#F35A74]">
{item.note} {item.note}
</p> </p>
) : null} ) : null}

2
src/components/questions/question-checkbox.tsx

@ -70,7 +70,7 @@ export function QuestionCheckbox({
htmlFor={optionId} htmlFor={optionId}
className={[ className={[
"w-full cursor-pointer rounded-[12px] px-4 py-3.5", "w-full cursor-pointer rounded-[12px] px-4 py-3.5",
"text-start text-[13px] leading-tight font-semibold transition-all duration-200",
"text-start group-14 leading-tight font-semibold transition-all duration-200",
isSelected isSelected
? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]" ? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]"
: "bg-[#F5F5F5] text-[#181818] hover:bg-[#EBEBEB]", : "bg-[#F5F5F5] text-[#181818] hover:bg-[#EBEBEB]",

10
src/components/questions/question-date.tsx

@ -102,7 +102,7 @@ export function QuestionDate({
onChange={(e) => handleSelectChange(year, month, e.target.value)} onChange={(e) => handleSelectChange(year, month, e.target.value)}
disabled={disabled} disabled={disabled}
aria-label="روز" aria-label="روز"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
> >
<option value="">روز</option> <option value="">روز</option>
{DAYS.map((d) => ( {DAYS.map((d) => (
@ -118,7 +118,7 @@ export function QuestionDate({
onChange={(e) => handleSelectChange(year, e.target.value, day)} onChange={(e) => handleSelectChange(year, e.target.value, day)}
disabled={disabled} disabled={disabled}
aria-label="ماه" aria-label="ماه"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
> >
<option value="">ماه</option> <option value="">ماه</option>
{MONTHS.map((m) => ( {MONTHS.map((m) => (
@ -134,7 +134,7 @@ export function QuestionDate({
onChange={(e) => handleSelectChange(e.target.value, month, day)} onChange={(e) => handleSelectChange(e.target.value, month, day)}
disabled={disabled} disabled={disabled}
aria-label="سال" aria-label="سال"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
> >
<option value="">سال</option> <option value="">سال</option>
{YEARS.map((y) => ( {YEARS.map((y) => (
@ -147,7 +147,7 @@ export function QuestionDate({
{/* Display Calculated Age */} {/* Display Calculated Age */}
<div className="mt-2.5 flex w-full flex-col gap-2"> <div className="mt-2.5 flex w-full flex-col gap-2">
<span className="block text-[12px] leading-tight font-semibold text-[#181818]">
<span className="block group-14 leading-tight font-semibold text-[#181818]">
Age Age
</span> </span>
<input <input
@ -155,7 +155,7 @@ export function QuestionDate({
disabled disabled
readOnly readOnly
value={calculatedAge !== null ? calculatedAge : ""} value={calculatedAge !== null ? calculatedAge : ""}
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-[#F5F2F1] px-4 text-[14px] font-medium text-[#7C7472] outline-none cursor-not-allowed disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-[#F5F2F1] px-4 group-12 font-medium text-[#7C7472] outline-none cursor-not-allowed disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
/> />
</div> </div>
</div> </div>

10
src/components/questions/question-dropdown.tsx

@ -94,7 +94,7 @@ export function QuestionDropdown({
type="button" type="button"
disabled={disabled} disabled={disabled}
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="flex h-[55px] w-full items-center justify-between rounded-[11px] border border-[#BCBCBC] bg-white py-[10px] px-[10px] text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
className="flex h-[55px] w-full items-center justify-between rounded-[11px] border border-[#BCBCBC] bg-white py-[10px] px-[10px] group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]"
> >
<span className={selectValue ? "text-[#181818]" : "text-[#8B8B8B]"}> <span className={selectValue ? "text-[#181818]" : "text-[#8B8B8B]"}>
{selectValue || question.extras.placeHolder || "Select"} {selectValue || question.extras.placeHolder || "Select"}
@ -148,13 +148,13 @@ export function QuestionDropdown({
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search..." placeholder="Search..."
className="flex-1 bg-transparent text-[13px] text-[#181818] outline-none placeholder:text-[#9D8F8C]"
className="flex-1 bg-transparent group-12 text-[#181818] outline-none placeholder:text-[#9D8F8C]"
/> />
{searchQuery ? ( {searchQuery ? (
<button <button
type="button" type="button"
onClick={() => setSearchQuery("")} onClick={() => setSearchQuery("")}
className="text-[12px] text-[#747474] hover:text-[#181818]"
className="group-10 text-[#747474] hover:text-[#181818]"
> >
</button> </button>
@ -192,14 +192,14 @@ export function QuestionDropdown({
: "border-[#4D4D4D] bg-transparent", : "border-[#4D4D4D] bg-transparent",
].join(" ")} ].join(" ")}
/> />
<span className="text-[14px] font-bold text-[#4D4D4D]">
<span className="group-12 font-bold text-[#4D4D4D]">
{option} {option}
</span> </span>
</button> </button>
); );
}) })
) : ( ) : (
<span className="text-[12px] text-gray-400">No options found</span>
<span className="group-10 text-gray-400">No options found</span>
)} )}
</div> </div>
</div> </div>

233
src/components/questions/question-file.tsx

@ -4,6 +4,7 @@ import Image from "next/image";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media";
import { getApiRequestUrl } from "@/lib/http";
import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions";
import { useQuestionAnswers } from "./question-answer-storage"; import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title"; import QuestionTitle from "./question-title";
@ -37,23 +38,60 @@ function resolveMediaType(
return "file"; return "file";
} }
function isImageFile(fileName?: string | null, fileUrl?: string | null): boolean {
if (fileUrl && (fileUrl.startsWith("data:image/") || fileUrl.startsWith("blob:"))) {
if (fileUrl.startsWith("data:application/pdf")) return false;
if (fileName && /\.(pdf|doc|docx|xls|xlsx|txt|zip|rar)$/i.test(fileName)) {
return false;
}
return true;
}
const nameToCheck = fileName || fileUrl || "";
return /\.(jpg|jpeg|png|webp|gif|svg|bmp|avif)$/i.test(nameToCheck);
}
export function QuestionFile({ export function QuestionFile({
question, question,
questionIndex, questionIndex,
disabled, disabled,
}: QuestionFileProps) { }: QuestionFileProps) {
const [selectedFileName, setSelectedFileName] = useState<string | null>(null);
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question, questionIndex);
const initialFileName =
typeof storedValue === "string" && storedValue.trim().length > 0
? storedValue.split("/").pop() ?? storedValue
: null;
const initialFileUrl =
typeof storedValue === "string" && storedValue.trim().length > 0
? storedValue.startsWith("http") ||
storedValue.startsWith("blob:") ||
storedValue.startsWith("data:")
? storedValue
: getApiRequestUrl(storedValue)
: null;
const [selectedFileName, setSelectedFileName] = useState<string | null>(
initialFileName,
);
const [filePreviewUrl, setFilePreviewUrl] = useState<string | null>(
initialFileUrl,
);
const [isFlutterPicking, setIsFlutterPicking] = useState(false); const [isFlutterPicking, setIsFlutterPicking] = useState(false);
const acceptedFiles = question.extras.options
const acceptedFiles = (question.extras?.options ?? [])
.map((option) => option.replace(/^\./, "")) .map((option) => option.replace(/^\./, ""))
.join(", "); .join(", ");
const { setAnswerValue } = useQuestionAnswers();
const uploadTmpMediaMutation = useUploadTmpMediaMutation({ const uploadTmpMediaMutation = useUploadTmpMediaMutation({
onSuccess: (response) => { onSuccess: (response) => {
setAnswerValue(question, questionIndex, response.path);
if (response.path) {
setAnswerValue(question, questionIndex, response.path);
}
}, },
onError: () => {
setAnswerValue(question, questionIndex, null);
onError: (error) => {
console.error("File upload error:", error);
}, },
}); });
@ -72,28 +110,32 @@ export function QuestionFile({
break; break;
case "picked": case "picked":
if (event.data?.files?.[0]) { if (event.data?.files?.[0]) {
setSelectedFileName(event.data.files[0].name ?? null);
const fileName = event.data.files[0].name ?? null;
setSelectedFileName(fileName);
if (fileName) {
setAnswerValue(question, questionIndex, fileName);
}
} }
break; break;
case "progress": case "progress":
// Could show progress bar – for now just keep pending state
break; break;
case "completed": { case "completed": {
setIsFlutterPicking(false); setIsFlutterPicking(false);
const file = event.data?.files?.[0]; const file = event.data?.files?.[0];
if (file?.url) { if (file?.url) {
// Flutter uploaded the file – use the returned URL
setAnswerValue(question, questionIndex, file.url); setAnswerValue(question, questionIndex, file.url);
setSelectedFileName(file.name ?? "uploaded"); setSelectedFileName(file.name ?? "uploaded");
setFilePreviewUrl(file.url);
} else if (file?.base64) { } else if (file?.base64) {
// base64 mode – convert to File and use existing mutation
fetch(file.base64)
const b64 = file.base64;
setSelectedFileName(file.name ?? "upload");
setFilePreviewUrl(b64);
fetch(b64)
.then((res) => res.blob()) .then((res) => res.blob())
.then((blob) => { .then((blob) => {
const f = new File([blob], file.name ?? "upload", { const f = new File([blob], file.name ?? "upload", {
type: blob.type, type: blob.type,
}); });
setSelectedFileName(f.name);
uploadTmpMediaMutation.mutate(f); uploadTmpMediaMutation.mutate(f);
}); });
} }
@ -112,12 +154,11 @@ export function QuestionFile({
return () => { return () => {
unsubscribe?.(); unsubscribe?.();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [question, questionIndex]);
}, [question, questionIndex, setAnswerValue, uploadTmpMediaMutation]);
/** Handle file pick in Flutter WebView via upload_file action. */ /** Handle file pick in Flutter WebView via upload_file action. */
const handleFlutterPick = useCallback(() => { const handleFlutterPick = useCallback(() => {
const extensions = question.extras.options.map((o) =>
const extensions = (question.extras?.options ?? []).map((o) =>
o.replace(/^\./, "").toLowerCase(), o.replace(/^\./, "").toLowerCase(),
); );
const mediaType = resolveMediaType(extensions); const mediaType = resolveMediaType(extensions);
@ -140,18 +181,52 @@ export function QuestionFile({
if (!file) { if (!file) {
setSelectedFileName(null); setSelectedFileName(null);
setFilePreviewUrl(null);
setAnswerValue(question, questionIndex, null); setAnswerValue(question, questionIndex, null);
return; return;
} }
setSelectedFileName(file.name); setSelectedFileName(file.name);
setAnswerValue(question, questionIndex, file.name);
if (file.type.startsWith("image/")) {
const objectUrl = URL.createObjectURL(file);
setFilePreviewUrl(objectUrl);
} else {
setFilePreviewUrl(null);
}
uploadTmpMediaMutation.mutate(file); uploadTmpMediaMutation.mutate(file);
} }
const handleRemoveFile = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
setSelectedFileName(null);
setFilePreviewUrl(null);
setAnswerValue(question, questionIndex, null);
};
const inWebView = isInFlutterWebView(); const inWebView = isInFlutterWebView();
const isUploaded = Boolean(selectedFileName || storedValue);
const currentFileName =
selectedFileName ??
(typeof storedValue === "string" ? storedValue.split("/").pop() : null);
const currentFileUrl =
filePreviewUrl ??
(typeof storedValue === "string" && storedValue.trim().length > 0
? storedValue.startsWith("http") ||
storedValue.startsWith("blob:") ||
storedValue.startsWith("data:")
? storedValue
: getApiRequestUrl(storedValue)
: null);
const isImg = isImageFile(currentFileName, currentFileUrl);
return ( return (
<div <div
data-question-answered={isUploaded ? "true" : "false"}
className={[ className={[
"flex w-full flex-col gap-2 transition-opacity duration-200", "flex w-full flex-col gap-2 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "", disabled ? "pointer-events-none opacity-30" : "",
@ -159,48 +234,120 @@ export function QuestionFile({
> >
<QuestionTitle question={question} /> <QuestionTitle question={question} />
<span <span
className="relative flex aspect-[727/330] min-h-[156px] w-full cursor-pointer flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#6F6F6F] hover:border-[#777777]"
className="relative flex aspect-[727/330] min-h-[156px] w-full cursor-pointer flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#6F6F6F] hover:border-[#777777] overflow-hidden p-4"
role={inWebView ? "button" : undefined} role={inWebView ? "button" : undefined}
tabIndex={inWebView ? 0 : undefined} tabIndex={inWebView ? 0 : undefined}
onClick={inWebView ? handleFlutterPick : undefined}
onClick={inWebView && !isUploaded ? handleFlutterPick : undefined}
onKeyDown={ onKeyDown={
inWebView
inWebView && !isUploaded
? (e) => { ? (e) => {
if (e.key === "Enter" || e.key === " ") handleFlutterPick(); if (e.key === "Enter" || e.key === " ") handleFlutterPick();
} }
: undefined : undefined
} }
> >
{/* Fallback: browser file input (hidden in WebView) */}
{!inWebView && (
{/* Fallback: browser file input (hidden in WebView or when uploaded) */}
{!inWebView && !isUploaded && (
<input <input
type="file" type="file"
accept={question.extras.options.join(",")}
disabled={disabled || isPending}
accept={acceptedFiles ? acceptedFiles : "*/*"}
disabled={disabled}
onChange={(event) => handleBrowserFileChange(event.target.files)} onChange={(event) => handleBrowserFileChange(event.target.files)}
className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0"
/> />
)} )}
<Image
src="/assets/images/Image.svg"
alt="Upload"
width={24}
height={24}
/>
<span className="mt-3 block text-sm leading-none font-normal text-[#111111]">
{isPending
? "uploading..."
: (selectedFileName ?? "upload certificates")}
</span>
{uploadTmpMediaMutation.isError ? (
<span className="mt-2 block text-[10px] leading-none font-bold text-[#D44747]">
Upload failed. Please try again.
</span>
) : acceptedFiles ? (
<span className="mt-2 block text-[10px] leading-none font-bold text-[#8B8B8B]">
{acceptedFiles}
</span>
) : null}
{isUploaded ? (
/* ────── UPLOADED STATE ────── */
<div className="relative flex h-full w-full flex-col items-center justify-center">
{isImg && currentFileUrl ? (
/* Image Preview (Left design in screenshot) */
<img
src={currentFileUrl}
alt={currentFileName ?? "Uploaded image"}
className="max-h-[120px] max-w-[85%] rounded-[12px] object-contain shadow-xs"
/>
) : (
/* Document / PDF Preview (Right design in screenshot) */
<div className="flex flex-col items-center justify-center gap-2">
<div className="relative flex h-14 w-11 items-center justify-center rounded-[6px] border border-[#D1D5DB] bg-white shadow-xs">
<svg
width="28"
height="32"
viewBox="0 0 32 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 0C1.79086 0 0 1.79086 0 4V32C0 34.2091 1.79086 36 4 36H28C30.2091 36 32 34.2091 32 32V10L22 0H4Z"
fill="#E5E7EB"
/>
<path d="M22 0V10H32L22 0Z" fill="#9CA3AF" />
</svg>
<span className="absolute bottom-1 rounded-[3px] bg-[#F0445B] px-1 py-[1px] text-[8px] font-bold text-white leading-none">
PDF
</span>
</div>
<span className="max-w-[240px] truncate text-xs font-semibold text-[#111111]">
{currentFileName ?? "document"}
</span>
</div>
)}
{/* Trash Button in Bottom-Right */}
<button
type="button"
onClick={handleRemoveFile}
title="Remove file"
className="absolute bottom-0 right-0 z-20 flex h-8 w-8 items-center justify-center rounded-full bg-[#EAEAEA] text-[#36363C] transition-colors hover:bg-[#DDD] active:scale-95 cursor-pointer shadow-xs"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
</svg>
</button>
{isPending && (
<div className="absolute inset-0 z-30 flex items-center justify-center rounded-[29px] bg-black/40 backdrop-blur-[1px]">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-white border-t-transparent" />
</div>
)}
</div>
) : (
/* ────── DEFAULT EMPTY STATE ────── */
<>
<Image
src="/assets/images/Image.svg"
alt="Upload"
width={24}
height={24}
/>
<span className="mt-3 block group-12 leading-none font-normal text-[#111111]">
{isPending
? "uploading..."
: (selectedFileName ?? "upload certificates")}
</span>
{uploadTmpMediaMutation.isError ? (
<span className="mt-2 block group-10 leading-none font-bold text-[#D44747]">
Upload failed. Please try again.
</span>
) : acceptedFiles ? (
<span className="mt-2 block group-10 leading-none font-bold text-[#8B8B8B]">
{acceptedFiles}
</span>
) : null}
</>
)}
</span> </span>
</div> </div>
); );
@ -208,3 +355,5 @@ export function QuestionFile({
export default QuestionFile; export default QuestionFile;

6
src/components/questions/question-phone.tsx

@ -273,7 +273,7 @@ export function QuestionPhone({
setCodeValue(nextCodeValue); setCodeValue(nextCodeValue);
updateStoredValue(nextCodeValue, phoneValue); updateStoredValue(nextCodeValue, phoneValue);
}} }}
className="w-[33px] border-0 bg-transparent p-0 text-left text-[14px] leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
className="w-[33px] border-0 bg-transparent p-0 text-left group-12 leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
/> />
<span aria-hidden="true" className="h-5 w-px bg-[#181818]/35" /> <span aria-hidden="true" className="h-5 w-px bg-[#181818]/35" />
</div> </div>
@ -291,12 +291,12 @@ export function QuestionPhone({
updateStoredValue(codeValue, nextPhoneValue); updateStoredValue(codeValue, nextPhoneValue);
}} }}
dir="ltr" dir="ltr"
className="h-full w-full border-0 bg-transparent p-0 text-left text-[14px] leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
className="h-full w-full border-0 bg-transparent p-0 text-left group-12 leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
/> />
</span> </span>
</div> </div>
{showInvalidState ? ( {showInvalidState ? (
<span className="block text-[10px] font-semibold text-[#F2465F]">
<span className="block group-10 font-semibold text-[#F2465F]">
Enter a valid phone number with country code. Enter a valid phone number with country code.
</span> </span>
) : null} ) : null}

231
src/components/questions/question-photo.tsx

@ -1,8 +1,11 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
import type { ReactNode } from "react";
import { useCallback, useEffect, useId, useState, type ReactNode } from "react";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media";
import { getApiRequestUrl } from "@/lib/http";
import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions";
import { useQuestionAnswers } from "./question-answer-storage"; import { useQuestionAnswers } from "./question-answer-storage";
type QuestionPhotoProps = { type QuestionPhotoProps = {
@ -12,65 +15,231 @@ type QuestionPhotoProps = {
disabled?: boolean; disabled?: boolean;
}; };
function getFileInputValue(files: FileList | null) {
const fileNames = Array.from(files ?? []).map((file) => file.name);
return fileNames.length > 0 ? fileNames.join(", ") : null;
}
export function QuestionPhoto({ export function QuestionPhoto({
question, question,
questionIndex, questionIndex,
description, description,
disabled, disabled,
}: QuestionPhotoProps) { }: QuestionPhotoProps) {
const acceptedFiles = question.extras.options.join(",");
const inputId = useId();
const [localPreviewUrl, setLocalPreviewUrl] = useState<string | null>(null);
const [isFlutterPicking, setIsFlutterPicking] = useState(false);
const acceptedFiles =
question.extras?.options && question.extras.options.length > 0
? question.extras.options.join(",")
: "image/*";
const descriptionContent = description ?? question.description; const descriptionContent = description ?? question.description;
const { setAnswerValue } = useQuestionAnswers();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question, questionIndex);
const uploadTmpMediaMutation = useUploadTmpMediaMutation({
onSuccess: (response) => {
if (response.path) {
setAnswerValue(question, questionIndex, response.path);
}
},
onError: (error) => {
console.error("Photo upload error:", error);
},
});
const isPending = uploadTmpMediaMutation.isPending || isFlutterPicking;
// Listen for upload_file responses from Flutter WebView if active
useEffect(() => {
if (!isInFlutterWebView()) return;
const unsubscribe = window.addFlutterResponseListener?.((event) => {
if (event.action !== "upload_file") return;
switch (event.status) {
case "picking":
setIsFlutterPicking(true);
break;
case "picked":
if (event.data?.files?.[0]?.base64) {
const b64 = event.data.files[0].base64;
setLocalPreviewUrl(b64);
setAnswerValue(question, questionIndex, b64);
}
break;
case "completed": {
setIsFlutterPicking(false);
const file = event.data?.files?.[0];
if (file?.url) {
setAnswerValue(question, questionIndex, file.url);
setLocalPreviewUrl(file.url);
} else if (file?.base64) {
const b64 = file.base64;
setLocalPreviewUrl(b64);
setAnswerValue(question, questionIndex, b64);
fetch(b64)
.then((res) => res.blob())
.then((blob) => {
const f = new File([blob], file.name ?? "photo.jpg", {
type: blob.type || "image/jpeg",
});
uploadTmpMediaMutation.mutate(f);
});
}
break;
}
case "cancelled":
case "failed":
setIsFlutterPicking(false);
break;
}
});
return () => {
unsubscribe?.();
};
}, [question, questionIndex, setAnswerValue, uploadTmpMediaMutation]);
const handleFlutterPick = useCallback(() => {
const extensions = (question.extras?.options ?? []).map((o) =>
o.replace(/^\./, "").toLowerCase(),
);
uploadFile({
mediaType: "image",
source: "gallery",
returnAs: "upload",
uploadUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/upload-tmp-media/`,
fieldName: "file",
allowedExtensions: extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"],
maxBytes: 10_485_760, // 10 MB
title: typeof question.title === "string" ? question.title : "Upload Photo",
});
}, [question]);
const handleBrowserFileChange = (files: FileList | null) => {
const file = files?.[0];
if (!file) return;
// Create synchronous object URL for instant preview & immediate state update
const objectUrl = URL.createObjectURL(file);
setLocalPreviewUrl(objectUrl);
setAnswerValue(question, questionIndex, objectUrl);
// Trigger background upload
uploadTmpMediaMutation.mutate(file);
};
const handleLabelClick = (e: React.MouseEvent) => {
if (isInFlutterWebView()) {
e.preventDefault();
handleFlutterPick();
}
};
// Determine photo URL to display
let displayUrl: string | null = localPreviewUrl;
if (!displayUrl && typeof storedValue === "string" && storedValue.trim().length > 0) {
const val = storedValue.trim();
if (
val.startsWith("http://") ||
val.startsWith("https://") ||
val.startsWith("blob:") ||
val.startsWith("data:")
) {
displayUrl = val;
} else {
displayUrl = getApiRequestUrl(val);
}
}
const hasAnswer = Boolean(displayUrl);
return ( return (
<div <div
data-question-answered={hasAnswer ? "true" : "false"}
className={[ className={[
"flex w-full flex-col items-center text-center transition-opacity duration-200", "flex w-full flex-col items-center text-center transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "", disabled ? "pointer-events-none opacity-30" : "",
].join(" ")} ].join(" ")}
> >
<input <input
id={inputId}
type="file" type="file"
accept={acceptedFiles} accept={acceptedFiles}
onChange={(event) => setAnswerValue(question, questionIndex, getFileInputValue(event.target.files))}
onChange={(event) => handleBrowserFileChange(event.target.files)}
disabled={disabled} disabled={disabled}
className="sr-only" className="sr-only"
id={`photo-input-${questionIndex}`}
/> />
<label <label
htmlFor={`photo-input-${questionIndex}`}
htmlFor={inputId}
onClick={handleLabelClick}
className="flex w-full cursor-pointer flex-col items-center" className="flex w-full cursor-pointer flex-col items-center"
> >
<span className="flex w-full flex-col items-center">
<Image
src="/assets/images/Frame 2095586679.svg"
alt=""
aria-hidden="true"
width={86}
height={92}
className="h-[92px] w-[86px]"
/>
<span className="mt-4 block text-xs leading-none font-semibold text-[#36363C]">
{question.title}
{question.required ? (
<span className="ml-1 text-[#F33D52]">*</span>
<span className="flex w-full flex-col items-center">
<div className="relative flex h-[92px] w-[86px] items-center justify-center">
{displayUrl ? (
<img
src={displayUrl}
alt={typeof question.title === "string" ? question.title : ""}
className="h-[86px] w-[86px] rounded-full object-cover border border-[#D7DBE2]"
/>
) : (
<Image
src="/assets/images/Frame 2095586679.svg"
alt=""
aria-hidden="true"
width={86}
height={92}
className="h-[92px] w-[86px]"
/>
)}
{/* Red camera badge when image is uploaded */}
{displayUrl && (
<span className="absolute bottom-[2px] right-0 flex h-7 w-7 items-center justify-center rounded-full bg-[#F0445B]">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-white"
>
<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" />
<circle cx="12" cy="13" r="3" />
</svg>
</span>
)}
{/* Loading spinner during upload */}
{isPending && (
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 backdrop-blur-[1px]">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-white border-t-transparent" />
</div>
)}
</div>
<span className="mt-4 block group-14 leading-none font-semibold text-[#36363C]">
{question.title}
{question.required ? (
<span className="ml-1 text-[#F33D52]">*</span>
) : null}
</span>
{descriptionContent ? (
<span className="mt-4 block max-w-[350px] group-10 leading-[1.35] font-semibold text-[#D44747]">
{descriptionContent}
</span>
) : null} ) : null}
</span> </span>
{descriptionContent ? (
<span className="mt-4 block max-w-[350px] text-xs leading-[1.35] font-semibold text-[#D44747]">
{descriptionContent}
</span>
) : null}
</span>
</label> </label>
</div> </div>
); );
} }
export default QuestionPhoto; export default QuestionPhoto;

4
src/components/questions/question-progress-tracker.tsx

@ -93,7 +93,7 @@ export function QuestionProgressTracker({
>(() => new Set()); >(() => new Set());
const safeTotal = Math.max(total, 0); const safeTotal = Math.max(total, 0);
const progress = safeTotal > 0 ? (answered / safeTotal) * 100 : 0; const progress = safeTotal > 0 ? (answered / safeTotal) * 100 : 0;
const isCompleted = safeTotal > 0 && answered >= safeTotal;
const isCompleted = safeTotal === 0 || answered >= safeTotal;
const markQuestionPassed = useCallback((questionIndex: number) => { const markQuestionPassed = useCallback((questionIndex: number) => {
setPassedQuestionIndexes((currentIndexes) => { setPassedQuestionIndexes((currentIndexes) => {
@ -180,7 +180,7 @@ export function QuestionProgressTracker({
role="progressbar" role="progressbar"
className="w-full bg-[#F7F1F0]" className="w-full bg-[#F7F1F0]"
> >
<div className="mb-[7px] flex items-center justify-between text-xs leading-none font-normal text-[#747474]">
<div className="mb-[7px] flex items-center justify-between group-10 leading-none font-normal text-[#747474]">
<span>fields to complete</span> <span>fields to complete</span>
<span> <span>
{answered} /{safeTotal} {answered} /{safeTotal}

2
src/components/questions/question-radio.tsx

@ -45,7 +45,7 @@ export function QuestionRadio({
htmlFor={optionId} htmlFor={optionId}
className={[ className={[
"w-full cursor-pointer rounded-[12px] px-4 py-3.5", "w-full cursor-pointer rounded-[12px] px-4 py-3.5",
"text-start text-[13px] leading-tight font-semibold transition-all duration-200",
"text-start group-14 leading-tight font-semibold transition-all duration-200",
isSelected isSelected
? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]" ? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]"
: "bg-[#F5F5F5] text-[#181818] hover:bg-[#EBEBEB]", : "bg-[#F5F5F5] text-[#181818] hover:bg-[#EBEBEB]",

4
src/components/questions/question-slider.tsx

@ -77,7 +77,7 @@ export function QuestionSlider({
<div className="pt-9"> <div className="pt-9">
<div ref={sliderWrapperRef} className="relative"> <div ref={sliderWrapperRef} className="relative">
<div <div
className="-translate-x-1/2 absolute -top-6.5 z-10 flex items-center justify-center rounded-[7px] bg-[#F43F5E] px-2.5 font-semibold text-white shadow-sm after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F43F5E] after:content-['']"
className="-translate-x-1/2 absolute -top-6.5 z-10 flex items-center justify-center rounded-[7px] bg-[#F43F5E] px-2.5 group-12 font-semibold text-white shadow-sm after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F43F5E] after:content-['']"
style={{ left: `${bubblePosition}px` }} style={{ left: `${bubblePosition}px` }}
> >
{value} {value}
@ -97,7 +97,7 @@ export function QuestionSlider({
}} }}
/> />
</div> </div>
<div className="mt-2 grid grid-flow-col auto-cols-fr text-center text-[8.5px] text-[#B7B7B7]">
<div className="mt-2 grid grid-flow-col auto-cols-fr text-center group-10 text-[#B7B7B7]">
{steps.map((step) => ( {steps.map((step) => (
<span key={step}>{step}</span> <span key={step}>{step}</span>
))} ))}

4
src/components/questions/question-snap-list.tsx

@ -242,9 +242,9 @@ export function QuestionSnapList({
let wrapperStyles = "w-full"; let wrapperStyles = "w-full";
if (isActive) { if (isActive) {
containerStyles = "top-1/2 left-0 -translate-y-1/2 z-10 opacity-100 scale-100 pointer-events-auto";
containerStyles = "top-[35%] left-0 -translate-y-1/2 z-10 opacity-100 scale-100 pointer-events-auto";
} else if (isPrev) { } else if (isPrev) {
containerStyles = "top-4 left-0 z-0 opacity-20 pointer-events-none scale-[0.98]";
containerStyles = "top-4 left-0 z-0 opacity-0 pointer-events-none scale-[0.98]";
wrapperStyles = "w-full max-h-[110px] overflow-hidden pointer-events-none"; wrapperStyles = "w-full max-h-[110px] overflow-hidden pointer-events-none";
} else if (isNext) { } else if (isNext) {
containerStyles = "bottom-4 left-0 z-0 opacity-20 pointer-events-none scale-[0.98]"; containerStyles = "bottom-4 left-0 z-0 opacity-20 pointer-events-none scale-[0.98]";

10
src/components/questions/question-text.tsx

@ -59,18 +59,18 @@ export default function QuestionText({
onChange={(e) => setAnswerValue(question, questionIndex, e.target.value)} onChange={(e) => setAnswerValue(question, questionIndex, e.target.value)}
placeholder={question.extras.placeHolder} placeholder={question.extras.placeHolder}
disabled={disabled} disabled={disabled}
className="h-full w-full border-0 bg-transparent px-[18px] text-[14px] leading-none text-[#181818] outline-none placeholder:text-[#9D8F8C]"
className="h-full w-full border-0 bg-transparent px-[18px] group-12 leading-none text-[#181818] outline-none placeholder:text-[#9D8F8C]"
/> />
</div> </div>
{showInvalidState ? ( {showInvalidState ? (
<span className="block text-[10px] font-semibold text-[#F2465F]">
<span className="block group-10 font-semibold text-[#F2465F]">
{/[\u0600-\u06FF]/.test(question.title) {/[\u0600-\u06FF]/.test(question.title)
? "یک آدرس ایمیل معتبر وارد کنید." ? "یک آدرس ایمیل معتبر وارد کنید."
: "Enter a valid email address."} : "Enter a valid email address."}
</span> </span>
) : null} ) : null}
{description ? ( {description ? (
<span className="block text-[10px] font-semibold text-[#747474]">
<span className="block group-10 font-semibold text-[#747474]">
{description} {description}
</span> </span>
) : null} ) : null}
@ -94,10 +94,10 @@ export default function QuestionText({
onChange={(e) => setAnswerValue(question, questionIndex, e.target.value)} onChange={(e) => setAnswerValue(question, questionIndex, e.target.value)}
placeholder={question.extras.placeHolder} placeholder={question.extras.placeHolder}
disabled={disabled} disabled={disabled}
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 text-[14px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#9D8F8C] focus:border-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 group-12 font-medium text-[#181818] outline-none transition-all placeholder:text-[#9D8F8C] focus:border-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
/> />
{description ? ( {description ? (
<span className="block text-[10px] font-semibold text-[#747474]">
<span className="block group-10 font-semibold text-[#747474]">
{description} {description}
</span> </span>
) : null} ) : null}

8
src/components/questions/question-title.tsx

@ -10,7 +10,7 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
return ( return (
<span className="block"> <span className="block">
{question.private ? ( {question.private ? (
<span className="mb-3 mx-auto flex w-fit items-center gap-1.5 rounded-[10px] bg-[#F6D7D8] px-2.5 py-1.5 text-[10px] leading-tight font-semibold text-[#DF4146]">
<span className="mb-3 mx-auto flex w-fit items-center gap-1.5 rounded-[10px] bg-[#F6D7D8] px-2.5 py-1.5 group-10 leading-tight font-semibold text-[#DF4146]">
<IoEyeOff <IoEyeOff
aria-hidden="true" aria-hidden="true"
className="shrink-0 text-[18px] text-[#DF4146]" className="shrink-0 text-[18px] text-[#DF4146]"
@ -21,7 +21,7 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
<span <span
className={[ className={[
"block text-[12px] leading-tight font-semibold text-[#181818]",
"block group-14 leading-tight font-semibold text-[#181818]",
className, className,
] ]
.filter(Boolean) .filter(Boolean)
@ -29,12 +29,12 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) {
> >
{question.title} {question.title}
{question.description ? ( {question.description ? (
<span className="ml-1 text-[10px] font-semibold text-[#747474]">
<span className="ml-1 group-10 font-semibold text-[#747474]">
({question.description}) ({question.description})
</span> </span>
) : null} ) : null}
{question.required ? ( {question.required ? (
<span className="ml-1 text-[14px] text-[#F33D52]">*</span>
<span className="ml-1 group-14 text-[#F33D52]">*</span>
) : null} ) : null}
</span> </span>
</span> </span>

6
src/components/questions/required-steps-card.tsx

@ -104,12 +104,12 @@ export default function RequiredStepsCard() {
<IoAlert aria-hidden="true" className="text-[16px]" /> <IoAlert aria-hidden="true" className="text-[16px]" />
)} )}
</span> </span>
<h2 className="leading-none font-bold tracking-[-0.02em]">
<h2 className="group-16 leading-none font-bold tracking-[-0.02em]">
{t.questions.requiredSteps} {t.questions.requiredSteps}
</h2> </h2>
</div> </div>
<p className="mt-2.5 max-w-[220px] text-[10px] leading-[1.18] font-semibold text-white/70">
<p className="mt-2.5 max-w-[220px] group-12 leading-[1.25] font-medium text-white/80">
{isCompleted {isCompleted
? t.questions.requiredStepsDescriptionCompleted ? t.questions.requiredStepsDescriptionCompleted
: t.questions.requiredStepsDescription} : t.questions.requiredStepsDescription}
@ -127,7 +127,7 @@ export default function RequiredStepsCard() {
}} }}
> >
<div className="absolute inset-[6px] rounded-full bg-[#40506A]" /> <div className="absolute inset-[6px] rounded-full bg-[#40506A]" />
<span className="relative leading-none font-bold tracking-[-0.02em]">
<span className="relative group-14 leading-none font-bold tracking-[-0.02em]">
{completed}/{total} {completed}/{total}
</span> </span>
</div> </div>

10
src/components/sliders/slider-slide-four.tsx

@ -27,13 +27,13 @@ export function SliderSlideFour({
> >
<div className="min-h-0 flex-1 overflow-y-auto pb-28"> <div className="min-h-0 flex-1 overflow-y-auto pb-28">
<div className="text-center"> <div className="text-center">
<p className="text-[#747474] text-xs font-semibold">Submit Process</p>
<p className="text-[#111111] font-bold">
{index + 1}/4 terms & conditions
<p className="text-[#747474] group-10 font-semibold">Submit Process</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 Registration Type
</p> </p>
</div> </div>
<div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-8"> <div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-8">
<p className="text-[#F14B46] text-xs font-semibold text-center">
<p className="text-[#F14B46] group-12 font-semibold text-center">
Read the terms and conditions carefully so that you don't run into Read the terms and conditions carefully so that you don't run into
any problems in the next process - reading this section is any problems in the next process - reading this section is
mandatory. mandatory.
@ -82,7 +82,7 @@ export function SliderSlideFour({
/> />
</svg> </svg>
</span> </span>
<span className="text-[15px] font-semibold sm:text-[16px]">
<span className="group-14 font-semibold">
{option.label} {option.label}
</span> </span>
</button> </button>

24
src/components/sliders/slider-slide-one.tsx

@ -19,13 +19,13 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
}} }}
> >
<div className="text-center"> <div className="text-center">
<p className="text-[#747474] text-xs font-semibold">Submit Process</p>
<p className="text-[#111111] font-bold">
<p className="text-[#747474] group-10 font-semibold">Submit Process</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 terms & conditions {index + 1}/4 terms & conditions
</p> </p>
</div> </div>
<div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-8"> <div className="bg-[#F14B46]/10 py-2.5 px-3.5 border border-[#F14B46] rounded-xl mt-8">
<p className="text-[#F14B46] text-xs font-semibold text-center">
<p className="text-[#F14B46] group-12 font-semibold text-center">
Dear user, while welcoming you to the Habib Marij application, Dear user, while welcoming you to the Habib Marij application,
please read the following rules and regulations carefully before please read the following rules and regulations carefully before
using the services of this platform. Your registration and use of using the services of this platform. Your registration and use of
@ -35,11 +35,11 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
<div className="bg-white rounded-2xl mt-4 overflow-y-auto shadow-xs"> <div className="bg-white rounded-2xl mt-4 overflow-y-auto shadow-xs">
<div className="space-y-6 px-5 py-5"> <div className="space-y-6 px-5 py-5">
<div className="space-y-3.5"> <div className="space-y-3.5">
<h3 className="text-[#F14B46] text-[16px] font-bold tracking-tight mb-3">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
1. Eligibility and Membership Requirements 1. Eligibility and Membership Requirements
</h3> </h3>
<p className="text-[13px] leading-[1.55]">
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Legal Age:</span>{" "} <span className="font-bold text-[#262626]">Legal Age:</span>{" "}
<span className="font-normal text-[#525252]"> <span className="font-normal text-[#525252]">
Users must meet the minimum legal age for independent Users must meet the minimum legal age for independent
@ -47,7 +47,7 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
</span> </span>
</p> </p>
<p className="text-[13px] leading-[1.55]">
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Identity Verification:</span>{" "} <span className="font-bold text-[#262626]">Identity Verification:</span>{" "}
<span className="font-normal text-[#525252]"> <span className="font-normal text-[#525252]">
Mandatory submission of valid government-issued ID upon Mandatory submission of valid government-issued ID upon
@ -55,7 +55,7 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
</span> </span>
</p> </p>
<p className="text-[13px] leading-[1.55]">
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Single Status Commitment:</span>{" "} <span className="font-bold text-[#262626]">Single Status Commitment:</span>{" "}
<span className="font-normal text-[#525252]"> <span className="font-normal text-[#525252]">
Users must provide proof of being single, or documents Users must provide proof of being single, or documents
@ -63,7 +63,7 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
</span> </span>
</p> </p>
<p className="text-[13px] leading-[1.55]">
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Intent:</span>{" "} <span className="font-bold text-[#262626]">Intent:</span>{" "}
<span className="font-normal text-[#525252]"> <span className="font-normal text-[#525252]">
Commitment to monogamy and intention for permanent marriage Commitment to monogamy and intention for permanent marriage
@ -71,7 +71,7 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
</span> </span>
</p> </p>
<p className="text-[13px] leading-[1.55]">
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">General Health:</span>{" "} <span className="font-bold text-[#262626]">General Health:</span>{" "}
<span className="font-normal text-[#525252]"> <span className="font-normal text-[#525252]">
Self-declaration regarding mental health, absence of Self-declaration regarding mental health, absence of
@ -81,11 +81,11 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
</div> </div>
<div className="space-y-3.5 pt-1"> <div className="space-y-3.5 pt-1">
<h3 className="text-[#F14B46] text-[16px] font-bold tracking-tight mb-3">
<h3 className="text-[#F14B46] group-14 font-bold tracking-tight mb-3">
2. Privacy and Data Management 2. Privacy and Data Management
</h3> </h3>
<p className="text-[13px] leading-[1.55]">
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Content Security:</span>{" "} <span className="font-bold text-[#262626]">Content Security:</span>{" "}
<span className="font-normal text-[#525252]"> <span className="font-normal text-[#525252]">
Technical prevention of screenshots from profiles and chat Technical prevention of screenshots from profiles and chat
@ -93,7 +93,7 @@ export function SliderSlideOne({ index }: SliderSlideProps) {
</span> </span>
</p> </p>
<p className="text-[13px] leading-[1.55]">
<p className="group-12 leading-[1.55]">
<span className="font-bold text-[#262626]">Progressive Disclosure:</span>{" "} <span className="font-bold text-[#262626]">Progressive Disclosure:</span>{" "}
<span className="font-normal text-[#525252]"> <span className="font-normal text-[#525252]">
Sensitive information (face, contact details) revealed Sensitive information (face, contact details) revealed

8
src/components/sliders/slider-slide-three.tsx

@ -47,9 +47,9 @@ export function SliderSlideThree({
className="flex h-full min-h-0 w-full shrink-0 flex-col justify-between" className="flex h-full min-h-0 w-full shrink-0 flex-col justify-between"
> >
<div className="text-center"> <div className="text-center">
<p className="text-[#747474] text-xs font-semibold">Submit Process</p>
<p className="text-[#111111] font-bold">
{index + 1}/4 terms & conditions
<p className="text-[#747474] group-10 font-semibold">Submit Process</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 Select Gender
</p> </p>
</div> </div>
@ -90,7 +90,7 @@ export function SliderSlideThree({
</span> </span>
<p <p
className={[ className={[
"mt-2 text-center text-sm font-semibold transition-colors duration-300 ease-out",
"mt-2 text-center group-14 font-semibold transition-colors duration-300 ease-out",
isSelected ? "text-[#FF6175]" : "text-[#111111]", isSelected ? "text-[#FF6175]" : "text-[#111111]",
].join(" ")} ].join(" ")}
> >

10
src/components/sliders/slider-slide-two.tsx

@ -8,9 +8,9 @@ export function SliderSlideTwo({ index }: SliderSlideProps) {
className="flex h-full min-h-0 w-full shrink-0 flex-col" className="flex h-full min-h-0 w-full shrink-0 flex-col"
> >
<div className="text-center"> <div className="text-center">
<p className="text-[#747474] text-xs font-semibold">Submit Process</p>
<p className="text-[#111111] font-bold">
{index + 1}/4 terms & conditions
<p className="text-[#747474] group-10 font-semibold">Submit Process</p>
<p className="text-[#111111] group-16 font-bold">
{index + 1}/4 Watch Video
</p> </p>
</div> </div>
<div className="mt-8 flex min-h-0 justify-center"> <div className="mt-8 flex min-h-0 justify-center">
@ -40,11 +40,11 @@ export function SliderSlideTwo({ index }: SliderSlideProps) {
height={20} height={20}
className="rounded-full" className="rounded-full"
/> />
<p className="text-[#2F3A48] text-sm font-semibold">
<p className="text-[#2F3A48] group-14 font-semibold">
Dr. Hasti Masoudi Dr. Hasti Masoudi
</p> </p>
</div> </div>
<p className="text-xs text-[#4D4D4D] text-center mt-2.5">
<p className="group-12 text-[#4D4D4D] text-center mt-2.5">
We have come together with the goal of creating a secure and We have come together with the goal of creating a secure and
confidential path for "permanent marriage" among Muslims confidential path for "permanent marriage" among Muslims
</p> </p>

6
src/components/ui/advisor-actions-card.tsx

@ -30,10 +30,10 @@ export function AdvisorActionsCard({
return ( return (
<section className={["space-y-3", className].filter(Boolean).join(" ")}> <section className={["space-y-3", className].filter(Boolean).join(" ")}>
<div className="rounded-[13px] border border-white/80 bg-white/72 px-3 py-3.5 text-left shadow-[0_18px_45px_rgba(15,23,42,0.06)] backdrop-blur-sm"> <div className="rounded-[13px] border border-white/80 bg-white/72 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]">
<h2 className="group-16 leading-none font-bold text-[#1C1C1C]">
{title} {title}
</h2> </h2>
<p className="mt-2 max-w-[280px] text-[11px] leading-[1.45] font-semibold text-[#8A8A8A]">
<p className="mt-2 max-w-[280px] group-10 leading-[1.45] font-semibold text-[#8A8A8A]">
{description} {description}
</p> </p>
@ -53,7 +53,7 @@ export function AdvisorActionsCard({
/> />
</span> </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]">
<span className="-ml-1.5 flex h-[30px] w-[30px] items-center justify-center rounded-full border-2 border-white bg-[#EDEEF1] group-10 font-semibold text-[#1C1C1C]">
+{extraCount} +{extraCount}
</span> </span>
</div> </div>

6
src/components/ui/button.tsx

@ -152,13 +152,13 @@ export function Button({
<span className="flex w-full items-center justify-center gap-2"> <span className="flex w-full items-center justify-center gap-2">
{renderArrow("left")} {renderArrow("left")}
<span className="flex min-w-0 flex-col items-center justify-center"> <span className="flex min-w-0 flex-col items-center justify-center">
<span className="text-center text-[16px] font-semibold leading-none">
<span className="text-center group-16 font-semibold leading-none">
{children} {children}
</span> </span>
{description && !isOutlined ? ( {description && !isOutlined ? (
<span <span
id={countdownId} id={countdownId}
className="mt-1 text-center text-[10px] font-semibold leading-none text-white"
className="mt-1 text-center group-10 font-semibold leading-none text-white"
> >
{description} {description}
</span> </span>
@ -219,7 +219,7 @@ function CountdownProgress({ value, progress }: CountdownProgressProps) {
strokeDashoffset={dashOffset} strokeDashoffset={dashOffset}
/> />
</svg> </svg>
<span className="absolute inset-0 flex items-center justify-center text-[12px] font-semibold leading-none text-white">
<span className="absolute inset-0 flex items-center justify-center group-12 font-semibold leading-none text-white">
{value} {value}
</span> </span>
</span> </span>

10
src/components/ui/info-progress-card.tsx

@ -41,9 +41,9 @@ export function InfoProgressCard({
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h2 className="truncate text-[16px] leading-none font-bold tracking-[-0.03em] text-[#171717]">
<h2 className="truncate group-14 leading-none font-bold tracking-[-0.03em] text-[#171717]">
{title}{" "} {title}{" "}
<span className="text-[10px] font-bold text-[#FF6175]">
<span className="group-10 font-bold text-[#FF6175]">
{requiredLabel && "(Required)"} {requiredLabel && "(Required)"}
</span> </span>
</h2> </h2>
@ -67,7 +67,7 @@ export function InfoProgressCard({
<div <div
id={tooltipId} id={tooltipId}
role="tooltip" role="tooltip"
className="pointer-events-none absolute top-full right-0 z-10 mt-2 w-48 rounded-[10px] bg-[#171717] px-3 py-2 text-[11px] leading-4 font-medium text-white opacity-0 shadow-lg transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100"
className="pointer-events-none absolute top-full right-0 z-10 mt-2 w-48 rounded-[10px] bg-[#171717] px-3 py-2 group-10 leading-4 font-medium text-white opacity-0 shadow-lg transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100"
> >
{tooltip} {tooltip}
</div> </div>
@ -76,7 +76,7 @@ export function InfoProgressCard({
</div> </div>
<div className="mt-2 flex items-center justify-between"> <div className="mt-2 flex items-center justify-between">
<p className="text-[12px] font-semibold text-[#747474]">
<p className="group-10 font-semibold text-[#747474]">
Estimate time: {estimate} Estimate time: {estimate}
</p> </p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -117,7 +117,7 @@ export function InfoProgressCard({
className="mt-0.5" className="mt-0.5"
/> />
)} )}
<span className="text-[12px] font-semibold text-[#111111]">
<span className="group-10 font-semibold text-[#111111]">
{normalizedProgress}% {normalizedProgress}%
</span> </span>
</div> </div>

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

@ -259,14 +259,14 @@ export function InformationSheet({
<h2 <h2
className={[ className={[
resolvedIcon ? "mt-2.5" : "mt-0", resolvedIcon ? "mt-2.5" : "mt-0",
"w-full leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]",
"w-full group-16 leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]",
].join(" ")} ].join(" ")}
> >
{resolvedTitle} {resolvedTitle}
</h2> </h2>
{description ? ( {description ? (
<div className="mt-3 text-sm text-[#4D4D4D]">{description}</div>
<div className="mt-3 group-12 text-[#4D4D4D]">{description}</div>
) : null} ) : null}
{resolvedButtons ? ( {resolvedButtons ? (

9
src/components/ui/sticky-header.tsx

@ -3,7 +3,9 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useViewPaddings } from "@/hooks/use-view-paddings"; import { useViewPaddings } from "@/hooks/use-view-paddings";
const DESIGN_TOP_PADDING = 63;
// پدینگ بالای طراحی هدر (معادل pt-7 = 28px). روی دستگاه، safe-area + 4px جایگزین
// می‌شود (مثل صفحات بومی najm)، و طراحی اصلی به‌عنوان کف برای مرورگر حفظ می‌شود.
const DESIGN_TOP_PADDING = 28;
const SAFE_AREA_GAP = 4; const SAFE_AREA_GAP = 4;
type StickyHeaderProps = { type StickyHeaderProps = {
@ -29,10 +31,7 @@ export default function StickyHeader({
} }
className={[ className={[
sticky ? "sticky z-30" : "relative z-30", sticky ? "sticky z-30" : "relative z-30",
"rounded-b-[32px] bg-[linear-gradient(135deg,#E03950_0%,#FE6F82_100%)] px-[29px] pb-[29px] text-[14px]",
"[&_button]:size-[74px] [&_button]:shrink-0 [&_button]:rounded-[24px] [&_button]:p-0",
"[&_button_img]:size-8 [&_button_svg]:size-8",
"[&_h1]:min-w-0 [&_h1]:truncate [&_h1]:text-[14px] [&_h1]:font-semibold [&_h1]:leading-5",
"rounded-b-[15px] bg-[linear-gradient(135deg,#E03950_0%,#FE6F82_100%)] px-[17px] pb-5",
className, className,
] ]
.filter(Boolean) .filter(Boolean)

1
src/hooks/marriage/use-upload-tmp-media.ts

@ -18,7 +18,6 @@ export async function uploadTmpMedia(file: File) {
{ {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
"Content-Type": "multipart/form-data",
"X-CSRFToken": CSRF_TOKEN, "X-CSRFToken": CSRF_TOKEN,
}, },
}, },

100
src/i18n/locales/en/questions.json

@ -1406,92 +1406,26 @@
] ]
}, },
{ {
"title": "Katel Test",
"title": "Personality Test",
"icon": "layout-grid", "icon": "layout-grid",
"slug": "katel_test",
"slug": "personality_test",
"required": false, "required": false,
"estimateTime": "30 minutes",
"tooltip": "Katel test section.",
"estimateTime": "15 minutes",
"tooltip": "Personality Test",
"progress": 0, "progress": 0,
"description": "Captures Katel test responses.",
"questions": [
{
"title": "Katel Test Step 1",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the first Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
},
{
"title": "Katel Test Step 2",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the second Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
},
{
"title": "Katel Test Step 3",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the third Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
},
{
"title": "Katel Test Step 4",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the fourth Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
}
]
"description": "Personality Test",
"questions": []
},
{
"title": "Glasser 5 Needs Test",
"icon": "layout-grid",
"slug": "glasser_5_needs_test",
"required": false,
"estimateTime": "15 minutes",
"tooltip": "Glasser 5 Needs Test",
"progress": 0,
"description": "Glasser 5 Needs Test",
"questions": []
}, },
{ {
"title": "Future Spouse Criteria and Red Lines", "title": "Future Spouse Criteria and Red Lines",

100
src/i18n/locales/fa/questions.json

@ -1406,92 +1406,26 @@
] ]
}, },
{ {
"title": "آزمون کتل",
"title": "تست شخصیت شناسی",
"icon": "layout-grid", "icon": "layout-grid",
"slug": "katel_test",
"slug": "personality_test",
"required": false, "required": false,
"estimateTime": "30 minutes",
"tooltip": "Katel test section.",
"estimateTime": "15 دقیقه",
"tooltip": "تست شخصیت شناسی",
"progress": 0, "progress": 0,
"description": "Captures Katel test responses.",
"questions": [
{
"title": "مرحله ۱ آزمون کتل",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the first Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
},
{
"title": "مرحله ۲ آزمون کتل",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the second Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
},
{
"title": "مرحله ۳ آزمون کتل",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the third Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
},
{
"title": "مرحله ۴ آزمون کتل",
"type": "radio",
"required": false,
"description": "Select the option that applies.",
"tooltip": "This is the fourth Katel test step.",
"extras": {
"placeHolder": "Select one option",
"range": [
0,
0
],
"options": [
"Option A",
"Option B",
"Option C"
]
}
}
]
"description": "تست شخصیت شناسی",
"questions": []
},
{
"title": "تست ۵ نیاز گلاسر",
"icon": "layout-grid",
"slug": "glasser_5_needs_test",
"required": false,
"estimateTime": "15 دقیقه",
"tooltip": "تست ۵ نیاز گلاسر",
"progress": 0,
"description": "تست ۵ نیاز گلاسر",
"questions": []
}, },
{ {
"title": "بخش ۸: معیارها و خطوط قرمز همسر آینده", "title": "بخش ۸: معیارها و خطوط قرمز همسر آینده",

Loading…
Cancel
Save