Browse Source

feat: update InformationSheet styling and add phone number questions

- Adjusted the border radius of the InformationSheet component for a more modern look.
- Added new optional phone number fields for "Home Phone Number" and "Father's Phone Number" in English and Persian localization files.
- Introduced new SVG assets for various UI components.
- Created new components for handling phone number input and advisor actions.
- Implemented a FemaleConsentSheet component for user consent interactions.
- Added a terms page route and linked it to the main application.
master
sina_sajjadi 2 months ago
parent
commit
4c48238049
  1. 9
      public/assets/images/Ellipse 1210.svg
  2. 4
      public/assets/images/Frame 2095586fdas679.svg
  3. 9
      public/assets/images/Grfdasfoup.svg
  4. 21
      public/assets/images/Group 15978fdsa80467.svg
  5. 5
      public/assets/images/Groupfdas 2.svg
  6. 3
      public/assets/images/Vecfadsftor.svg
  7. 3
      public/assets/images/Vecfdastor.svg
  8. 14
      public/assets/images/boxicons_education-filled.svg
  9. 8
      public/assets/images/mingcute_user-info-fill.svg
  10. 14
      public/assets/images/noun-test-4525471 1.svg
  11. 9
      public/assets/images/solar_user-id-bold.svg
  12. 1
      src/app/[lang]/slider/page.tsx
  13. 1
      src/app/[lang]/terms/page.tsx
  14. 75
      src/app/finding-match/page.tsx
  15. 29
      src/app/intro/page.tsx
  16. 232
      src/app/new-match/page.tsx
  17. 167
      src/app/new-match/profile/page.tsx
  18. 5
      src/app/questions-list/[slug]/page.tsx
  19. 37
      src/app/questions-list/page.tsx
  20. 207
      src/app/request-accepted/page.tsx
  21. 22
      src/app/request-sent/page.tsx
  22. 2
      src/app/terms/page.tsx
  23. 2
      src/components/dev/locator-paths.ts
  24. 102
      src/components/questions/question-card.tsx
  25. 160
      src/components/questions/question-phone.tsx
  26. 4
      src/components/sliders/slider-slide-five.tsx
  27. 73
      src/components/ui/advisor-actions-card.tsx
  28. 24
      src/components/ui/call-result-sheet.tsx
  29. 159
      src/components/ui/female-consent-sheet.tsx
  30. 2
      src/components/ui/information-sheet.tsx
  31. 24
      src/i18n/locales/en/questions.json
  32. 24
      src/i18n/locales/fa/questions.json

9
public/assets/images/Ellipse 1210.svg
File diff suppressed because it is too large
View File

4
public/assets/images/Frame 2095586fdas679.svg

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4286 0H6.57143C5.70355 0 5 0.703552 5 1.57143V9.42857C5 10.2964 5.70355 11 6.57143 11H14.4286C15.2964 11 16 10.2964 16 9.42857V1.57143C16 0.703552 15.2964 0 14.4286 0Z" fill="white"/>
<path d="M5.10714 12.8571C4.58618 12.8571 4.08656 12.6502 3.71818 12.2818C3.34981 11.9134 3.14286 11.4138 3.14286 10.8929V5H1.57143C0.707143 5 0 5.70714 0 6.57143V14.4286C0 15.2929 0.707143 16 1.57143 16H9.42857C10.2929 16 11 15.2929 11 14.4286V12.8571H5.10714Z" fill="white"/>
</svg>

9
public/assets/images/Grfdasfoup.svg

@ -0,0 +1,9 @@
<svg width="19" height="21" viewBox="0 0 19 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 4.66045e-09C14.7652 -4.26217e-05 15.5015 0.292325 16.0583 0.817284C16.615 1.34224 16.9501 2.06011 16.995 2.824L17 3V12.25C17 12.6642 17.3358 13 17.75 13C18.397 13 18.93 13.492 18.994 14.122L19 14.25V16C19 16.7652 18.7077 17.5015 18.1827 18.0583C17.6578 18.615 16.9399 18.9501 16.176 18.995L16 19H6C5.23479 19 4.49849 18.7077 3.94174 18.1827C3.38499 17.6578 3.04989 16.9399 3.005 16.176L3 16V7.75C3 6.7835 2.2165 6 1.25 6C0.940542 6.00014 0.642032 5.88549 0.412234 5.67823C0.182437 5.47097 0.0376885 5.18583 0.00600005 4.878L4.66045e-09 4.75V3C-4.26217e-05 2.23479 0.292325 1.49849 0.817284 0.941739C1.34224 0.384993 2.06011 0.0498925 2.824 0.00500012L3 4.66045e-09H14ZM17 16C17 15.4477 16.5523 15 16 15H9C8.44771 15 8 15.4477 8 16C8 16.4978 8.35137 17 8.84919 17H16C16.2652 17 16.5196 16.8946 16.7071 16.7071C16.8946 16.5196 17 16.2652 17 16ZM10 9H8C7.74512 9.00028 7.49997 9.09788 7.31463 9.27285C7.1293 9.44782 7.01776 9.68695 7.00283 9.94139C6.98789 10.1958 7.07067 10.4464 7.23426 10.6418C7.39785 10.8373 7.6299 10.9629 7.883 10.993L8 11H10C10.2549 10.9997 10.5 10.9021 10.6854 10.7272C10.8707 10.5522 10.9822 10.313 10.9972 10.0586C11.0121 9.80416 10.9293 9.55362 10.7657 9.35817C10.6021 9.16271 10.3701 9.0371 10.117 9.007L10 9ZM12 5H8C7.73478 5 7.48043 5.10536 7.29289 5.29289C7.10536 5.48043 7 5.73478 7 6C7 6.26522 7.10536 6.51957 7.29289 6.70711C7.48043 6.89464 7.73478 7 8 7H12C12.2652 7 12.5196 6.89464 12.7071 6.70711C12.8946 6.51957 13 6.26522 13 6C13 5.73478 12.8946 5.48043 12.7071 5.29289C12.5196 5.10536 12.2652 5 12 5ZM3 2.78538C3 2.35163 2.5996 1.98618 2.29289 2.29289C2.10536 2.48043 2 2.73478 2 3V3.5C2 3.77614 2.22386 4 2.5 4C2.77614 4 3 3.77614 3 3.5V2.78538Z" fill="url(#paint0_linear_2456_20638)"/>
<defs>
<linearGradient id="paint0_linear_2456_20638" x1="19.1388" y1="19.0477" x2="4.51281" y2="1.02042" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6F82"/>
<stop offset="1" stop-color="#E03950"/>
</linearGradient>
</defs>
</svg>

21
public/assets/images/Group 15978fdsa80467.svg
File diff suppressed because it is too large
View File

5
public/assets/images/Groupfdas 2.svg

@ -0,0 +1,5 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.0390625" y="0.0546676" width="12" height="12" rx="6" fill="#0EB13C"/>
<rect x="8.625" y="4.12661" width="0.715424" height="4.78806" transform="rotate(45 8.625 4.12661)" fill="white" stroke="white" stroke-width="0.18"/>
<rect x="3.51335" y="6.68553" width="0.715424" height="2.52178" transform="rotate(-45 3.51335 6.68553)" fill="white" stroke="white" stroke-width="0.18"/>
</svg>

3
public/assets/images/Vecfadsftor.svg

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7883 -0.00011266L6.95083 5.83733L1.11339 -0.00011266L0.00150072 1.11178L5.83894 6.94922L0.00150072 12.7867L1.11339 13.8986L6.95083 8.06111L12.7883 13.8986L13.9002 12.7867L8.06273 6.94922L13.9002 1.11178L12.7883 -0.00011266Z" fill="#8B8B8B"/>
</svg>

3
public/assets/images/Vecfdastor.svg

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.72028 0H4.18516C4.28541 0 4.38327 0.030785 4.46569 0.0882475C4.54812 0.14571 4.61118 0.227117 4.64646 0.321608L5.60324 2.88256C5.63477 2.96725 5.64273 3.05901 5.62624 3.14792L5.14654 5.73666C5.73598 7.13162 6.70918 8.06931 8.32243 8.91038L10.8622 8.41408C10.9525 8.39656 11.0458 8.40481 11.1316 8.4379L13.6826 9.41728C13.7759 9.45306 13.8561 9.51649 13.9128 9.59921C13.9695 9.68192 13.9999 9.78001 14 9.8805V12.2522C14 13.3282 13.059 14.201 11.9577 13.9595C9.95144 13.5201 6.23408 12.4024 3.63055 9.78058C1.13609 7.26926 0.30023 3.8004 0.0196373 1.92568C-0.141359 0.854973 0.710934 0 1.72028 0Z" fill="white"/>
</svg>

