Browse Source

feat: add question detail and list pages with dynamic routing

- Implemented `QuestionDetailPage` to display individual questions based on slug.
- Created `QuestionsListPage` to list all questions with navigation.
- Added various question components: `QuestionButton`, `QuestionDate`, `QuestionDropdown`, `QuestionFile`, `QuestionNumber`, `QuestionRadio`, `QuestionSlider`, and `QuestionText`.
- Introduced `BookingTermsCard` for displaying terms and conditions.
- Added SVG icon for play action.
- Integrated `InformationSheet` component for user guidance.
- Created mock data for questions and structured data handling in `question-data.ts`.
- Developed a utility to add data locators for easier debugging in development.
master
sina_sajjadi 2 months ago
parent
commit
2fe0ee1af5
  1. 4
      .babelrc
  2. 3
      public/assets/images/Frame 1116607110.svg
  3. 5
      public/assets/images/Group 2.svg
  4. 110
      public/assets/images/Inner Plugdsain Iframe.svg
  5. 3
      public/assets/images/Vector.svg
  6. 11
      public/assets/images/cuida_history-outline.svg
  7. 3
      public/assets/images/mingcute_user-info-fill.svg
  8. 9
      public/assets/images/stash_play-solid.svg
  9. 21
      src/app/globals.css
  10. 6
      src/app/intro/page.tsx
  11. 14
      src/app/layout.tsx
  12. 38
      src/app/page.tsx
  13. 92
      src/app/questions-list/[slug]/page.tsx
  14. 55
      src/app/questions-list/page.tsx
  15. 99
      src/components/dev/dev-click-to-component.tsx
  16. 29
      src/components/dev/locator-paths.ts
  17. 29
      src/components/questions/booking-terms-card.tsx
  18. 30
      src/components/questions/question-button.tsx
  19. 119
      src/components/questions/question-card.tsx
  20. 11
      src/components/questions/question-date.tsx
  21. 11
      src/components/questions/question-dropdown.tsx
  22. 11
      src/components/questions/question-file.tsx
  23. 11
      src/components/questions/question-number.tsx
  24. 11
      src/components/questions/question-radio.tsx
  25. 11
      src/components/questions/question-slider.tsx
  26. 11
      src/components/questions/question-text.tsx
  27. 24
      src/components/sliders/slider-slide-three.tsx
  28. 82
      src/components/ui/info-progress-card.tsx
  29. 206
      src/components/ui/information-sheet.tsx
  30. 4
      src/components/utils/page-background.tsx
  31. 266
      src/data/mock-questions.json
  32. 83
      src/data/question-data.ts
  33. 40
      src/plugins/add-data-locator.cjs

4
.babelrc

@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["./src/plugins/add-data-locator.cjs"]
}

3
public/assets/images/Frame 1116607110.svg

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 12.5L12.5 10L10 7.5L5 12.5" stroke="#111111" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

5
public/assets/images/Group 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>

110
public/assets/images/Inner Plugdsain Iframe.svg

@ -0,0 +1,110 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2285_14449)">
<path d="M40.2195 40.7027C39.7046 42.8247 33.7287 44.6503 26.8789 44.7907C20.0292 44.9311 14.9114 43.324 15.4263 41.2176C15.9412 39.0956 21.9172 37.27 28.7669 37.1296C35.601 36.9736 40.7344 38.5807 40.2195 40.7027Z" fill="#E0BFAD"/>
<path d="M43.7125 26.676C44.2274 22.978 43.6188 19.1241 41.7465 15.8631C41.138 14.8021 40.4202 13.8191 39.5777 12.9453C38.6259 11.9467 37.5493 11.0729 36.5195 10.1523C39.3592 12.6644 41.138 16.2063 41.6373 19.9511C42.1522 23.7114 41.4032 27.6746 39.6557 31.0448C37.9237 34.3995 35.3648 37.2548 32.1662 39.2676C28.9988 41.2648 25.3009 42.3726 21.5405 42.279C18.1235 42.1698 14.7688 41.0308 12.0851 38.8931C11.8823 38.7371 11.6794 38.5655 11.4922 38.3938C12.8497 39.5953 14.1603 40.8747 15.705 41.8265C17.1249 42.7003 18.7008 43.3244 20.3235 43.7145C23.9278 44.557 27.7506 44.1358 31.152 42.7315C34.5535 41.3272 37.5493 38.9868 39.7649 36.0534C41.8713 33.3073 43.2444 30.0618 43.7125 26.676Z" fill="url(#paint0_linear_2285_14449)"/>
<path opacity="0.5" d="M31.2027 42.7162C32.3885 42.2325 33.5276 41.624 34.6042 40.9063L33.3091 39.5332C33.1531 39.6424 32.9971 39.736 32.841 39.8453C29.6112 41.8893 25.8353 43.0127 21.9969 42.9035C19.282 42.8254 16.5983 42.0921 14.2578 40.7346C14.7415 41.1091 15.2408 41.4836 15.7713 41.7956C17.1912 42.6694 18.7671 43.2935 20.3898 43.6836C23.9785 44.5262 27.7856 44.1205 31.2027 42.7162Z" fill="url(#paint1_linear_2285_14449)" fill-opacity="0.5"/>
<path d="M27.7522 6.34516C37.348 7.35936 43.4644 16.1907 41.4204 26.0986C39.3608 35.9909 29.921 43.1995 20.3095 42.1853C10.7137 41.1711 4.59727 32.3398 6.65687 22.4319C8.70087 12.5396 18.1563 5.33096 27.7522 6.34516Z" fill="url(#paint2_linear_2285_14449)"/>
<path opacity="0.2" d="M27.6271 6.76639C18.3589 5.7834 9.24673 12.7424 7.24954 22.2914C7.09351 23.0248 6.98429 23.7581 6.92188 24.4758C10.807 27.2844 15.5035 29.1255 20.7774 29.6717C27.8143 30.405 34.8045 28.7355 40.9053 25.2872C42.5904 15.9566 36.7237 7.71817 27.6271 6.76639Z" fill="url(#paint3_linear_2285_14449)"/>
<path opacity="0.5" d="M21.7724 41.3423C13.2063 40.4374 6.98068 32.9791 8.89986 23.7265C10.819 14.4739 19.666 7.73339 28.6377 8.66957C36.174 9.46533 40.5896 15.8158 40.9173 22.8371C41.2918 14.4895 35.6591 7.49935 27.3114 6.62558C18.0276 5.64258 8.86865 12.6171 6.88707 22.1974C4.88988 31.7777 10.819 40.3437 20.1184 41.3267C20.961 41.4204 21.8036 41.436 22.6461 41.4048C22.3497 41.3891 22.0532 41.3735 21.7724 41.3423Z" fill="#94512A"/>
<path d="M26.3646 8.27908C26.9107 8.27908 27.4569 8.31028 27.9874 8.35709C31.9973 8.77837 35.4612 10.7444 37.7549 13.8806C38.9407 15.5189 39.7677 17.3756 40.1889 19.4196C40.6258 21.5573 40.6102 23.7729 40.1421 26.0353C39.2996 30.1077 37.0215 33.8681 33.7293 36.6142C30.4682 39.3291 26.5051 40.827 22.5575 40.827C22.0114 40.827 21.4653 40.7958 20.9348 40.749C16.9248 40.3277 13.4609 38.3617 11.1673 35.2255C9.98145 33.5716 9.17009 31.7148 8.74881 29.6708C8.31192 27.5332 8.32752 25.3176 8.79561 23.0552C9.63818 18.9828 11.9162 15.2224 15.2085 12.4763C18.4539 9.76137 22.4171 8.27908 26.3646 8.27908ZM26.3646 6.73438C17.5021 6.73438 9.17009 13.5685 7.26652 22.7431C5.23812 32.5262 11.2921 40.4994 20.7787 41.4979C21.3717 41.5604 21.9646 41.5916 22.5575 41.5916C31.42 41.5916 39.0343 35.0227 40.9379 25.8481C42.9663 16.065 37.63 7.82659 28.1434 6.82799C27.5505 6.74998 26.9576 6.73438 26.3646 6.73438Z" fill="#E99855"/>
<path d="M25.9578 7.81226C26.5039 7.81226 27.05 7.84346 27.5961 7.89027C31.6529 8.31155 35.148 10.2931 37.4728 13.4762C38.6743 15.1301 39.5012 17.0181 39.9381 19.0777C40.3906 21.2309 40.375 23.4777 39.8913 25.7714C39.0331 29.8906 36.7395 33.6821 33.4004 36.4594C30.1082 39.2056 26.0982 40.7191 22.1194 40.7191C21.5733 40.7191 21.0272 40.6879 20.4811 40.6411C16.4399 40.2198 12.9293 38.2382 10.6044 35.0552C9.40297 33.4012 8.57601 31.5133 8.13912 29.4537C7.68664 27.3005 7.70224 25.0536 8.18593 22.76C9.0441 18.6408 11.3534 14.8492 14.6768 12.0719C17.9534 9.32575 21.9634 7.81226 25.9578 7.81226ZM25.9578 6.25195C17.0017 6.25195 8.57601 13.1641 6.65683 22.4323C4.59723 32.3246 10.7136 41.1716 20.3251 42.1858C20.9336 42.2482 21.5265 42.2794 22.135 42.2794C31.0912 42.2794 39.5168 35.3672 41.436 26.099C43.4956 16.2067 37.3792 7.35977 27.7677 6.34557C27.1592 6.28316 26.5507 6.25195 25.9578 6.25195Z" fill="url(#paint4_linear_2285_14449)"/>
<path opacity="0.7" d="M27.7042 6.34557C27.0957 6.28316 26.5027 6.25195 25.8942 6.25195C16.9381 6.25195 8.51244 13.1641 6.59327 22.4323C6.42163 23.2749 6.31241 24.1018 6.25 24.9132C6.7493 25.2565 7.24859 25.5841 7.76349 25.9118C7.7791 24.8664 7.90392 23.821 8.12236 22.76C8.98053 18.6408 11.2742 14.8492 14.6132 12.0719C17.9055 9.32575 21.9155 7.81226 25.8942 7.81226C26.4403 7.81226 26.9864 7.84346 27.5325 7.89027C31.5893 8.31155 35.0844 10.2931 37.4093 13.4762C38.6107 15.1301 39.4377 17.0181 39.8745 19.0777C40.327 21.2309 40.3114 23.4777 39.8277 25.7714C39.7965 25.9586 39.7497 26.1458 39.7029 26.3331C40.3114 26.021 40.9044 25.6933 41.4973 25.3657C43.12 15.7698 37.066 7.32856 27.7042 6.34557Z" fill="url(#paint5_linear_2285_14449)"/>
<path d="M27.3137 17.9377L24.9576 13.9121L22.5859 17.9065L24.9108 23.6484L27.3137 17.9377Z" fill="#94512A"/>
<path d="M31.7654 21.6206L32.998 17.127L28.4731 18.2348L26.0234 23.9143L31.7654 21.6206Z" fill="#94512A"/>
<path d="M26.4922 25.0064L32.2185 27.3469L36.2285 24.9752L32.2185 22.6348L26.4922 25.0064Z" fill="#94512A"/>
<path d="M23.8029 23.9297L21.4624 18.2034L16.9688 17.002L18.1078 21.5268L23.8029 23.9297Z" fill="#94512A"/>
<path d="M23.8886 26.1133L18.1311 28.4069L16.9141 32.9006L21.4233 31.7928L22.1411 30.1545L23.8886 26.1133Z" fill="#94512A"/>
<path d="M23.3691 24.9908L17.6428 22.6504L13.6484 25.0221L17.6584 27.3781L23.3691 24.9908Z" fill="#94512A"/>
<path d="M31.7497 28.6406L31.656 28.2818L25.9609 25.8789L28.3014 31.6052L32.7951 32.7911L31.7497 28.6406Z" fill="#94512A"/>
<path d="M26.4477 30.0612L24.9186 26.3789L22.5625 32.1052L24.9342 36.1152L27.2902 32.1052L26.4477 30.0612Z" fill="#94512A"/>
<path d="M25.5078 25.5371L27.9731 31.2166L32.4824 32.3088L31.2653 27.8308L25.5078 25.5371Z" fill="url(#paint6_linear_2285_14449)"/>
<path d="M26.7043 17.5647L24.2702 13.5859L21.9766 17.6427L24.4262 23.3222L26.7043 17.5647Z" fill="url(#paint7_linear_2285_14449)"/>
<path d="M31.2341 21.1538L32.3732 16.6445L27.8639 17.846L25.5234 23.5723L31.2341 21.1538Z" fill="url(#paint8_linear_2285_14449)"/>
<path d="M26.0391 24.6488L31.8122 26.8801L35.7598 24.4304L31.703 22.1523L26.0391 24.6488Z" fill="url(#paint9_linear_2285_14449)"/>
<path d="M23.3183 23.635L20.8686 17.9555L16.3438 16.8477L17.5764 21.3413L23.3183 23.635Z" fill="url(#paint10_linear_2285_14449)"/>
<path d="M22.2422 31.8259L24.6763 35.7734L26.9543 31.7166L24.4734 26.0527L22.2422 31.8259Z" fill="url(#paint11_linear_2285_14449)"/>
<path d="M17.7406 28.2212L16.6016 32.7305L21.0952 31.5447L23.4357 25.8027L17.7406 28.2212Z" fill="url(#paint12_linear_2285_14449)"/>
<path d="M22.916 24.7117L17.1273 22.4805L13.1797 24.9301L17.2365 27.1926L22.916 24.7117Z" fill="url(#paint13_linear_2285_14449)"/>
<path d="M27.0859 28.0248L27.613 29.8874L31.2526 31.2507L30.5121 27.084C29.4203 27.66 28.3535 27.8328 27.0859 28.0248Z" fill="url(#paint14_linear_2285_14449)"/>
<path d="M17.7403 29.166L16.6641 31.2493L22.1021 30.5518L22.9141 29.579C21.1014 29.5239 19.3642 29.3863 17.7403 29.166Z" fill="url(#paint15_linear_2285_14449)"/>
<path d="M23.9729 29.391C23.8354 29.391 23.6979 29.391 23.5604 29.391L22.9141 31.335L25.073 35.416L27.0807 31.2386L26.2969 29.166C25.5406 29.2946 24.7705 29.3749 23.9729 29.391Z" fill="url(#paint16_linear_2285_14449)"/>
<path d="M16.1853 17.3456L14.5859 17.491L16.1712 17.9478L16.4378 18.4046L16.536 20.834L16.8446 18.4461L17.1392 18.0516L18.7526 17.9062L17.1673 17.4494L16.9008 16.9926L16.7885 14.584L16.4939 16.9511L16.1853 17.3456Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_2285_14449" x1="47.396" y1="-7.61476" x2="16.0114" y2="57.8089" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFD243"/>
<stop offset="1" stop-color="#CD571B"/>
</linearGradient>
<linearGradient id="paint1_linear_2285_14449" x1="28.8998" y1="31.4915" x2="20.7917" y2="48.3936" gradientUnits="userSpaceOnUse">
<stop stop-color="#E08C3C"/>
<stop offset="1" stop-color="#FFD06F"/>
</linearGradient>
<linearGradient id="paint2_linear_2285_14449" x1="14.2589" y1="73.2536" x2="35.8054" y2="-34.7135" gradientUnits="userSpaceOnUse">
<stop stop-color="#F5942A"/>
<stop offset="1" stop-color="#F5D85D"/>
</linearGradient>
<linearGradient id="paint3_linear_2285_14449" x1="18.7637" y1="50.4652" x2="32.8478" y2="-20.1086" gradientUnits="userSpaceOnUse">
<stop stop-color="#E08C3C"/>
<stop offset="1" stop-color="#FFD06F"/>
</linearGradient>
<linearGradient id="paint4_linear_2285_14449" x1="31.0872" y1="-3.53858" x2="12.6463" y2="69.1709" gradientUnits="userSpaceOnUse">
<stop offset="0.199" stop-color="#FFDE43"/>
<stop offset="0.5646" stop-color="#FFD23E"/>
<stop offset="1" stop-color="#FFBD37"/>
</linearGradient>
<linearGradient id="paint5_linear_2285_14449" x1="30.1264" y1="-0.0881076" x2="18.1182" y2="47.2584" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint6_linear_2285_14449" x1="25.5829" y1="28.9934" x2="32.417" y2="28.8554" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint7_linear_2285_14449" x1="21.9941" y1="18.5055" x2="26.7166" y2="18.4102" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint8_linear_2285_14449" x1="25.4619" y1="20.1795" x2="32.4347" y2="20.0388" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint9_linear_2285_14449" x1="26.0334" y1="24.6355" x2="35.7609" y2="24.4392" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint10_linear_2285_14449" x1="16.4158" y1="20.3114" x2="23.2499" y2="20.1734" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint11_linear_2285_14449" x1="22.2194" y1="30.9604" x2="26.9421" y2="30.8651" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint12_linear_2285_14449" x1="16.5368" y1="29.3409" x2="23.5097" y2="29.2002" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint13_linear_2285_14449" x1="13.1811" y1="24.9162" x2="22.9086" y2="24.7199" gradientUnits="userSpaceOnUse">
<stop offset="0.0180995" stop-color="#FFFFA0"/>
<stop offset="0.9762" stop-color="#FFEB6A"/>
</linearGradient>
<linearGradient id="paint14_linear_2285_14449" x1="27.0973" y1="29.2703" x2="31.217" y2="29.216" gradientUnits="userSpaceOnUse">
<stop offset="0.1642" stop-color="#FFDE43"/>
<stop offset="0.2484" stop-color="#FFDA42"/>
<stop offset="1" stop-color="#FFBD37"/>
</linearGradient>
<linearGradient id="paint15_linear_2285_14449" x1="16.6082" y1="30.2128" x2="22.9236" y2="29.9506" gradientUnits="userSpaceOnUse">
<stop offset="0.1642" stop-color="#FFDE43"/>
<stop offset="0.2484" stop-color="#FFDA42"/>
<stop offset="1" stop-color="#FFBD37"/>
</linearGradient>
<linearGradient id="paint16_linear_2285_14449" x1="22.9236" y1="32.3583" x2="27.0863" y2="32.2864" gradientUnits="userSpaceOnUse">
<stop offset="0.1642" stop-color="#FFDE43"/>
<stop offset="0.2484" stop-color="#FFDA42"/>
<stop offset="1" stop-color="#FFBD37"/>
</linearGradient>
<clipPath id="clip0_2285_14449">
<rect width="50" height="50" fill="white"/>
</clipPath>
</defs>
</svg>