14
public/assets/images/boxicons_education-filled.svg

@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.4528 8.61533C21.4495 8.61202 21.4457 8.60926 21.4415 8.60717L12.4478 4.11033C12.3095 4.04162 12.1572 4.00586 12.0028 4.00586C11.8484 4.00586 11.6961 4.04162 11.5578 4.11033L5.55781 7.11033L2.55781 8.61033L1.55781 9.11033C1.3929 9.19343 1.25422 9.32057 1.15714 9.47766C1.06006 9.63476 1.00837 9.81566 1.00781 10.0003V15.0003C1.00781 15.5526 1.45553 16.0003 2.00781 16.0003C2.5601 16.0003 3.00781 15.5526 3.00781 15.0003V13.946C3.00781 12.4185 4.61573 11.4252 5.9817 12.109L11.5578 14.9003C11.6978 14.9703 11.8478 15.0103 12.0078 15.0103C12.1678 15.0103 12.3178 14.9703 12.4578 14.9003L21.4578 10.4003C21.7978 10.2303 22.0078 9.88033 22.0078 9.51033C22.0078 9.14263 21.8004 8.79468 21.4641 8.62352C21.46 8.6214 21.4561 8.61864 21.4528 8.61533Z" fill="url(#paint0_linear_2456_20617)"/>
<path d="M12 16.9996C11.54 16.9996 11.07 16.8896 10.66 16.6796L6.42214 14.5607C5.76876 14.234 5 14.7091 5 15.4396C5 17.4996 8.12 22 12 22C15.88 22 19 17.5096 19 15.4396C19 14.7091 18.2312 14.234 17.5779 14.5607L13.34 16.6796C12.93 16.8896 12.46 16.9996 12 16.9996Z" fill="url(#paint1_linear_2456_20617)"/>
<defs>
<linearGradient id="paint0_linear_2456_20617" x1="22.1612" y1="16.0305" x2="14.9621" y2="0.495013" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6F82"/>
<stop offset="1" stop-color="#E03950"/>
</linearGradient>
<linearGradient id="paint1_linear_2456_20617" x1="19.1022" y1="22.0205" x2="14.1502" y2="11.5361" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6F82"/>
<stop offset="1" stop-color="#E03950"/>
</linearGradient>
</defs>
</svg>

8
public/assets/images/mingcute_user-info-fill.svg

@ -1,3 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 7C6 5.67392 6.52678 4.40215 7.46447 3.46447C8.40215 2.52678 9.67392 2 11 2C12.3261 2 13.5979 2.52678 14.5355 3.46447C15.4732 4.40215 16 5.67392 16 7C16 8.32608 15.4732 9.59785 14.5355 10.5355C13.5979 11.4732 12.3261 12 11 12C9.67392 12 8.40215 11.4732 7.46447 10.5355C6.52678 9.59785 6 8.32608 6 7ZM4.822 14.672C6.425 13.694 8.605 13 11 13C11.4473 13 11.886 13.0233 12.316 13.07C12.4878 13.0884 12.6518 13.151 12.7922 13.2517C12.9326 13.3524 13.0445 13.4878 13.117 13.6446C13.1895 13.8014 13.2202 13.9743 13.206 14.1465C13.1918 14.3186 13.1332 14.4842 13.036 14.627C12.3587 15.6213 11.9976 16.797 12 18C12 18.92 12.207 19.79 12.575 20.567C12.6467 20.7184 12.6792 20.8853 12.6696 21.0525C12.66 21.2198 12.6085 21.3819 12.5199 21.524C12.4313 21.6662 12.3085 21.7838 12.1626 21.8661C12.0167 21.9484 11.8525 21.9927 11.685 21.995L11 22C8.771 22 6.665 21.86 5.087 21.442C4.302 21.234 3.563 20.936 3.003 20.486C2.41 20.01 2 19.345 2 18.5C2 17.713 2.358 16.977 2.844 16.361C3.338 15.736 4.021 15.161 4.822 14.671V14.672ZM16 18C16 17.7348 16.1054 17.4804 16.2929 17.2929C16.4804 17.1054 16.7348 17 17 17H17.99C18.548 17 19 17.452 19 18.01V20.134C19.1906 20.2441 19.3396 20.414 19.4238 20.6173C19.5081 20.8207 19.5229 21.0462 19.4659 21.2588C19.4089 21.4714 19.2834 21.6593 19.1087 21.7933C18.9341 21.9273 18.7201 22 18.5 22H18.01C17.7421 22 17.4852 21.8936 17.2958 21.7042C17.1064 21.5148 17 21.2579 17 20.99V19C16.7348 19 16.4804 18.8946 16.2929 18.7071C16.1054 18.5196 16 18.2652 16 18ZM18 14C17.7451 14.0003 17.5 14.0979 17.3146 14.2728C17.1293 14.4478 17.0178 14.687 17.0028 14.9414C16.9879 15.1958 17.0707 15.4464 17.2343 15.6418C17.3979 15.8373 17.6299 15.9629 17.883 15.993L18.002 16C18.2569 15.9997 18.502 15.9021 18.6874 15.7272C18.8727 15.5522 18.9842 15.313 18.9992 15.0586C19.0141 14.8042 18.9313 14.5536 18.7677 14.3582C18.6041 14.1627 18.3721 14.0371 18.119 14.007L18 14Z" fill="white"/>
<path d="M6 7C6 5.67392 6.52678 4.40215 7.46447 3.46447C8.40215 2.52678 9.67392 2 11 2C12.3261 2 13.5979 2.52678 14.5355 3.46447C15.4732 4.40215 16 5.67392 16 7C16 8.32608 15.4732 9.59785 14.5355 10.5355C13.5979 11.4732 12.3261 12 11 12C9.67392 12 8.40215 11.4732 7.46447 10.5355C6.52678 9.59785 6 8.32608 6 7ZM4.822 14.6715C4.822 14.6717 4.82224 14.6719 4.82242 14.6717C6.42538 13.6939 8.60521 13 11 13C11.4473 13 11.886 13.0233 12.316 13.07C12.4878 13.0884 12.6518 13.151 12.7922 13.2517C12.9326 13.3524 13.0445 13.4878 13.117 13.6446C13.1895 13.8014 13.2202 13.9743 13.206 14.1465C13.1918 14.3186 13.1332 14.4842 13.036 14.627C12.3587 15.6213 11.9976 16.797 12 18C12 18.92 12.207 19.79 12.575 20.567C12.6467 20.7184 12.6792 20.8853 12.6696 21.0525C12.66 21.2198 12.6085 21.3819 12.5199 21.524C12.4313 21.6662 12.3085 21.7838 12.1626 21.8661C12.0167 21.9484 11.8525 21.9927 11.685 21.995L11 22C8.771 22 6.665 21.86 5.087 21.442C4.302 21.234 3.563 20.936 3.003 20.486C2.41 20.01 2 19.345 2 18.5C2 17.713 2.358 16.977 2.844 16.361C3.33791 15.7361 4.02076 15.1612 4.82157 14.6713C4.82176 14.6711 4.822 14.6713 4.822 14.6715ZM16 18C16 17.7348 16.1054 17.4804 16.2929 17.2929C16.4804 17.1054 16.7348 17 17 17H17.99C18.548 17 19 17.452 19 18.01V19.4795C19 19.8845 19.2689 20.2432 19.4238 20.6173C19.5081 20.8207 19.5229 21.0462 19.4659 21.2588C19.4089 21.4714 19.2834 21.6593 19.1087 21.7933C18.9341 21.9273 18.7201 22 18.5 22H18.01C17.7421 22 17.4852 21.8936 17.2958 21.7042C17.1064 21.5148 17 21.2579 17 20.99V19.7854C17 19.3516 16.5996 19.0138 16.2929 18.7071C16.1054 18.5196 16 18.2652 16 18ZM18 14C17.7451 14.0003 17.5 14.0979 17.3146 14.2728C17.1293 14.4478 17.0178 14.687 17.0028 14.9414C16.9879 15.1958 17.0707 15.4464 17.2343 15.6418C17.3979 15.8373 17.6299 15.9629 17.883 15.993L18.002 16C18.2569 15.9997 18.502 15.9021 18.6874 15.7272C18.8727 15.5522 18.9842 15.313 18.9992 15.0586C19.0141 14.8042 18.9313 14.5536 18.7677 14.3582C18.6041 14.1627 18.3721 14.0371 18.119 14.007L18 14Z" fill="url(#paint0_linear_2456_20588)"/>
<defs>
<linearGradient id="paint0_linear_2456_20588" x1="19.6278" y1="22.0503" x2="3.93906" y2="5.13022" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6F82"/>
<stop offset="1" stop-color="#E03950"/>
</linearGradient>
</defs>
</svg>

14
public/assets/images/noun-test-4525471 1.svg

@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2456_20658)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.9922 3.75C19.9922 3.021 19.7022 2.321 19.1872 1.805C18.6712 1.29 17.9712 1 17.2422 1C14.3712 1 9.11319 1 6.24219 1C5.51319 1 4.81319 1.29 4.29719 1.805C3.78219 2.321 3.49219 3.021 3.49219 3.75C3.49219 7.582 3.49219 15.918 3.49219 19.75C3.49219 20.479 3.78219 21.179 4.29719 21.695C4.81319 22.21 5.51319 22.5 6.24219 22.5C9.11319 22.5 14.3712 22.5 17.2422 22.5C17.9712 22.5 18.6712 22.21 19.1872 21.695C19.7022 21.179 19.9922 20.479 19.9922 19.75V3.75ZM6.21219 18.28L7.21219 19.28C7.50519 19.573 7.97919 19.573 8.27219 19.28L10.2722 17.28C10.5652 16.988 10.5652 16.512 10.2722 16.22C9.98019 15.927 9.50419 15.927 9.21219 16.22L8.21185 17.2197C7.95235 17.479 7.53187 17.4791 7.27219 17.22C6.98019 16.927 6.50419 16.927 6.21219 17.22C5.91919 17.512 5.91919 17.988 6.21219 18.28ZM12.7422 18.5H16.2422C16.6562 18.5 16.9922 18.164 16.9922 17.75C16.9922 17.336 16.6562 17 16.2422 17H12.7422C12.3282 17 11.9922 17.336 11.9922 17.75C11.9922 18.164 12.3282 18.5 12.7422 18.5ZM9.77219 12.22C9.513 11.9603 9.513 11.5397 9.77219 11.28C10.0652 10.988 10.0652 10.512 9.77219 10.22C9.48019 9.927 9.00419 9.927 8.71219 10.22C8.45245 10.4792 8.03192 10.4792 7.77219 10.22C7.48019 9.927 7.00419 9.927 6.71219 10.22C6.41919 10.512 6.41919 10.988 6.71219 11.28C6.97137 11.5397 6.97137 11.9603 6.71219 12.22C6.41919 12.512 6.41919 12.988 6.71219 13.28C7.00419 13.573 7.48019 13.573 7.77219 13.28C8.03192 13.0208 8.45245 13.0208 8.71219 13.28C9.00419 13.573 9.48019 13.573 9.77219 13.28C10.0652 12.988 10.0652 12.512 9.77219 12.22ZM12.7422 12.5H16.2422C16.6562 12.5 16.9922 12.164 16.9922 11.75C16.9922 11.336 16.6562 11 16.2422 11H12.7422C12.3282 11 11.9922 11.336 11.9922 11.75C11.9922 12.164 12.3282 12.5 12.7422 12.5ZM6.21219 6.28L7.21219 7.28C7.50519 7.573 7.97919 7.573 8.27219 7.28L10.2722 5.28C10.5652 4.988 10.5652 4.512 10.2722 4.22C9.98019 3.927 9.50419 3.927 9.21219 4.22L8.21185 5.21966C7.95235 5.47898 7.53187 5.47913 7.27219 5.22C6.98019 4.927 6.50419 4.927 6.21219 5.22C5.91919 5.512 5.91919 5.988 6.21219 6.28ZM12.7422 6.5H16.2422C16.6562 6.5 16.9922 6.164 16.9922 5.75C16.9922 5.336 16.6562 5 16.2422 5H12.7422C12.3282 5 11.9922 5.336 11.9922 5.75C11.9922 6.164 12.3282 6.5 12.7422 6.5Z" fill="url(#paint0_linear_2456_20658)"/>
</g>
<defs>
<linearGradient id="paint0_linear_2456_20658" x1="20.1127" y1="22.554" x2="3.22528" y2="6.57994" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6F82"/>
<stop offset="1" stop-color="#E03950"/>
</linearGradient>
<clipPath id="clip0_2456_20658">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

9
public/assets/images/solar_user-id-bold.svg

@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 4H14C17.771 4 19.657 4 20.828 5.172C21.999 6.344 22 8.229 22 12C22 15.771 22 17.657 20.828 18.828C19.656 19.999 17.771 20 14 20H10C6.229 20 4.343 20 3.172 18.828C2.001 17.656 2 15.771 2 12C2 8.229 2 6.343 3.172 5.172C4.344 4.001 6.229 4 10 4ZM13.25 9C13.25 8.80109 13.329 8.61032 13.4697 8.46967C13.6103 8.32902 13.8011 8.25 14 8.25H19C19.1989 8.25 19.3897 8.32902 19.5303 8.46967C19.671 8.61032 19.75 8.80109 19.75 9C19.75 9.19891 19.671 9.38968 19.5303 9.53033C19.3897 9.67098 19.1989 9.75 19 9.75H14C13.8011 9.75 13.6103 9.67098 13.4697 9.53033C13.329 9.38968 13.25 9.19891 13.25 9ZM14.25 12C14.25 11.8011 14.329 11.6103 14.4697 11.4697C14.6103 11.329 14.8011 11.25 15 11.25H19C19.1989 11.25 19.3897 11.329 19.5303 11.4697C19.671 11.6103 19.75 11.8011 19.75 12C19.75 12.1989 19.671 12.3897 19.5303 12.5303C19.3897 12.671 19.1989 12.75 19 12.75H15C14.8011 12.75 14.6103 12.671 14.4697 12.5303C14.329 12.3897 14.25 12.1989 14.25 12ZM15.25 15C15.25 14.8011 15.329 14.6103 15.4697 14.4697C15.6103 14.329 15.8011 14.25 16 14.25H19C19.1989 14.25 19.3897 14.329 19.5303 14.4697C19.671 14.6103 19.75 14.8011 19.75 15C19.75 15.1989 19.671 15.3897 19.5303 15.5303C19.3897 15.671 19.1989 15.75 19 15.75H16C15.8011 15.75 15.6103 15.671 15.4697 15.5303C15.329 15.3897 15.25 15.1989 15.25 15ZM11 9C11 9.53043 10.7893 10.0391 10.4142 10.4142C10.0391 10.7893 9.53043 11 9 11C8.46957 11 7.96086 10.7893 7.58579 10.4142C7.21071 10.0391 7 9.53043 7 9C7 8.46957 7.21071 7.96086 7.58579 7.58579C7.96086 7.21071 8.46957 7 9 7C9.53043 7 10.0391 7.21071 10.4142 7.58579C10.7893 7.96086 11 8.46957 11 9ZM9 17C13 17 13 16.105 13 15C13 13.895 11.21 13 9 13C6.79 13 5 13.895 5 15C5 16.105 5 17 9 17Z" fill="url(#paint0_linear_2456_20691)"/>
<defs>
<linearGradient id="paint0_linear_2456_20691" x1="22.1461" y1="20.0402" x2="10.65" y2="2.32821" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6F82"/>
<stop offset="1" stop-color="#E03950"/>
</linearGradient>
</defs>
</svg>

1
src/app/[lang]/slider/page.tsx

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

1
src/app/[lang]/terms/page.tsx

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

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

@ -3,9 +3,10 @@
import Image from "next/image";
import Link from "next/link";
import { FaPen } from "react-icons/fa6";
import Button from "@/components/ui/button";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
@ -17,7 +18,12 @@ const advisorAvatars = [
export default function FindingMatchPage() {
const { dictionary: t, locale } = useI18n();
const { data: profile } = useMarriageProfileQuery();
const copy = t.findingMatch;
const matchImageSrc =
profile?.gender === "female"
? "/assets/images/Group 15978fdsa80467.svg"
: "/assets/images/Group 159788fd0467.svg";
return (
<>
@ -33,7 +39,7 @@ export default function FindingMatchPage() {
<section className="flex flex-1 flex-col items-center mt-32">
<div className="relative h-[124px] w-[130px]" aria-hidden="true">
<Image
src="/assets/images/Group 159788fd0467.svg"
src={matchImageSrc}
alt=""
fill
sizes="58px"
@ -51,55 +57,24 @@ export default function FindingMatchPage() {
</p>
</section>
<section className="space-y-3">
<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]">
{copy.advisorTitle}
</h2>
<p className="mt-2 max-w-[280px] text-[11px] leading-[1.45] font-semibold text-[#8A8A8A]">
{copy.advisorDescription}
</p>
<AdvisorActionsCard
title={copy.advisorTitle}
description={copy.advisorDescription}
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={copy.getAdvisor}
getAdvisorHref="/questions-list"
/>
<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"
>
{copy.getAdvisor}
</Button>
</div>
</div>
<Link
className="inline-flex 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)}
>
<span className="flex min-w-0 items-center justify-center gap-2 text-center text-[16px] leading-none font-semibold">
<FaPen aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span>{copy.editProfile}</span>
</span>
</Link>
</section>
<Link
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)}
>
<span className="flex min-w-0 items-center justify-center gap-2 text-center text-[16px] leading-none font-semibold">
<FaPen aria-hidden="true" className="h-3.5 w-3.5 shrink-0" />
<span>{copy.editProfile}</span>
</span>
</Link>
</main>
</>
);

29
src/app/intro/page.tsx

@ -10,20 +10,25 @@ import { useI18n } from "@/i18n/provider";
export default function Intro() {
const { dictionary: t, locale } = useI18n();
const { data: profile } = useMarriageProfileQuery();
const submitPath =
profile?.active_case?.status === "female_accepted"
? "/request-accepted"
: profile?.active_case?.status === "male_accepted"
const isInCase = profile?.status === "in_case";
const isFemaleAcceptedFlow =
isInCase &&
(profile?.active_case?.status === "payment_pending" ||
profile?.active_case?.status === "female_accepted" ||
profile?.active_case?.status === "payment_done");
const submitPath = isFemaleAcceptedFlow
? "/request-accepted"
: isInCase && profile?.active_case?.status === "male_accepted"
? "/request-sent"
: profile?.status === "pending_onboarding"
? "/rules"
: profile?.status === "pending_info"
? "/questions-list"
: profile?.status === "waiting"
? "/finding-match"
: profile?.status === "in_case" || profile?.status === "matched"
? "/new-match"
: "/slider";
? "/rules"
: profile?.status === "pending_info"
? "/questions-list"
: profile?.status === "waiting"
? "/finding-match"
: profile?.status === "in_case" || profile?.status === "matched"
? "/new-match"
: "/terms";
const submitHref = localizePath(submitPath, locale);
return (

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

@ -2,7 +2,10 @@
import Image from "next/image";
import { useMemo } from "react";
import { FaBell, FaLock } from "react-icons/fa6";
import { FaLock } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
import type {
MarriageField,
@ -172,135 +175,112 @@ export default function NewMatchPage() {
<>
<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.
<main className="-mx-[17px] min-h-screen h-full pb-5 text-center px-4 mt-10">
<header className="flex items-center justify-between">
<NavigationButton icon="back" />
<h1 className="font-faminela">{t.common.appName}</h1>
<NavigationButton icon="support" iconLabel={t.common.support} />
</header>
<div className="">
<section className="flex flex-col items-center mt-9">
<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)]"
>
<Image
src={"/assets/images/Ellipse 1210.svg"}
width={70}
height={70}
alt="notification"
/>
</div>
<h2 className="text-[22px] font-bold mt-3.5">YOU HAVE A NEW MATCH!</h2>
<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>
) : 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>
</section>
<div className="flex h-full flex-col justify-between">
<section className="mt-[36px] rounded-[15px] 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} />
))}
</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>
<a
href="/new-match/profile"
className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white py-[12px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none"
>
View Profile
</a>
</>
) : (
<p className="py-8 text-[13px] font-semibold">
No match summary is available yet.
</p>
)}
</section>
<div className="w-full space-y-3">
<AdvisorActionsCard
className="mt-[36px]"
title={t.findingMatch.advisorTitle}
description={t.findingMatch.advisorDescription}
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={t.findingMatch.getAdvisor}
getAdvisorHref="/questions-list"
/>
<div className="flex cursor-not-allowed w-full items-center justify-center rounded-[11px] border-none bg-[#DBDBDB] px-6 py-3.5 text-[#747474] no-underline shadow-none">
<span className="flex items-center gap-1">
<FaLock className="size-6 shrink-0 text-[#747474]" />
<span className="leading-none font-semibold tracking-[-0.03em]">
Profile locked
</span>
</span>
</div>
<a
href="/new-match/profile"
className="mt-[15px] inline-flex w-full items-center justify-center rounded-[7px] border-none bg-white py-[12px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none"
>
View Profile
</a>
</>
) : (
<p className="py-8 text-[13px] font-semibold">
No match summary is available yet.
</p>
)}
</section>
<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>
<a
href="/questions-list"
className="inline-flex w-auto items-center justify-center rounded-[9px] border-none bg-[#EBEDF0] px-5 py-[13px] text-[16px] font-semibold text-[#111111] no-underline shadow-none"
>
{t.findingMatch.getAdvisor}
</a>
</div>
</section>
<div className="mt-auto px-[12px] pt-6">
<div
className="flex cursor-not-allowed py-3.5 w-full items-center justify-center rounded-[11px] border-none bg-[#DBDBDB] px-6 text-[#747474] no-underline shadow-none"
>
<span className="flex items-center gap-1">
<FaLock className="size-6 shrink-0 text-[#747474]" />
<span className="leading-none font-semibold tracking-[-0.03em]">
Profile locked
</span>
</span>
</div>
</div>
</main>