3
public/assets/images/Vector.svg

@ -0,0 +1,3 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 0C8.1 0 0 8.1 0 18C0 27.9 8.1 36 18 36C27.9 36 36 27.9 36 18C36 8.1 27.9 0 18 0ZM16.5857 7.71429H19.4143V21.8571H16.5857V7.71429ZM18 29.5714C16.9714 29.5714 16.0714 28.6714 16.0714 27.6429C16.0714 26.6143 16.9714 25.7143 18 25.7143C19.0286 25.7143 19.9286 26.6143 19.9286 27.6429C19.9286 28.6714 19.0286 29.5714 18 29.5714Z" fill="#D54747"/>
</svg>

11
public/assets/images/cuida_history-outline.svg

@ -0,0 +1,11 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2106_449)">
<path d="M5.81527 3.10492C6.30043 3.10295 6.69214 2.70806 6.69017 2.2229C6.6882 1.73774 6.29331 1.34603 5.80815 1.348C5.32299 1.34997 4.93128 1.74486 4.93325 2.23002C4.93522 2.71518 5.33011 3.10689 5.81527 3.10492Z" fill="white"/>
<path d="M6.72699 6.66728C6.70265 6.65106 6.67532 6.63985 6.64659 6.63433C6.61787 6.62881 6.58833 6.62908 6.55971 6.63513C6.53109 6.64118 6.50397 6.65288 6.47993 6.66956C6.4559 6.68623 6.43544 6.70754 6.41975 6.73223C6.20869 7.04754 5.94971 7.32797 5.65216 7.56341C5.5799 7.61861 5.31059 7.82837 5.19619 7.7849C5.11703 7.76106 5.16252 7.60493 5.17763 7.53897L5.29264 7.19366C5.34039 7.05289 6.1715 4.55434 6.2626 4.26843C6.39708 3.85056 6.33611 3.44007 5.71492 3.54143C5.54587 3.55969 3.83136 3.78848 3.80062 3.7908C3.77178 3.79279 3.74362 3.80045 3.71774 3.81332C3.69186 3.8262 3.66877 3.84404 3.64979 3.86584C3.63081 3.88764 3.61631 3.91297 3.60711 3.94037C3.59792 3.96777 3.59421 3.99672 3.5962 4.02555C3.59819 4.05439 3.60585 4.08255 3.61872 4.10843C3.6316 4.13431 3.64944 4.1574 3.67124 4.17638C3.69304 4.19536 3.71836 4.20987 3.74577 4.21906C3.77317 4.22826 3.80212 4.23196 3.83095 4.22997C3.83095 4.22997 4.48954 4.14164 4.56199 4.13476C4.5991 4.13098 4.63651 4.13803 4.66971 4.15504C4.70291 4.17205 4.73047 4.19831 4.74908 4.23064C4.78938 4.35351 4.78446 4.48673 4.73523 4.60629C4.67901 4.82617 3.78659 7.37329 3.75864 7.52057C3.72851 7.64384 3.73754 7.77343 3.78447 7.89133C3.83139 8.00924 3.91389 8.10959 4.02049 8.17844C4.22057 8.31063 4.45843 8.3737 4.69772 8.358C4.93046 8.35437 5.16046 8.30725 5.37587 8.21907C5.92189 7.99721 6.49059 7.41065 6.78964 6.95037C6.81536 6.90363 6.82319 6.84912 6.81166 6.79702C6.80013 6.74493 6.77004 6.69881 6.72699 6.66728Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_2106_449">
<rect width="7.9072" height="7.9072" fill="white" transform="translate(1.25 0.927734) rotate(-0.232334)"/>
</clipPath>
</defs>
</svg>

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

@ -0,0 +1,3 @@
<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"/>
</svg>

9
public/assets/images/stash_play-solid.svg

@ -0,0 +1,9 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.8568 12.264C17.3839 10.239 13.0234 12.7432 13.0234 16.764V33.2348C13.0234 37.2557 17.3839 39.7598 20.8568 37.7348L34.9755 29.4973C38.4193 27.489 38.4193 22.5098 34.9755 20.5015L20.8568 12.264Z" fill="url(#paint0_linear_2285_14970)"/>
<defs>
<linearGradient id="paint0_linear_2285_14970" x1="37.7375" y1="38.5196" x2="16.7157" y2="14.8915" gradientUnits="userSpaceOnUse">
<stop stop-color="#FE6F82"/>
<stop offset="1" stop-color="#E03950"/>
</linearGradient>
</defs>
</svg>

21
src/app/globals.css

@ -9,6 +9,8 @@
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--font-arabic: var(--font-amiri);
--font-ryling: var(--font-amiri);
--font-faminela: var(--font-faminela-local);
}
@ -25,6 +27,25 @@ body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
html:lang(ar) body,
:lang(ar) {
font-family: var(--font-arabic), "Times New Roman", serif;
}
.font-arabic,
.font-ryling {
font-family: var(--font-arabic), "Times New Roman", serif;
}
.font-faminela:lang(ar),
.font-ryling:lang(ar),
[dir="rtl"] .font-faminela,
[dir="rtl"] .font-ryling,
[dir="rtl"].font-faminela,
[dir="rtl"].font-ryling {
font-family: var(--font-arabic), "Times New Roman", serif;
}
.app-shell {
width: min(100%, 375px);
min-height: 100vh;

6
src/app/intro/page.tsx

@ -1,6 +1,6 @@
import Image from "next/image";
import Button from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button";
import Image from "next/image";
export default function Intro() {
return (
@ -86,9 +86,7 @@ export default function Intro() {
/>
</div>
<div className="mt-7 pb-5">
<Button >
Submit
</Button>
<Button>Submit</Button>
</div>
</main>
</div>

14
src/app/layout.tsx

@ -1,5 +1,7 @@
import type { Metadata } from "next";
import { Amiri } from "next/font/google";
import localFont from "next/font/local";
import DevClickToComponent from "@/components/dev/dev-click-to-component";
import "./globals.css";
const faminela = localFont({
@ -8,6 +10,13 @@ const faminela = localFont({
display: "swap",
});
const amiri = Amiri({
weight: ["400", "700"],
subsets: ["arabic"],
variable: "--font-amiri",
display: "swap",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
@ -20,8 +29,11 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={faminela.variable}>
<body className={`${faminela.variable} ${amiri.variable}`}>
<div className="app-shell">{children}</div>
{process.env.NODE_ENV === "development" ? (
<DevClickToComponent />
) : null}
</body>
</html>
);

38
src/app/page.tsx

@ -1,15 +1,43 @@
import InfoProgressCard from "@/components/ui/info-progress-card";
import NavigationButton from "@/components/ui/navigation-button";
import Image from "next/image";
import QUESTIONS from "@/data/mock-questions.json"
export default function Home() {
return (
<main className="flex min-h-screen items-start justify-center px-3 py-6">
<div className="w-full max-w-[345px]">
<main className="min-h-screen py-6">
<header className="flex items-center justify-between">
<NavigationButton icon="back" />
<h1 className="font-faminela">Profile registration</h1>
<NavigationButton icon="support" />
</header>
<div className="bg-white/50 rounded-[15px] border border-white p-3.5 mt-7 mb-3">
<div className="flex items-center justify-between">
<p className="text-[#111111] font-bold">Bookings Terms & Conditions</p>
<Image src={"/assets/images/Frame 1116607110.svg"} alt="arrow up" width={20} height={20} />
</div>
<div className="mt-4">
<ul className="px-4 space-y-2">
<li className="text-xs list-disc">You will be contacted by your consultant.</li>
<li className="text-xs list-disc">The call may start 1015 minutes earlier or later than scheduled.</li>
<li className="text-xs list-disc">Make sure you are available and in a quiet place at least 10 minutes before the session.</li>
</ul>
</div>
</div>
<div className="w-full space-y-3">
{
QUESTIONS.map((question) => (
<InfoProgressCard
title="persenoal info"
requiredLabel="(Required)"
key={question.slug}
slug={question.slug}
title={question.title}
requiredLabel={question.required}
estimate="30 min"
progress={46}
progress={question.progress}
tooltip={question.tooltip}
/>
))
}
</div>
</main>
);

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

@ -0,0 +1,92 @@
import { notFound } from "next/navigation";
import QuestionButton from "@/components/questions/question-button";
import QuestionDate from "@/components/questions/question-date";
import QuestionDropdown from "@/components/questions/question-dropdown";
import QuestionFile from "@/components/questions/question-file";
import QuestionNumber from "@/components/questions/question-number";
import QuestionRadio from "@/components/questions/question-radio";
import QuestionSlider from "@/components/questions/question-slider";
import QuestionText from "@/components/questions/question-text";
import Button from "@/components/ui/button";
import InformationSheet from "@/components/ui/information-sheet";
import { PageBackground } from "@/components/utils/page-background";
import {
getQuestionListItemBySlug,
type QuestionField,
questionListItems,
} from "@/data/question-data";
export function generateStaticParams() {
return questionListItems.map((item) => ({
slug: item.slug,
}));
}
type QuestionDetailPageProps = {
params: Promise<{
slug: string;
}>;
};
function renderQuestion(question: QuestionField) {
switch (question.type) {
case "button":
return <QuestionButton question={question} />;
case "date":
return <QuestionDate question={question} />;
case "dropdown":
return <QuestionDropdown question={question} />;
case "file":
return <QuestionFile question={question} />;
case "number":
return <QuestionNumber question={question} />;
case "radio":
return <QuestionRadio question={question} />;
case "slider":
return <QuestionSlider question={question} />;
case "text":
return <QuestionText question={question} />;
default:
return null;
}
}
export default async function QuestionDetailPage({
params,
}: QuestionDetailPageProps) {
const { slug } = await params;
const item = getQuestionListItemBySlug(slug);
if (!item) {
notFound();
}
return (
<>
<PageBackground disabled />
<InformationSheet
icon="play"
title="Answer at Your Own Pace"
description="You can pause the survey anytime and resume later. Your progress is saved automatically."
buttons="Continue"
/>
<main className="-mx-[17px] min-h-screen bg-[#F7F1F0] px-[17px] pt-7 pb-8">
<div className="mx-auto flex w-full max-w-md flex-col gap-6">
<section className="space-y-4">
{item.questions.map((question) => (
<div key={`${item.slug}-${question.title}`}>
{renderQuestion(question)}
</div>
))}
</section>
<Button className="rounded-[14px] from-[#F29BAB] to-[#E88597] py-[16px] shadow-[0_16px_30px_rgba(232,133,151,0.34)]">
Continue
</Button>
</div>
</main>
</>
);
}

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

@ -0,0 +1,55 @@
import BookingTermsCard from "@/components/questions/booking-terms-card";
import QuestionCard from "@/components/questions/question-card";
import Button from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
import { bookingTerms, questionListItems } from "@/data/question-data";
export default function QuestionsListPage() {
return (
<>
<PageBackground disabled />
<main className="-mx-[17px] relative min-h-screen overflow-hidden bg-[#F7F1F0] px-[17px] pt-7 pb-8">
<div className="pointer-events-none absolute inset-x-0 top-0 h-[240px] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.98)_0%,rgba(255,255,255,0.7)_42%,rgba(255,255,255,0)_100%)]" />
<div className="pointer-events-none absolute -top-16 left-1/2 h-[220px] w-[220px] -translate-x-1/2 rounded-full bg-white/70 blur-3xl" />
<div className="relative">
<header className="flex items-center justify-between">
<NavigationButton icon="close" iconLabel="Close questions list" />
<h1 className="text-[13px] font-semibold text-[#151515]">
Profile registration
</h1>
<NavigationButton
icon="support"
iconLabel="Support"
className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]"
/>
</header>
<section className="mt-7">
<BookingTermsCard
title="Bookings Terms & Conditions"
items={bookingTerms}
/>
</section>
<section className="mt-5 space-y-3">
{questionListItems.map((item) => (
<QuestionCard key={item.slug} item={item} />
))}
</section>
<div className="mt-6">
<Button
className="rounded-[14px] from-[#F29BAB] to-[#E88597] py-[16px] shadow-[0_16px_30px_rgba(232,133,151,0.34)]"
description="(Complete Necessary forms)"
>
Find Matches
</Button>
</div>
</div>
</main>
</>
);
}

99
src/components/dev/dev-click-to-component.tsx

@ -0,0 +1,99 @@
"use client";
import { useEffect } from "react";
const IDE_SCHEMES = [
{
matches: ["antigravity"],
createUrl: (locator: string) => `antigravity://file/${locator}`,
},
{
matches: ["cursor"],
createUrl: (locator: string) => `cursor://file/${locator}`,
},
{
matches: ["vscode", "code"],
createUrl: (locator: string) => `vscode://file/${locator}`,
},
{
matches: ["webstorm", "intellij"],
createUrl: (locator: string) => `webstorm://open?file=${locator}`,
},
{
matches: ["sublime"],
createUrl: (locator: string) => `subl://open?url=file://${locator}`,
},
{
matches: ["atom", "nova"],
createUrl: (locator: string) => `atom://open?url=file://${locator}`,
},
] as const;
function parseLocator(locator: string) {
const match = locator.match(/^(.*):(\d+|unknown):(\d+|unknown)$/);
if (!match) {
return { filePath: locator, line: null, column: null };
}
const [, filePath, line, column] = match;
return {
filePath,
line: line === "unknown" ? null : Number(line),
column: column === "unknown" ? null : Number(column),
};
}
export function DevClickToComponent() {
useEffect(() => {
const userAgent = navigator.userAgent.toLowerCase();
const handleClick = (event: MouseEvent) => {
if (!event.altKey) {
return;
}
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const locator = target
.closest<HTMLElement>("[data-locator]")
?.getAttribute("data-locator");
if (!locator) {
return;
}
event.preventDefault();
event.stopPropagation();
const { filePath, line, column } = parseLocator(locator);
const positionSuffix =
line === null ? "" : `:${line}${column === null ? "" : `:${column}`}`;
const ideUrl =
IDE_SCHEMES.find(({ matches }) =>
matches.some((match) => userAgent.includes(match)),
)?.createUrl(`${filePath}${positionSuffix}`) ??
`vscode://file/${filePath}${positionSuffix}`;
try {
window.location.href = ideUrl;
} catch {
window.open(`file://${filePath}`, "_blank", "noopener,noreferrer");
}
};
document.addEventListener("click", handleClick, true);
return () => {
document.removeEventListener("click", handleClick, true);
};
}, []);
return null;
}
export default DevClickToComponent;

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

@ -0,0 +1,29 @@
export const LOCATORS = {
appHomePage: "D:/sajjadi/marriage/src/app/page.tsx",
appIntroPage: "D:/sajjadi/marriage/src/app/intro/page.tsx",
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",
bookingTermsCard:
"D:/sajjadi/marriage/src/components/questions/booking-terms-card.tsx",
questionCard:
"D:/sajjadi/marriage/src/components/questions/question-card.tsx",
sliderPage: "D:/sajjadi/marriage/src/components/sliders/slider-page.tsx",
sliderSlide: "D:/sajjadi/marriage/src/components/sliders/slider-slide.tsx",
sliderSlideOne:
"D:/sajjadi/marriage/src/components/sliders/slider-slide-one.tsx",
sliderSlideTwo:
"D:/sajjadi/marriage/src/components/sliders/slider-slide-two.tsx",
sliderSlideThree:
"D:/sajjadi/marriage/src/components/sliders/slider-slide-three.tsx",
sliderSlideFour:
"D:/sajjadi/marriage/src/components/sliders/slider-slide-four.tsx",
button: "D:/sajjadi/marriage/src/components/ui/button.tsx",
infoProgressCard:
"D:/sajjadi/marriage/src/components/ui/info-progress-card.tsx",
navigationButton:
"D:/sajjadi/marriage/src/components/ui/navigation-button.tsx",
pageBackground:
"D:/sajjadi/marriage/src/components/utils/page-background.tsx",
} as const;

29
src/components/questions/booking-terms-card.tsx

@ -0,0 +1,29 @@
import { IoChevronUp } from "react-icons/io5";
type BookingTermsCardProps = {
title: string;
items: readonly string[];
};
export function BookingTermsCard({ title, items }: BookingTermsCardProps) {
return (
<section className="rounded-[24px] border border-white/80 bg-white/92 px-4 py-4 shadow-[0_18px_40px_rgba(15,23,42,0.06)] backdrop-blur-sm">
<div className="flex items-start justify-between gap-3">
<h2 className="text-[14px] leading-5 font-semibold text-[#1E1E1E]">
{title}
</h2>
<span className="mt-0.5 shrink-0 text-[#2A2A2A]">
<IoChevronUp aria-hidden="true" className="size-4" />
</span>
</div>
<ul className="mt-3 space-y-2.5 pl-4 text-[12px] leading-5 text-[#575757] marker:text-[#2D2D2D]">
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</section>
);
}
export default BookingTermsCard;

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

@ -0,0 +1,30 @@
import type { QuestionField } from "@/data/question-data";
import { IoInformation } from "react-icons/io5";
type QuestionButtonProps = {
question: QuestionField;
};
export function QuestionButton({ question }: QuestionButtonProps) {
return (
<div>
<div className="flex items-center gap-2">
<p className="text-[#111111] text-xs font-semibold">{question.title}</p>
{question.tooltip ? (
<span className="flex h-[14px] w-[14px] items-center justify-center rounded-full mt-0.5 bg-[#F24E63] text-white">
<IoInformation aria-hidden="true" className="text-[9px]" />
</span>
) : (
<span className="h-[14px] w-[14px]" aria-hidden="true" />
)}
</div>
<div>
<p>
{question.description}
</p>
</div>
</div>
);
}
export default QuestionButton;

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

@ -0,0 +1,119 @@
import Link from "next/link";
import type { IconType } from "react-icons";
import {
IoCheckbox,
IoDocumentText,
IoInformation,
IoPeople,
IoPerson,
IoSchool,
} from "react-icons/io5";
import type { QuestionCardIcon, QuestionListItem } from "@/data/question-data";
type QuestionCardProps = {
item: QuestionListItem;
};
const RADIUS = 8;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
const iconMap: Record<QuestionCardIcon, IconType> = {
profile: IoPerson,
education: IoSchool,
details: IoDocumentText,
checklist: IoCheckbox,
contact: IoPeople,
};
export function QuestionCard({ item }: QuestionCardProps) {
const normalizedProgress = Math.max(0, Math.min(item.progress, 100));
const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE;
const CardIcon = iconMap[item.icon];
return (
<Link
href={`/questions-list/${item.slug}`}
aria-label={`Open ${item.title}`}
className="block rounded-[20px] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#F26C85]"
>
<article
id={item.slug}
data-question-slug={item.slug}
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>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-baseline gap-1">
<h2 className="truncate text-[12px] leading-none font-bold text-[#1B1B1B]">
{item.title}
</h2>
{item.required ? (
<span className="shrink-0 text-[9px] leading-none font-semibold text-[#FF5B73]">
(Required)
</span>
) : null}
</div>
<p className="mt-1.5 text-[10px] leading-none font-medium text-[#7A7A7A]">
Estimate time: {item.estimate}
</p>
</div>
<div className="flex h-[44px] w-[44px] shrink-0 flex-col items-end justify-between">
{item.showInfoBadge ? (
<span className="flex h-[14px] w-[14px] items-center justify-center rounded-[6px] bg-[#F24E63] text-white">
<IoInformation aria-hidden="true" className="text-[9px]" />
</span>
) : (
<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}
/>
</svg>
<span className="text-[10px] leading-none font-semibold text-[#1F1F1F]">
{normalizedProgress}%
</span>
</div>
</div>
</div>
{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]">
{item.note}
</p>
) : null}
</article>
</Link>
);
}
export default QuestionCard;

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

@ -0,0 +1,11 @@
import type { QuestionField } from "@/data/question-data";
type QuestionDateProps = {
question: QuestionField;
};
export function QuestionDate({ question }: QuestionDateProps) {
return <div data-question-type={question.type} />;
}
export default QuestionDate;

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

@ -0,0 +1,11 @@
import type { QuestionField } from "@/data/question-data";
type QuestionDropdownProps = {
question: QuestionField;
};
export function QuestionDropdown({ question }: QuestionDropdownProps) {
return <div data-question-type={question.type} />;
}
export default QuestionDropdown;

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

@ -0,0 +1,11 @@
import type { QuestionField } from "@/data/question-data";
type QuestionFileProps = {
question: QuestionField;
};
export function QuestionFile({ question }: QuestionFileProps) {
return <div data-question-type={question.type} />;
}
export default QuestionFile;

11
src/components/questions/question-number.tsx

@ -0,0 +1,11 @@
import type { QuestionField } from "@/data/question-data";
type QuestionNumberProps = {
question: QuestionField;
};
export function QuestionNumber({ question }: QuestionNumberProps) {
return <div data-question-type={question.type} />;
}
export default QuestionNumber;

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

@ -0,0 +1,11 @@
import type { QuestionField } from "@/data/question-data";
type QuestionRadioProps = {
question: QuestionField;
};
export function QuestionRadio({ question }: QuestionRadioProps) {
return <div data-question-type={question.type} />;
}
export default QuestionRadio;

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

@ -0,0 +1,11 @@
import type { QuestionField } from "@/data/question-data";
type QuestionSliderProps = {
question: QuestionField;
};
export function QuestionSlider({ question }: QuestionSliderProps) {
return <div data-question-type={question.type} />;
}
export default QuestionSlider;

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

@ -0,0 +1,11 @@
import type { QuestionField } from "@/data/question-data";
type QuestionTextProps = {
question: QuestionField;
};
export function QuestionText({ question }: QuestionTextProps) {
return <div data-question-type={question.type} />;
}
export default QuestionText;

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

@ -1,11 +1,11 @@
import { SliderSlide, type SliderSlideProps } from "@/components/sliders/slider-slide";
import Image from "next/image";
import type { SliderSlideProps } from "@/components/sliders/slider-slide";
export function SliderSlideThree({ index }: SliderSlideProps) {
return (
<section
aria-label={`Slide ${index + 1}`}
className="flex justify-between h-full min-h-0 w-full shrink-0 flex-col"
className="flex h-full min-h-0 w-full shrink-0 flex-col justify-between"
>
<div className="text-center">
<p className="text-[#747474] text-xs font-semibold">Submit Process</p>
@ -14,14 +14,24 @@ export function SliderSlideThree({ index }: SliderSlideProps) {
</p>
</div>
<div className="flex justify-evenly items-center">
<div className="flex items-center justify-evenly">
<div className="flex flex-col items-center">
<Image src={"/assets/images/Group 27033.svg"} alt="man" width={83} height={83}/>
<p className="text-sm font-semibold text-center">Submit Man</p>
<Image
src={"/assets/images/Group 27033.svg"}
alt="man"
width={83}
height={83}
/>
<p className="text-center text-sm font-semibold">Submit Man</p>
</div>
<div className="flex flex-col items-center">
<Image src={"/assets/images/Group 27032.svg"} alt="man" width={83} height={83}/>
<p className="text-sm font-semibold text-center">Submit Woman</p>
<Image
src={"/assets/images/Group 27032.svg"}
alt="man"
width={83}
height={83}
/>
<p className="text-center text-sm font-semibold">Submit Woman</p>
</div>
</div>
<div className="h-28" />

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

@ -1,10 +1,14 @@
import { IoInformation, IoInformationCircle, IoPerson } from "react-icons/io5";
import Image from "next/image";
import Link from "next/link";
import { useId } from "react";
type InfoProgressCardProps = {
title: string;
requiredLabel?: string;
requiredLabel: boolean;
estimate: string;
progress: number;
tooltip?: string;
slug: string;
};
const RADIUS = 12;
@ -12,53 +16,77 @@ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
export function InfoProgressCard({
title,
requiredLabel = "(Required)",
requiredLabel = false,
estimate,
progress,
tooltip,
slug,
}: InfoProgressCardProps) {
const tooltipId = useId();
const normalizedProgress = Math.max(0, Math.min(progress, 100));
const dashOffset = CIRCUMFERENCE - (normalizedProgress / 100) * CIRCUMFERENCE;
return (
<article className="flex items-center gap-4 rounded-[30px] bg-white px-7 py-7 shadow-[0_12px_40px_rgba(15,23,42,0.08)]">
<div className="relative flex h-[84px] w-[84px] shrink-0 items-center justify-center rounded-[22px] bg-linear-to-br from-[#EA3D59] to-[#F76B8A] text-white">
<IoPerson aria-hidden="true" className="translate-y-0.5 text-[38px]" />
<span className="absolute bottom-4 right-4 flex h-5 w-5 items-center justify-center rounded-full bg-white/18">
<IoInformation aria-hidden="true" className="text-[12px]" />
</span>
<Link href={`/questions-list/${slug}`} className="block">
<article className="flex items-center gap-2 rounded-[15px] bg-white px-3 py-3 shadow-[0_12px_40px_rgba(15,23,42,0.08)]">
<div className="relative flex h-[44px] w-[44px] shrink-0 items-center justify-center rounded-[10px] bg-linear-to-br from-[#EA3D59] to-[#F76B8A] text-white">
<Image
src={"/assets/images/mingcute_user-info-fill.svg"}
alt="User Info"
width={24}
height={24}
/>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h2 className="truncate text-[28px] leading-none font-extrabold tracking-[-0.03em] text-[#171717]">
<h2 className="truncate text-[16px] leading-none font-bold tracking-[-0.03em] text-[#171717]">
{title}{" "}
<span className="text-[18px] font-bold text-[#FF5C6C]">
{requiredLabel}
<span className="text-[10px] font-bold text-[#FF6175]">
{requiredLabel && "(Required)"}
</span>
</h2>
<p className="mt-4 text-[18px] font-semibold text-[#7A7A7A]">
Estimate time: {estimate}
</p>
</div>
{tooltip ? (
<div className="group relative shrink-0">
<button
type="button"
aria-label="More information"
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-2xl bg-[#F24E63] text-white"
aria-describedby={tooltipId}
className="flex h-4 w-4 items-center justify-center rounded-md bg-[#F24E63] text-white"
>
<IoInformationCircle aria-hidden="true" className="text-[20px]" />
<Image
src={"/assets/images/cuida_history-outline.svg"}
alt=""
width={10}
height={10}
/>
</button>
<div
id={tooltipId}
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"
>
{tooltip}
</div>
</div>
) : null}
</div>
<div className="mt-4 flex justify-end">
<div className="flex items-center gap-2">
<div className="mt-2 flex items-center justify-between">
<p className="text-[12px] font-semibold text-[#747474]">
Estimate time: {estimate}
</p>
<div className="flex items-center gap-1">
{normalizedProgress < 100 ? (
<svg
aria-hidden="true"
className="-rotate-90"
viewBox="0 0 32 32"
width="28"
height="28"
width="12"
height="12"
>
<circle
cx="16"
@ -80,13 +108,23 @@ export function InfoProgressCard({
strokeDashoffset={dashOffset}
/>
</svg>
<span className="text-[20px] font-extrabold text-[#171717]">
) : (
<Image
src={"/assets/images/Group 2.svg"}
width={12}
height={12}
alt="checked"
className="mt-0.5"
/>
)}
<span className="text-[12px] font-semibold text-[#111111]">
{normalizedProgress}%
</span>
</div>
</div>
</div>
</article>
</Link>
);
}

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

@ -0,0 +1,206 @@
"use client";
import Image, { type StaticImageData } from "next/image";
import type { HTMLAttributes, ReactNode } from "react";
import { useEffect, useState } from "react";
import Button from "@/components/ui/button";
type InformationSheetPresetIcon =
| "play"
| "warning"
| "coin"
| "stash_play-solid.svg"
| "warning.svg"
| "coin.svg";
type InformationSheetIcon =
| InformationSheetPresetIcon
| string
| {
src: string | StaticImageData;
alt?: string;
width?: number;
height?: number;
};
export type InformationSheetProps = Omit<
HTMLAttributes<HTMLDivElement>,
"children" | "title"
> & {
icon?: InformationSheetIcon;
title: ReactNode;
description?: ReactNode;
buttons?: ReactNode;
closeOnOutside?: boolean;
};
const DEFAULT_ICON = {
src: "/assets/images/stash_play-solid.svg",
alt: "Play",
width: 50,
height: 50,
} as const;
const ICON_PRESETS: Record<
InformationSheetPresetIcon,
{
src: string;
alt: string;
width: number;
height: number;
}
> = {
play: DEFAULT_ICON,
"stash_play-solid.svg": DEFAULT_ICON,
warning: {
src: "/assets/images/Vector.svg",
alt: "Warning",
width: 36,
height: 36,
},
"warning.svg": {
src: "/assets/images/Vector.svg",
alt: "Warning",
width: 36,
height: 36,
},
coin: {
src: "/assets/images/Inner Plugdsain Iframe.svg",
alt: "Coin",
width: 50,
height: 50,
},
"coin.svg": {
src: "/assets/images/Inner Plugdsain Iframe.svg",
alt: "Coin",
width: 50,
height: 50,
},
};
function resolveIcon(icon: InformationSheetIcon | undefined) {
if (!icon) {
return DEFAULT_ICON;
}
if (typeof icon === "string") {
return (
ICON_PRESETS[icon as InformationSheetPresetIcon] ?? {
src: icon,
alt: "Information",
width: DEFAULT_ICON.width,
height: DEFAULT_ICON.height,
}
);
}
return {
src: icon.src,
alt: icon.alt ?? "Information",
width: icon.width ?? DEFAULT_ICON.width,
height: icon.height ?? DEFAULT_ICON.height,
};
}
export function InformationSheet({
icon,
title,
description,
buttons,
closeOnOutside = true,
className,
...props
}: InformationSheetProps) {
const [isOpen, setIsOpen] = useState(true);
const resolvedIcon = resolveIcon(icon);
const resolvedButtons =
typeof buttons === "string" ? (
<Button onClick={() => setIsOpen(false)} className="py-[18px]">
{buttons}
</Button>
) : (
buttons
);
useEffect(() => {
if (!isOpen) {
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;
};
}, [isOpen]);
if (!isOpen) {
return null;
}
return (
<div
className="fixed inset-0 z-50 flex items-end justify-center bg-[#171717]/55"
role="dialog"
aria-modal="true"
aria-label="Information sheet"
tabIndex={-1}
onClick={(event) => {
if (closeOnOutside && event.target === event.currentTarget) {
setIsOpen(false);
}
}}
onKeyDown={(event) => {
if (
closeOnOutside &&
event.target === event.currentTarget &&
(event.key === "Escape" || event.key === "Enter" || event.key === " ")
) {
event.preventDefault();
setIsOpen(false);
}
}}
>
<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)]",
className,
]
.filter(Boolean)
.join(" ")}
>
<div className="mx-auto flex flex-col items-center">
<Image
src={resolvedIcon.src}
alt={resolvedIcon.alt}
width={resolvedIcon.width}
height={resolvedIcon.height}
priority
/>
<h2 className="mt-2.5 leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]">
{title}
</h2>
{description ? (
<p className="mt-3 text-sm text-[#4D4D4D]">{description}</p>
) : null}
{resolvedButtons ? (
<div className="mt-4 flex w-full flex-col gap-3">
{resolvedButtons}
</div>
) : null}
</div>
</section>
</div>
);
}
export default InformationSheet;

4
src/components/utils/page-background.tsx

@ -14,7 +14,9 @@ export function PageBackground({
useEffect(() => {
const { body } = document;
const previousMode = body.dataset.pageBackground;
const previousImage = body.style.getPropertyValue("--page-background-image");
const previousImage = body.style.getPropertyValue(
"--page-background-image",
);
if (disabled) {
body.dataset.pageBackground = "none";

266
src/data/mock-questions.json

@ -0,0 +1,266 @@
[
{
"title": "Personal Information",
"icon": "user-circle",
"slug": "personal-information",
"required": true,
"estimateTime": "5 minutes",
"tooltip": "Basic identity and profile details for the applicant.",
"progress": 35,
"description": "Collects general personal details to start the marriage application flow.",
"questions": [
{
"title": "Full Name",
"type": "text",
"required": true,
"description": "Enter your legal full name as it appears on official documents.",
"tooltip": "Use your passport or national ID spelling.",
"extras": {
"placeHolder": "e.g. Sara Ahmadi",
"range": [0, 0],
"options": []
}
},
{
"title": "Date of Birth",
"type": "date",
"required": true,
"description": "Select your date of birth.",
"tooltip": "Make sure the date matches your official record.",
"extras": {
"placeHolder": "YYYY-MM-DD",
"range": [0, 0],
"options": []
}
},
{
"title": "Gender",
"type": "radio",
"required": true,
"description": "Choose the gender option that applies to you.",
"tooltip": "Only one option can be selected.",
"extras": {
"placeHolder": "",
"range": [0, 0],
"options": ["Female", "Male", "Prefer not to say"]
}
},
{
"title": "Age",
"type": "number",
"required": true,
"description": "Provide your current age in years.",
"tooltip": "Numbers only.",
"extras": {
"placeHolder": "e.g. 29",
"range": [18, 80],
"options": []
}
}
]
},
{
"title": "Preferences",
"icon": "heart-handshake",
"slug": "preferences",
"required": false,
"estimateTime": "7 minutes",
"tooltip": "Relationship expectations and lifestyle preferences.",
"progress": 60,
"description": "Captures values, preferences, and match expectations.",
"questions": [
{
"title": "Preferred City",
"type": "dropdown",
"required": false,
"description": "Select the city you prefer to live in after marriage.",
"tooltip": "You can use this to help with compatibility matching.",
"extras": {
"placeHolder": "Choose a city",
"range": [0, 0],
"options": ["Tehran", "Isfahan", "Shiraz", "Tabriz", "Mashhad"]
}
},
{
"title": "Describe Your Ideal Partner",
"type": "text",
"required": false,
"description": "Write a short description of the qualities you value most.",
"tooltip": "Keep it concise and specific.",
"extras": {
"placeHolder": "Kind, family-oriented, emotionally mature...",
"range": [0, 0],
"options": []
}
},
{
"title": "Importance of Family Values",
"type": "slider",
"required": true,
"description": "Rate how important family values are to you.",
"tooltip": "Move the slider from low to high importance.",
"extras": {
"placeHolder": "",
"range": [1, 10],
"options": []
}
},
{
"title": "Ready to Continue",
"type": "button",
"required": false,
"description": "Confirms that you want to proceed to the next section.",
"tooltip": "This can be used as a UI action trigger.",
"extras": {
"placeHolder": "Continue",
"range": [0, 0],
"options": ["Continue"]
}
}
]
},
{
"title": "Documents",
"icon": "file-text",
"slug": "documents",
"required": true,
"estimateTime": "3 minutes",
"tooltip": "Upload supporting documents required for verification.",
"progress": 100,
"description": "Handles document uploads and verification-related information.",
"questions": [
{
"title": "National ID Upload",
"type": "file",
"required": true,
"description": "Upload a clear image or PDF of your national ID.",
"tooltip": "Accepted formats can be restricted in the UI layer.",
"extras": {
"placeHolder": "Choose a file",
"range": [0, 0],
"options": [".jpg", ".png", ".pdf"]
}
},
{
"title": "Marriage Timeline",
"type": "dropdown",
"required": false,
"description": "Choose your preferred timeline for marriage.",
"tooltip": "This helps prioritize urgency and compatibility.",
"extras": {
"placeHolder": "Select a timeline",
"range": [0, 0],
"options": ["Within 6 months", "6-12 months", "1-2 years", "Flexible"]
}
}
]
},
{
"title": "Question Type Showcase",
"icon": "layout-grid",
"slug": "question-type-showcase",
"required": false,
"estimateTime": "10 minutes",
"tooltip": "A complete sample box that demonstrates every supported question type with all fields populated.",
"progress": 0,
"description": "Includes one example of each question type so the UI can be tested against a complete dataset.",
"questions": [
{
"title": "Short Biography",
"type": "text",
"required": true,
"description": "Provide a short written introduction.",
"tooltip": "This exercises the text input type.",
"extras": {
"placeHolder": "Write a short introduction about yourself",
"range": [0, 0],
"options": []
}
},
{
"title": "Preferred Wedding Date",
"type": "date",
"required": false,
"description": "Select your preferred wedding date.",
"tooltip": "This exercises the date picker type.",
"extras": {
"placeHolder": "YYYY-MM-DD",
"range": [0, 0],
"options": []
}
},
{
"title": "Preferred Contact Method",
"type": "radio",
"required": true,
"description": "Choose one preferred contact method.",
"tooltip": "This exercises the radio selection type.",
"extras": {
"placeHolder": "Select one option",
"range": [0, 0],
"options": ["Phone", "WhatsApp", "Email"]
}
},
{
"title": "Household Size Preference",
"type": "number",
"required": false,
"description": "Enter the household size you are comfortable with.",
"tooltip": "This exercises the number input type.",
"extras": {
"placeHolder": "e.g. 4",
"range": [1, 12],
"options": []
}
},
{
"title": "Current City",
"type": "dropdown",
"required": true,
"description": "Choose the city you currently live in.",
"tooltip": "This exercises the dropdown type.",
"extras": {
"placeHolder": "Select your city",
"range": [0, 0],
"options": ["Tehran", "Karaj", "Shiraz", "Mashhad"]
}
},
{
"title": "Importance of Shared Goals",
"type": "slider",
"required": true,
"description": "Rate how important shared long-term goals are to you.",
"tooltip": "This exercises the slider type.",
"extras": {
"placeHolder": "Move the slider",
"range": [1, 10],
"options": []
}
},
{
"title": "Review Answers",
"type": "button",
"required": false,
"description": "Use this action to review the current section.",
"tooltip": "This exercises the button type.",
"extras": {
"placeHolder": "Review",
"range": [0, 0],
"options": ["Review"]
}
},
{
"title": "Profile Photo Upload",
"type": "file",
"required": false,
"description": "Upload a profile photo or supporting image.",
"tooltip": "This exercises the file upload type.",
"extras": {
"placeHolder": "Choose an image",
"range": [0, 0],
"options": [".jpg", ".jpeg", ".png"]
}
}
]
}
]

83
src/data/question-data.ts

@ -0,0 +1,83 @@
import rawQuestions from "@/data/mock-questions.json";
export const bookingTerms = [
"You will be contacted by your consultant.",
"The call may start 10-15 minutes earlier or later than scheduled.",
"Make sure you are available and in a quiet place at least 10 minutes before the session.",
] as const;
export type QuestionCardIcon =
| "profile"
| "education"
| "details"
| "checklist"
| "contact";
type QuestionExtras = {
placeHolder: string;
range: [number, number];
options: string[];
};
export type QuestionField = {
title: string;
type: string;
required: boolean;
description: string;
tooltip: string;
extras: QuestionExtras;
};
export type QuestionListItem = {
slug: string;
title: string;
estimate: string;
progress: number;
icon: QuestionCardIcon;
required?: boolean;
note?: string;
showInfoBadge?: boolean;
summary: string;
checkpoints: readonly string[];
tooltip: string;
questions: readonly QuestionField[];
};
type RawQuestionListItem = {
title: string;
icon: string;
slug: string;
required?: boolean;
estimateTime: string;
tooltip: string;
progress: number;
description: string;
questions: QuestionField[];
};
const iconMap: Record<string, QuestionCardIcon> = {
"user-circle": "profile",
"heart-handshake": "details",
"file-text": "contact",
"layout-grid": "checklist",
};
export const questionListItems: readonly QuestionListItem[] = (
rawQuestions as RawQuestionListItem[]
).map((item) => ({
slug: item.slug,
title: item.title,
estimate: item.estimateTime,
progress: item.progress,
icon: iconMap[item.icon] ?? "details",
required: item.required,
showInfoBadge: Boolean(item.tooltip),
summary: item.description,
checkpoints: item.questions.map((question) => question.title),
tooltip: item.tooltip,
questions: item.questions,
}));
export function getQuestionListItemBySlug(slug: string) {
return questionListItems.find((item) => item.slug === slug);
}

40
src/plugins/add-data-locator.cjs

@ -0,0 +1,40 @@
module.exports = function addDataLocator({ types: t }) {
return {
name: "add-data-locator",
visitor: {
JSXOpeningElement(path, state) {
if (process.env.NODE_ENV !== "development") {
return;
}
const filePath = state.file.opts.filename;
if (!filePath || filePath.includes("node_modules")) {
return;
}
const attributeExists = path.node.attributes.some(
(attribute) =>
t.isJSXAttribute(attribute) &&
t.isJSXIdentifier(attribute.name) &&
attribute.name.name === "data-locator",
);
if (attributeExists) {
return;
}
const lineNumber = path.node.loc?.start.line ?? "unknown";
const columnNumber = path.node.loc?.start.column ?? "unknown";
const locatorValue = `${filePath}:${lineNumber}:${columnNumber}`;
path.node.attributes.push(
t.jsxAttribute(
t.jsxIdentifier("data-locator"),
t.stringLiteral(locatorValue),
),
);
},
},
};
};
Loading…
Cancel
Save