167
src/app/new-match/profile/page.tsx

@ -5,13 +5,16 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import Button from "@/components/ui/button";
import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import InformationSheet from "@/components/ui/information-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
import {
type MarriageField,
type MarriageFieldValue,
import type {
MarriageCaseStatus,
MarriageField,
MarriageFieldValue,
MarriageGender,
} from "@/hooks/marriage/types";
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
@ -45,6 +48,21 @@ function isImageField(field: MarriageField) {
);
}
function canAcceptProfile(
gender: MarriageGender | null | undefined,
status: MarriageCaseStatus | null | undefined,
) {
if (!gender || !status) {
return false;
}
if (gender === "female") {
return status === "introduced" || status === "male_accepted";
}
return status === "introduced";
}
function MatchField({ field }: { field: MarriageField }) {
const value = formatFieldValue(field.value);
@ -66,12 +84,15 @@ export default function NewMatchProfilePage() {
const { dictionary: t, locale } = useI18n();
const router = useRouter();
const [isRequestSheetOpen, setIsRequestSheetOpen] = useState(false);
const [isFemaleConsentChecked, setIsFemaleConsentChecked] = useState(false);
const [isRejectSheetOpen, setIsRejectSheetOpen] = useState(false);
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] =
useState(false);
const { data: profile } = useMarriageProfileQuery();
const caseId = profile?.active_case?.case_id;
const isMaleAccepted = profile?.active_case?.status === "male_accepted";
const caseStatus = profile?.active_case?.status;
const isFemaleProfile = profile?.gender === "female";
const isMaleAccepted = caseStatus === "male_accepted";
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", {
onSuccess: async (_, variables) => {
if (variables.action === "accept") {
@ -84,42 +105,114 @@ export default function NewMatchProfilePage() {
});
const isSubmitting = respondMutation.isPending;
const isAcceptProfileEnabled =
Boolean(caseId) &&
!isSubmitting &&
canAcceptProfile(profile?.gender, caseStatus);
return (
<>
<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]"
disabled={!caseId || isSubmitting || isMaleAccepted}
onClick={async () => {
close();
if (!caseId) {
return;
}
isFemaleProfile ? (
<FemaleConsentSheet
title="Final Confirmation & Consent"
description="By approving this profile, the male candidate will be notified to proceed with acquiring your contact information for further communication. Please ensure full family alignment before proceeding"
buttons={({ close }) => (
<div className="space-y-5">
<button
type="button"
onClick={() => setIsFemaleConsentChecked((value) => !value)}
className="flex w-full items-start gap-4 rounded-[15px] border-2 border-white bg-white/50 px-5 py-5 text-left shadow-[0_8px_24px_rgba(0,0,0,0.05)]"
>
<span
className={[
"mt-1 flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full border-1",
isFemaleConsentChecked
? "border-[#F0445B] bg-[#F0445B]"
: "border-[#2B2B2B] bg-white",
].join(" ")}
aria-hidden="true"
>
{isFemaleConsentChecked ? (
<span className="h-3 w-3 rounded-full bg-[#F0445B]" />
) : null}
</span>
<span className="text-xs leading-[1.35] text-[#3F3F3F]">
I confirm that the female candidate and her family have
reviewed this profile and tentatively agree to further
communication
</span>
</button>
if (isMaleAccepted) {
return;
}
<div className="grid w-full grid-cols-2 gap-3">
<Button
variant="outlined"
className="py-[18px] text-[18px]"
onClick={close}
>
{t.common.cancel}
</Button>
<Button
className="py-[18px] text-[18px]"
disabled={
!caseId ||
isSubmitting ||
!isAcceptProfileEnabled ||
!isFemaleConsentChecked
}
onClick={async () => {
close();
if (!isAcceptProfileEnabled) {
return;
}
await respondMutation.mutateAsync({ action: "accept" });
}}
>
{t.common.confirm}
</Button>
</div>
)}
onClose={() => setIsRequestSheetOpen(false)}
/>
await respondMutation.mutateAsync({ action: "accept" });
}}
>
{t.common.confirm}
</Button>
</div>
</div>
)}
onClose={() => {
setIsFemaleConsentChecked(false);
setIsRequestSheetOpen(false);
}}
/>
) : (
<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]"
disabled={!isAcceptProfileEnabled}
onClick={async () => {
close();
if (!isAcceptProfileEnabled) {
return;
}
await respondMutation.mutateAsync({ action: "accept" });
}}
>
{t.common.confirm}
</Button>
</div>
)}
onClose={() => setIsRequestSheetOpen(false)}
/>
)
) : null}
{isRejectSheetOpen ? (
<InformationSheet
@ -220,12 +313,16 @@ export default function NewMatchProfilePage() {
</button>
<button
type="button"
disabled={!caseId || isSubmitting || isMaleAccepted}
disabled={!isAcceptProfileEnabled}
onClick={() => {
if (isMaleAccepted) {
if (!isAcceptProfileEnabled) {
return;
}
if (isFemaleProfile) {
setIsFemaleConsentChecked(false);
}
setIsRequestSheetOpen(true);
}}
className="inline-flex w-2/3 whitespace-nowrap items-center justify-center gap-1 rounded-[12px] bg-[#F0445B] px-4 py-[13px] text-[16px] font-semibold text-white shadow-[0_8px_16px_rgba(240,68,91,0.24)] disabled:cursor-not-allowed disabled:opacity-50"

5
src/app/questions-list/[slug]/page.tsx

@ -7,6 +7,7 @@ import QuestionDropdown from "@/components/questions/question-dropdown";
import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button";
import QuestionFile from "@/components/questions/question-file";
import QuestionNumber from "@/components/questions/question-number";
import QuestionPhone from "@/components/questions/question-phone";
import QuestionPhoto from "@/components/questions/question-photo";
import QuestionRadio from "@/components/questions/question-radio";
import QuestionSectionFlow from "@/components/questions/question-section-flow";
@ -59,6 +60,10 @@ function renderQuestion(question: QuestionField, questionIndex: number) {
return (
<QuestionNumber question={question} questionIndex={questionIndex} />
);
case "phone":
return (
<QuestionPhone question={question} questionIndex={questionIndex} />
);
case "photo":
return (
<QuestionPhoto question={question} questionIndex={questionIndex} />

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

@ -3,13 +3,17 @@
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { IoClose } from "react-icons/io5";
import QuestionCard from "@/components/questions/question-card";
import RequiredStepsCard from "@/components/questions/required-steps-card";
import Button from "@/components/ui/button";
import InformationSheet from "@/components/ui/information-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
import { getQuestionListItems } from "@/data/question-data";
import {
getQuestionListItems,
type QuestionListItem,
} from "@/data/question-data";
import { useStartMarriageMatchMutation } from "@/hooks/marriage/use-match-start";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections";
@ -28,6 +32,9 @@ export default function QuestionsListPage() {
},
});
const [isOptionalInfoSheetOpen, setIsOptionalInfoSheetOpen] = useState(false);
const [selectedSection, setSelectedSection] = useState<QuestionListItem | null>(
null,
);
const questionListItems = getQuestionListItems(locale);
const allRequiredSectionsCompleted = useMemo(() => {
if (!sections?.length) {
@ -106,6 +113,33 @@ export default function QuestionsListPage() {
)}
/>
) : null}
{selectedSection ? (
<InformationSheet
icon={null}
title={({ close }) => (
<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]">
{selectedSection.title}
</span>
<button
type="button"
aria-label={`Close ${selectedSection.title} explanation`}
className="-mt-0.5 flex size-6 shrink-0 items-center justify-center text-[#8F8F8F]"
onClick={close}
>
<IoClose aria-hidden="true" className="text-[22px]" />
</button>
</span>
)}
description={
<p className="px-0.5 text-sm leading-[1.45] text-[#2D2D2D]">
{selectedSection.summary}
</p>
}
onClose={() => setSelectedSection(null)}
className="text-left"
/>
) : null}
<SectionsRequest />
<PageBackground disabled />
@ -139,6 +173,7 @@ export default function QuestionsListPage() {
key={item.slug}
item={item}
progress={sectionProgressBySlug.get(item.slug) ?? null}
onInfoClick={(section) => setSelectedSection(section)}
/>
))}
</section>

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

@ -4,9 +4,14 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { FiCopy, FiPhone } from "react-icons/fi";
import CallResultSheet from "@/components/ui/call-result-sheet";
import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import SubscriptionRequiredSheet from "@/components/ui/subscription-required-sheet";
import { PageBackground } from "@/components/utils/page-background";
import type { MarriageField } from "@/hooks/marriage/types";
import { useMarriageContactInfoQuery } from "@/hooks/marriage/use-contact-info";
import {
extractHabcoinPaymentUrl,
useHabcoinPaymentMutation,
@ -15,14 +20,160 @@ import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
type ContactInfoPhoneItem = {
key: string;
label: string;
phoneNumber: string;
};
function sanitizePhoneNumber(value: MarriageField["value"]) {
if (value === null || value === "") {
return null;
}
const trimmedValue = String(value).trim();
if (!trimmedValue) {
return null;
}
const digits = trimmedValue.replace(/\D/g, "");
if (!digits) {
return null;
}
return trimmedValue.startsWith("+") ? `+${digits}` : digits;
}
function titleFromKey(key: string) {
return key
.replace(/^q\d+[_-]?/i, "")
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function formatContactLabel(field: MarriageField) {
const resolvedLabel = (field.label || titleFromKey(field.key)).trim();
return resolvedLabel.replace(/\s+Number$/i, "").trim();
}
function getContactInfoPhoneItems(fields: MarriageField[] | null | undefined) {
return (fields ?? []).flatMap((field) => {
if (field.type.toLowerCase() !== "phone") {
return [];
}
const phoneNumber = sanitizePhoneNumber(field.value);
if (!phoneNumber) {
return [];
}
return [
{
key: field.key,
label: formatContactLabel(field),
phoneNumber,
},
];
});
}
function ContactInfoPhoneCard({ item }: { item: ContactInfoPhoneItem }) {
return (
<div className="flex items-center gap-3 rounded-[15px] bg-[#F0445B]/10 p-3.5">
<div className="min-w-0 flex-1 text-left">
<p
dir="ltr"
className="text-lg leading-none font-semibold tracking-[0.01em] text-[#4D4D4D] tabular-nums"
>
{item.phoneNumber}
</p>
<p className="mt-2 text-[10px] leading-none font-semibold text-[#F0445B]">
{item.label}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<button
type="button"
aria-label={`Copy ${item.label}`}
onClick={() => {
void navigator.clipboard
.writeText(item.phoneNumber)
.catch(() => {});
}}
className="inline-flex p-3 items-center justify-center rounded-[10px] bg-[#F0445B] text-white"
>
<Image src={"/assets/images/Frame 2095586fdas679.svg"} width={16} height={16} alt="copy"/>
</button>
<a
href={`tel:${item.phoneNumber}`}
aria-label={`Call ${item.label}`}
className="inline-flex p-3 items-center justify-center rounded-[10px] bg-[#F0445B] text-white"
>
<Image src={"/assets/images/Vecfdastor.svg"} width={16} height={16} alt="copy"/>
</a>
</div>
</div>
);
}
export default function RequestAcceptedPage() {
const { locale } = useI18n();
const router = useRouter();
const [isCallResultSheetOpen, setIsCallResultSheetOpen] = useState(false);
const [isContactInfoSheetOpen, setIsContactInfoSheetOpen] = useState(false);
const [isSubscriptionSheetOpen, setIsSubscriptionSheetOpen] = useState(false);
const profileHref = localizePath("/new-match/profile", locale);
const { data: profile } = useMarriageProfileQuery();
const isFemaleProfile = profile?.gender === "female";
const caseId = profile?.active_case?.case_id;
const caseStatus = profile?.active_case?.status;
const recommendedPlanId = profile?.recommended_plan?.id;
const paymentMutation = useHabcoinPaymentMutation();
const contactInfoQuery = useMarriageContactInfoQuery(caseId, {
enabled: false,
});
const titleText = isFemaleProfile
? "The selected candidate will contact your family shortly."
: "REQUEST ACCEPTED";
const primaryActionText = isFemaleProfile
? "No contact yet?"
: "Match Profile";
const secondaryActionText = isFemaleProfile ? "Contacted" : "View Contact";
const contactInfoPhoneItems = getContactInfoPhoneItems(
contactInfoQuery.data?.contact_info,
);
const handleSecondaryAction = async () => {
if (isFemaleProfile) {
setIsCallResultSheetOpen(true);
return;
}
if (caseStatus === "female_accepted" || caseStatus === "payment_pending") {
setIsCallResultSheetOpen(true);
return;
}
if (caseStatus === "payment_done") {
if (!caseId) {
return;
}
if (!contactInfoQuery.data) {
await contactInfoQuery.refetch();
}
setIsContactInfoSheetOpen(true);
}
};
const handlePayment = async () => {
if (!recommendedPlanId || paymentMutation.isPending) {
@ -49,6 +200,31 @@ export default function RequestAcceptedPage() {
<>
<PageBackground />
{isCallResultSheetOpen ? (
<CallResultSheet onClose={() => setIsCallResultSheetOpen(false)} />
) : null}
{isContactInfoSheetOpen ? (
<FemaleConsentSheet
title="Contact Detail"
description="Please mention during the call that you were introduced by the Habib Marriage app."
buttons={
contactInfoPhoneItems.length ? (
<div className="space-y-4">
{contactInfoPhoneItems.map((item) => (
<ContactInfoPhoneCard key={item.key} item={item} />
))}
</div>
) : (
<div className="rounded-[12px] bg-[#ECECEC] px-4 py-3 text-sm font-semibold text-[#555]">
Contact information is not available yet.
</div>
)
}
onClose={() => setIsContactInfoSheetOpen(false)}
/>
) : null}
{isSubscriptionSheetOpen ? (
<SubscriptionRequiredSheet
onClose={() => setIsSubscriptionSheetOpen(false)}
@ -78,7 +254,7 @@ export default function RequestAcceptedPage() {
</div>
<h1 className="mt-11 text-[22px] leading-none font-black tracking-[0.02em] text-[#171717] uppercase">
REQUEST ACCEPTED
{titleText}
</h1>
<p className="mt-4 max-w-[315px] text-[16px] leading-[1.45] font-semibold text-[#777777]">
@ -89,25 +265,44 @@ export default function RequestAcceptedPage() {
<div className="flex mt-9 w-full justify-center gap-4">
<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]">
Match Profile
{primaryActionText}
</div>
</Link>
<button
type="button"
onClick={() => setIsSubscriptionSheetOpen(true)}
onClick={() => {
if (isFemaleProfile) {
setIsCallResultSheetOpen(true);
return;
}
if (
caseStatus === "female_accepted" ||
caseStatus === "payment_pending"
) {
setIsCallResultSheetOpen(true);
return;
}
if (caseStatus === "payment_done") {
void handleSecondaryAction();
return;
}
setIsSubscriptionSheetOpen(true);
}}
className="max-w-[212px] appearance-none border-0 bg-transparent p-0 text-left"
>
<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">
View Contact
{secondaryActionText}
</div>
</button>
</div>
<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">
Please note: if you do not make contact within 2 days, a penalty
may apply
If they dont contact you within 2 days, please inform us.
</p>
</div>
</section>

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

@ -1,17 +1,21 @@
"use client";
import Image from "next/image";
import { useRouter } from "next/navigation";
import Button from "@/components/ui/button";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
import Link from "next/link";
const advisorAvatars = [
{ id: "advisor-primary", src: "/assets/images/Avatar Image.png" },
{ id: "advisor-secondary", src: "/assets/images/Ellipse 370.png" },
{ id: "advisor-tertiary", src: "/assets/images/Avatar Image.png" },
];
export default function RequestSentPage() {
const { locale } = useI18n();
const router = useRouter();
const { dictionary: t } = useI18n();
const copy = t.findingMatch;
return (
<>
@ -53,6 +57,14 @@ export default function RequestSentPage() {
</section>
<div className="space-y-8">
<AdvisorActionsCard
title={copy.advisorTitle}
description={copy.advisorDescription}
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={copy.getAdvisor}
getAdvisorHref="/questions-list"
/>
<section className="flex flex-col items-center text-center">
<div className="flex items-center justify-center py-3.5 rounded-[11px] gap-1 w-full bg-[#DBDBDB]">

2
src/app/slider/page.tsx → src/app/terms/page.tsx

@ -1,5 +1,5 @@
import SliderPage from "@/components/sliders/slider-page";
export default function SliderRoute() {
export default function TermsRoute() {
return <SliderPage />;
}

2
src/components/dev/locator-paths.ts

@ -4,7 +4,7 @@ export const LOCATORS = {
appQuestionsListPage: "D:/sajjadi/marriage/src/app/questions-list/page.tsx",
appQuestionDetailPage:
"D:/sajjadi/marriage/src/app/questions-list/[slug]/page.tsx",
appSliderPage: "D:/sajjadi/marriage/src/app/slider/page.tsx",
appTermsPage: "D:/sajjadi/marriage/src/app/terms/page.tsx",
bookingTermsCard:
"D:/sajjadi/marriage/src/components/questions/booking-terms-card.tsx",
questionCard:

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

@ -11,15 +11,25 @@ import {
import type { QuestionCardIcon, QuestionListItem } from "@/data/question-data";
import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider";
import Image from "next/image";
type QuestionCardProps = {
item: QuestionListItem;
progress?: number | null;
onInfoClick?: (item: QuestionListItem) => void;
};
const RADIUS = 8;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
const iconAssetMap: Partial<Record<QuestionCardIcon, string>> = {
profile: "/assets/images/mingcute_user-info-fill.svg",
education: "/assets/images/boxicons_education-filled.svg",
details: "/assets/images/Grfdasfoup.svg",
checklist: "/assets/images/noun-test-4525471 1.svg",
contact: "/assets/images/solar_user-id-bold.svg",
};
const iconMap: Record<QuestionCardIcon, IconType> = {
profile: IoPerson,
education: IoSchool,
@ -31,6 +41,7 @@ const iconMap: Record<QuestionCardIcon, IconType> = {
export function QuestionCard({
item,
progress = item.progress,
onInfoClick,
}: QuestionCardProps) {
const { dictionary: t, locale } = useI18n();
const hasProgress = typeof progress === "number" && Number.isFinite(progress);
@ -39,6 +50,7 @@ export function QuestionCard({
: 0;
const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE;
const CardIcon = iconMap[item.icon];
const iconAsset = iconAssetMap[item.icon];
return (
<Link
@ -52,8 +64,20 @@ export function QuestionCard({
className="rounded-[20px] border border-white/80 bg-white px-3 py-3 shadow-[0_12px_28px_rgba(15,23,42,0.05)] transition-transform duration-200 hover:-translate-y-0.5"
>
<div className="flex items-center gap-2.5">
<div className="relative flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-[13px] bg-linear-to-br from-[#F04363] to-[#F96C8C] text-white shadow-[0_8px_18px_rgba(240,67,99,0.18)]">
<CardIcon aria-hidden="true" className="text-[19px]" />
<div className="rounded-[13px] bg-linear-to-br from-[#E03950]/15 to-[#E03950]/0 p-px shadow-[0_8px_18px_rgba(240,67,99,0.18)]">
<div className="relative flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-[12px] bg-linear-to-br from-[#E03950]/15 to-[#FE6F82]/15 text-white">
{iconAsset ? (
<Image
src={iconAsset}
alt=""
aria-hidden="true"
width={22}
height={22}
/>
) : (
<CardIcon aria-hidden="true" className="text-[19px]" />
)}
</div>
</div>
<div className="min-w-0 flex-1">
@ -74,41 +98,59 @@ export function QuestionCard({
<div className="flex h-[44px] w-[44px] shrink-0 flex-col items-end justify-between">
{item.showInfoBadge ? (
<span className="flex h-[17px] w-[17px] items-center justify-center rounded-[6px] bg-[#747474] text-white">
<button
type="button"
aria-label={`${item.title} details`}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onInfoClick?.(item);
}}
className="flex h-[17px] w-[17px] items-center justify-center rounded-[6px] bg-[#747474] text-white"
>
<IoInformation aria-hidden="true" className="text-[9px]" />
</span>
</button>
) : (
<span className="h-[14px] w-[14px]" aria-hidden="true" />
)}
<div className="flex items-center gap-1">
<svg
aria-hidden="true"
className="-rotate-90"
viewBox="0 0 22 22"
width="18"
height="18"
>
<circle
cx="11"
cy="11"
r={RADIUS}
fill="none"
stroke="#E9E9E9"
strokeWidth="2.25"
/>
<circle
cx="11"
cy="11"
r={RADIUS}
fill="none"
stroke="#202020"
strokeWidth="2.25"
strokeLinecap="round"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={dashOffset}
{progress === 100 ? (
<Image
src={"/assets/images/Groupfdas 2.svg"}
width={12}
height={12}
alt="Completed"
/>
</svg>
) : (
<svg
aria-hidden="true"
className="-rotate-90"
viewBox="0 0 22 22"
width="18"
height="18"
>
<circle
cx="11"
cy="11"
r={RADIUS}
fill="none"
stroke="#E9E9E9"
strokeWidth="2.25"
/>
<circle
cx="11"
cy="11"
r={RADIUS}
fill="none"
stroke="#202020"
strokeWidth="2.25"
strokeLinecap="round"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={dashOffset}
/>
</svg>
)}
<span className="text-[10px] leading-none font-semibold text-[#1F1F1F]">
{hasProgress ? `${normalizedProgress}%` : "..."}
</span>

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

@ -0,0 +1,160 @@
"use client";
import { useEffect, useRef, useState } from "react";
import type { QuestionField } from "@/data/question-data";
import { useQuestionAnswer } from "./question-answer-storage";
import QuestionTitle from "./question-title";
type QuestionPhoneProps = {
question: QuestionField;
questionIndex: number;
countryCode?: string;
};
type PhoneValueParts = {
codeValue: string;
phoneValue: string;
};
function readPhoneValue(value: unknown, fallbackCode: string): PhoneValueParts {
if (value === null) {
return {
codeValue: "",
phoneValue: "",
};
}
if (typeof value !== "string") {
return {
codeValue: fallbackCode,
phoneValue: "",
};
}
if (value.length === 0) {
return {
codeValue: "",
phoneValue: "",
};
}
const separatorIndex = value.indexOf(" ");
if (separatorIndex >= 0) {
return {
codeValue: value.slice(0, separatorIndex),
phoneValue: value.slice(separatorIndex + 1),
};
}
if (value.startsWith("+")) {
return {
codeValue: value,
phoneValue: "",
};
}
return {
codeValue: "",
phoneValue: value,
};
}
function writePhoneValue(codeValue: string, phoneValue: string) {
if (!codeValue && !phoneValue) {
return null;
}
if (codeValue && phoneValue) {
return `${codeValue} ${phoneValue}`;
}
if (codeValue) {
return codeValue.startsWith("+") ? codeValue : `${codeValue} `;
}
return phoneValue;
}
export function QuestionPhone({
question,
questionIndex,
countryCode = "+98",
}: QuestionPhoneProps) {
const { setValue, value } = useQuestionAnswer(question, questionIndex);
const defaultCodeValue = countryCode.trim() || "+98";
const initialValue = readPhoneValue(value, defaultCodeValue);
const [codeValue, setCodeValue] = useState(initialValue.codeValue);
const [phoneValue, setPhoneValue] = useState(initialValue.phoneValue);
const lastCommittedValueRef = useRef(value);
useEffect(() => {
if (value === lastCommittedValueRef.current) {
return;
}
const nextValue = readPhoneValue(value, defaultCodeValue);
setCodeValue(nextValue.codeValue);
setPhoneValue(nextValue.phoneValue);
lastCommittedValueRef.current = value;
}, [defaultCodeValue, value]);
const updateStoredValue = (nextCodeValue: string, nextPhoneValue: string) => {
const nextValue = writePhoneValue(nextCodeValue, nextPhoneValue);
lastCommittedValueRef.current = nextValue;
setValue(nextValue);
};
return (
<label data-question-type={question.type} className="block space-y-3">
<QuestionTitle question={question} />
<div
dir="ltr"
className="flex w-full items-center overflow-hidden rounded-[11px] border border-[#E7D8D5] bg-white text-[#292A2E] focus-within:border-[#F43F5E]"
>
<div className="flex shrink-0 items-center pr-2.5 pl-2 py-[17px]">
<input
type="tel"
inputMode="tel"
aria-label="Country code"
maxLength={4}
required={question.required}
value={codeValue}
onChange={(event) => {
const nextCodeValue = event.target.value;
setCodeValue(nextCodeValue);
updateStoredValue(nextCodeValue, phoneValue);
}}
className="border-0 w-14 bg-transparent text-center text-[14px] leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
/>
<span
aria-hidden="true"
className="mx-3 h-[17px] w-px bg-[#181818]/70"
/>
</div>
<span className="flex min-w-0 flex-1 items-center pr-6 sm:pr-8">
<input
type="tel"
inputMode="tel"
required={question.required}
placeholder={question.extras.placeHolder}
value={phoneValue}
onChange={(event) => {
const nextPhoneValue = event.target.value;
setPhoneValue(nextPhoneValue);
updateStoredValue(codeValue, nextPhoneValue);
}}
dir="ltr"
className="h-full w-full border-0 bg-transparent p-0 text-left text-[14px] leading-none tracking-[-0.03em] text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]"
/>
</span>
</div>
</label>
);
}
export default QuestionPhone;

4
src/components/sliders/slider-slide-five.tsx

@ -22,13 +22,13 @@ export function SliderSlideFive({ index }: SliderSlideProps) {
</div>
<div className="mt-6 space-y-5">
<p className="text-[#111111] text-[13px] font-semibold leading-5">
<p className="text-[#36363C] text-sm font-semibold leading-5">
Your privacy and safety are our top priorities. We are committed to
keeping your information secure and giving you full control
throughout the process.
</p>
<ul className="list-disc space-y-2 pl-5 text-[#111111] text-[13px] leading-5">
<ul className="list-disc space-y-2 pl-5 text-[#36363C] text-sm font-semibold leading-5">
{NOTICE_ITEMS.map((item) => (
<li key={item}>{item}</li>
))}

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

@ -0,0 +1,73 @@
"use client";
import Image from "next/image";
import Button from "@/components/ui/button";
export type AdvisorAvatar = {
id: string;
src: string;
};
type AdvisorActionsCardProps = {
title: string;
description: string;
avatars: AdvisorAvatar[];
extraCount: number;
getAdvisorLabel: string;
getAdvisorHref: string;
className?: string;
};
export function AdvisorActionsCard({
title,
description,
avatars,
extraCount,
getAdvisorLabel,
getAdvisorHref,
className,
}: AdvisorActionsCardProps) {
return (
<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">
<h2 className="text-[16px] leading-none font-bold text-[#1C1C1C]">
{title}
</h2>
<p className="mt-2 max-w-[280px] text-[11px] leading-[1.45] font-semibold text-[#8A8A8A]">
{description}
</p>
<div className="mt-4 flex items-center justify-between gap-3">
<div className="flex items-center pl-1">
{avatars.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]">
+{extraCount}
</span>
</div>
<Button
className="w-auto rounded-[9px] border-none bg-[#EBEDF0] bg-none px-5 py-[13px] text-[#111111]! shadow-none"
href={getAdvisorHref}
>
{getAdvisorLabel}
</Button>
</div>
</div>
</section>
);
}
export default AdvisorActionsCard;

24
src/components/ui/call-result-sheet.tsx

@ -2,7 +2,6 @@
import type { HTMLAttributes } from "react";
import { useEffect, useId, useState } from "react";
import Button from "@/components/ui/button";
import { useI18n } from "@/i18n/provider";
const EXIT_ANIMATION_MS = 220;
@ -192,16 +191,29 @@ export function CallResultSheet({
</div>
</div>
<div className="mt-8 w-full">
<Button
className="rounded-[14px] py-[18px] shadow-none"
<div className="mt-8 grid w-full grid-cols-[1fr_2fr] gap-3">
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left"
onClick={closeSheet}
>
<div className="inline-flex w-full items-center justify-center rounded-[14px] border border-[#9A9A9A] bg-[#F7F7F7] px-4 py-[18px] text-[16px] font-bold text-[#8B8B8B] shadow-[inset_0_1px_0_rgba(255,255,255,0.8)] transition-opacity active:opacity-90">
{t.common.cancel}
</div>
</button>
<button
type="button"
className="appearance-none border-0 bg-transparent p-0 text-left"
onClick={() => {
onSubmit?.(selectedReason);
closeSheet();
}}
>
{t.common.submit}
</Button>
<div className="inline-flex w-full items-center justify-center rounded-[14px] bg-[#F0445B] px-4 py-[18px] text-[16px] font-semibold text-white shadow-[0_10px_18px_rgba(240,68,91,0.28)] transition-opacity active:opacity-90">
{t.common.submit}
</div>
</button>
</div>
</div>
</section>

159
src/components/ui/female-consent-sheet.tsx

@ -0,0 +1,159 @@
"use client";
import type { HTMLAttributes, ReactNode } from "react";
import { useEffect, useState } from "react";
import { useI18n } from "@/i18n/provider";
import Image from "next/image";
const EXIT_ANIMATION_MS = 220;
export type FemaleConsentSheetProps = Omit<
HTMLAttributes<HTMLDivElement>,
"children" | "title"
> & {
title: ReactNode;
description?: ReactNode;
buttons?: ReactNode | ((controls: { close: () => void }) => ReactNode);
closeOnOutside?: boolean;
onClose?: () => void;
};
export function FemaleConsentSheet({
title,
description,
buttons,
closeOnOutside = true,
onClose,
className,
...props
}: FemaleConsentSheetProps) {
const { dictionary: t } = useI18n();
const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true);
const [isClosing, setIsClosing] = useState(false);
const closeSheet = () => {
if (isClosing) {
return;
}
setIsClosing(true);
};
const controls = { close: closeSheet };
const resolvedButtons =
typeof buttons === "function" ? buttons(controls) : buttons;
useEffect(() => {
const frameId = window.requestAnimationFrame(() => {
setIsEntering(false);
});
return () => {
window.cancelAnimationFrame(frameId);
};
}, []);
useEffect(() => {
if (!isVisible) {
return;
}
const previousBodyOverflow = document.body.style.overflow;
const previousHtmlOverflow = document.documentElement.style.overflow;
document.body.style.overflow = "hidden";
document.documentElement.style.overflow = "hidden";
return () => {
document.body.style.overflow = previousBodyOverflow;
document.documentElement.style.overflow = previousHtmlOverflow;
};
}, [isVisible]);
useEffect(() => {
if (!isClosing) {
return;
}
const timeoutId = window.setTimeout(() => {
setIsVisible(false);
onClose?.();
}, EXIT_ANIMATION_MS);
return () => {
window.clearTimeout(timeoutId);
};
}, [isClosing, onClose]);
if (!isVisible) {
return null;
}
return (
<div
className={[
"fixed inset-0 z-50 flex items-end justify-center transition-all duration-[220ms]",
isClosing || isEntering
? "bg-[#171717]/0 opacity-0"
: "bg-[#171717]/55 opacity-100",
]
.filter(Boolean)
.join(" ")}
role="dialog"
aria-modal="true"
aria-label={t.sheets.informationSheet}
tabIndex={-1}
onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) {
closeSheet();
}
}}
onKeyDown={(event) => {
if (
closeOnOutside &&
event.target === event.currentTarget &&
(event.key === "Escape" || event.key === "Enter" || event.key === " ")
) {
event.preventDefault();
closeSheet();
}
}}
>
<section
{...props}
className={[
"w-full max-w-[375px] rounded-t-[15px] bg-[#F9F8F8] py-5 px-4 text-left shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform",
isClosing || isEntering ? "translate-y-full" : "translate-y-0",
className,
]
.filter(Boolean)
.join(" ")}
>
<div className="flex items-center justify-between gap-4">
<h2 className="text-sm font-bold leading-[1.2] tracking-[-0.03em] text-[#8B8B8B]">
{title}
</h2>
<button
type="button"
onClick={closeSheet}
aria-label="Close"
className="items-center justify-center rounded-full text-[#A0A0A0]"
>
<Image src={"/assets/images/Vecfadsftor.svg"} alt="close" width={18} height={18}/>
</button>
</div>
{description ? (
<div className="mt-5 text-sm leading-[1.45] text-[#2B2B2B]">
{description}
</div>
) : null}
{resolvedButtons ? <div className="mt-5">{resolvedButtons}</div> : null}
</section>
</div>
);
}
export default FemaleConsentSheet;

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

@ -238,7 +238,7 @@ export function InformationSheet({
<section
{...props}
className={[
"w-full max-w-[375px] rounded-t-[34px] bg-[#F9F8F8] p-3.5 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform",
"w-full max-w-[375px] rounded-t-[15px] bg-[#F9F8F8] p-3.5 text-center shadow-[0_20px_60px_rgba(15,23,42,0.08)] transition-transform duration-[220ms] ease-out will-change-transform",
isClosing || isEntering ? "translate-y-full" : "translate-y-0",
className,
]

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

@ -201,6 +201,30 @@
"options": ["Phone", "WhatsApp", "Email"]
}
},
{
"title": "Home Phone Number",
"type": "phone",
"required": false,
"description": "Enter your home landline or household phone number.",
"tooltip": "Include the area code if applicable.",
"extras": {
"placeHolder": "e.g. 021-12345678",
"range": [0, 0],
"options": []
}
},
{
"title": "Father's Phone Number",
"type": "phone",
"required": false,
"description": "Enter the phone number of your father for contact purposes.",
"tooltip": "Use a number that can reliably receive calls.",
"extras": {
"placeHolder": "e.g. 0912-1234567",
"range": [0, 0],
"options": []
}
},
{
"title": "Household Size Preference",
"type": "number",

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

@ -201,6 +201,30 @@
"options": ["Phone", "WhatsApp", "Email"]
}
},
{
"title": "Home Phone Number",
"type": "phone",
"required": false,
"description": "Enter your home landline or household phone number.",
"tooltip": "Include the area code if applicable.",
"extras": {
"placeHolder": "e.g. 021-12345678",
"range": [0, 0],
"options": []
}
},
{
"title": "Father's Phone Number",
"type": "phone",
"required": false,
"description": "Enter the phone number of your father for contact purposes.",
"tooltip": "Use a number that can reliably receive calls.",
"extras": {
"placeHolder": "e.g. 0912-1234567",
"range": [0, 0],
"options": []
}
},
{
"title": "Household Size Preference",
"type": "number",

Loading…
Cancel
Save