Browse Source
feat: add marriage profile and section data hooks
feat: add marriage profile and section data hooks
- Implemented `useMarriageProfileQuery` and `getMarriageProfile` for fetching main marriage profile data. - Created `useMarriageSectionDataQuery` and `updateMarriageSectionData` for managing section data with query and mutation hooks. - Added `useMarriageSectionsQuery` for retrieving available marriage sections. feat: add temporary media upload hook - Introduced `useUploadTmpMediaMutation` and `uploadTmpMedia` for handling temporary media uploads. feat: implement internationalization support - Added localization configuration and dictionaries for English and Persian languages. - Created `I18nProvider` for managing locale and dictionary context. - Implemented utility functions for locale management and path localization. feat: define API communication layer - Established an HTTP client using Axios with proxy path handling for API requests.master
106 changed files with 5869 additions and 313 deletions
-
13next-dev-3000.err.log
-
4next-dev-3000.log
-
8next-dev-3001.err.log
-
10next-dev-3001.log
-
310package-lock.json
-
2package.json
-
31proxy.ts
-
BINpublic/assets/images/Avatar Image.png
-
BINpublic/assets/images/Ellipse 370.png
-
5public/assets/images/Frame 1597880476.svg
-
5public/assets/images/Frame 1597880477.svg
-
6public/assets/images/Frame 2095586679.svg
-
BINpublic/assets/images/Group 1000004916.png
-
27public/assets/images/Group 1597880466.svg
-
20public/assets/images/Group 1597880467.svg
-
39public/assets/images/Group 1597880468.svg
-
21public/assets/images/Group 159788fd0467.svg
-
3public/assets/images/Icon.svg
-
3public/assets/images/Image.svg
-
9public/assets/images/Union.svg
-
3public/assets/images/Vectorcheck.svg
-
11public/assets/images/cuida_history-outlfdsaine.svg
-
6public/assets/images/disabled Group 27033.svg
-
33public/assets/images/enabled Group 27032.svg
-
3public/assets/images/fluent_arrow-exit-12-regular.svg
-
12public/assets/images/icon-park-outline_diamond.svg
-
12public/assets/images/icon-park-solid_success.svg
-
3public/assets/images/material-symbols_lock.svg
-
15public/assets/images/noun-wedding-rings-6540466 1.svg
-
8public/assets/images/tabler_user-filled.svg
-
8public/assets/images/typcn_heart-full-outline.svg
-
1src/app/[lang]/candidate-contact/page.tsx
-
1src/app/[lang]/finding-match/page.tsx
-
1src/app/[lang]/intro/page.tsx
-
31src/app/[lang]/layout.tsx
-
1src/app/[lang]/new-match/page.tsx
-
1src/app/[lang]/page.tsx
-
4src/app/[lang]/questions-list/[slug]/page.tsx
-
1src/app/[lang]/questions-list/page.tsx
-
1src/app/[lang]/request-accepted/page.tsx
-
1src/app/[lang]/slider/page.tsx
-
303src/app/api/proxy/route.ts
-
64src/app/candidate-contact/page.tsx
-
106src/app/finding-match/page.tsx
-
53src/app/globals.css
-
46src/app/intro/page.tsx
-
5src/app/layout.tsx
-
193src/app/new-match/page.tsx
-
45src/app/page.tsx
-
26src/app/providers.tsx
-
117src/app/questions-list/[slug]/answer-pace-sheet.tsx
-
123src/app/questions-list/[slug]/page.tsx
-
169src/app/questions-list/page.tsx
-
89src/app/questions-list/sections-request.tsx
-
96src/app/request-accepted/page.tsx
-
445src/components/questions/question-answer-storage.tsx
-
39src/components/questions/question-button.tsx
-
26src/components/questions/question-card.tsx
-
27src/components/questions/question-date.tsx
-
38src/components/questions/question-dropdown.tsx
-
39src/components/questions/question-exit-navigation-button.tsx
-
73src/components/questions/question-file.tsx
-
42src/components/questions/question-number.tsx
-
66src/components/questions/question-photo.tsx
-
128src/components/questions/question-progress-tracker.tsx
-
58src/components/questions/question-radio.tsx
-
70src/components/questions/question-section-flow.tsx
-
98src/components/questions/question-slider.tsx
-
199src/components/questions/question-snap-list.tsx
-
39src/components/questions/question-text.tsx
-
44src/components/questions/question-title.tsx
-
82src/components/questions/required-steps-card.tsx
-
208src/components/sliders/slider-page.tsx
-
40src/components/sliders/slider-slide-five.tsx
-
24src/components/sliders/slider-slide-four.tsx
-
23src/components/sliders/slider-slide-one.tsx
-
106src/components/sliders/slider-slide-three.tsx
-
20src/components/sliders/slider-slide-two.tsx
-
3src/components/sliders/slider-slide.tsx
-
64src/components/ui/button.tsx
-
212src/components/ui/call-result-sheet.tsx
-
159src/components/ui/dismiss-reason-sheet.tsx
-
129src/components/ui/information-sheet.tsx
-
37src/components/ui/language-switcher.tsx
-
73src/components/ui/navigation-button.tsx
-
24src/components/ui/sticky-header.tsx
-
52src/data/question-data.ts
-
19src/hooks/marriage/options.ts
-
3src/hooks/marriage/path-param.ts
-
16src/hooks/marriage/query-keys.ts
-
158src/hooks/marriage/types.ts
-
50src/hooks/marriage/use-case-respond.ts
-
32src/hooks/marriage/use-contact-info.ts
-
50src/hooks/marriage/use-contact-status.ts
-
37src/hooks/marriage/use-match-start.ts
-
38src/hooks/marriage/use-profile-basic.ts
-
25src/hooks/marriage/use-profile-main.ts
-
74src/hooks/marriage/use-section-data.ts
-
23src/hooks/marriage/use-sections.ts
-
37src/hooks/marriage/use-upload-tmp-media.ts
@ -0,0 +1,13 @@ |
|||
Error: spawn EPERM |
|||
at ChildProcess.spawn (node:internal/child_process:421:11) |
|||
at spawn (node:child_process:796:9) |
|||
at fork (node:child_process:174:10) |
|||
at D:\sajjadi\marriage\node_modules\next\dist\cli\next-dev.js:253:45 |
|||
at new Promise (<anonymous>) |
|||
at startServer (D:\sajjadi\marriage\node_modules\next\dist\cli\next-dev.js:221:16) |
|||
at runDevServer (D:\sajjadi\marriage\node_modules\next\dist\cli\next-dev.js:355:23) |
|||
at Module.nextDev (D:\sajjadi\marriage\node_modules\next\dist\cli\next-dev.js:363:11) { |
|||
errno: -4048, |
|||
code: 'EPERM', |
|||
syscall: 'spawn' |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
|
|||
> marriage@0.1.0 dev |
|||
> next dev --hostname 127.0.0.1 --port 3000 |
|||
|
|||
@ -0,0 +1,8 @@ |
|||
⨯ Another next dev server is already running. |
|||
|
|||
- Local: http://localhost:3000 |
|||
- PID: 9080 |
|||
- Dir: D:\sajjadi\marriage |
|||
- Log: .next\dev\logs\next-development.log |
|||
|
|||
Run taskkill /PID 9080 /F to stop it. |
|||
@ -0,0 +1,10 @@ |
|||
|
|||
> marriage@0.1.0 dev |
|||
> next dev --port 3001 |
|||
|
|||
▲ Next.js 16.2.3 (Turbopack) |
|||
- Local: http://localhost:3001 |
|||
- Network: http://10.2.0.2:3001 |
|||
- Environments: .env |
|||
✓ Ready in 634ms |
|||
[?25h |
|||
@ -0,0 +1,31 @@ |
|||
import { type NextRequest, NextResponse } from "next/server"; |
|||
import { defaultLocale, isLocale } from "@/i18n/config"; |
|||
|
|||
function getPreferredLocale(request: NextRequest) { |
|||
const acceptLanguage = request.headers.get("accept-language") ?? ""; |
|||
|
|||
if (acceptLanguage.toLowerCase().includes("fa")) { |
|||
return "fa"; |
|||
} |
|||
|
|||
return defaultLocale; |
|||
} |
|||
|
|||
export function proxy(request: NextRequest) { |
|||
const { pathname } = request.nextUrl; |
|||
const pathnameHasLocale = isLocale(pathname.split("/")[1]); |
|||
|
|||
if (pathnameHasLocale) { |
|||
return NextResponse.next(); |
|||
} |
|||
|
|||
const locale = getPreferredLocale(request); |
|||
const url = request.nextUrl.clone(); |
|||
url.pathname = `/${locale}${pathname === "/" ? "" : pathname}`; |
|||
|
|||
return NextResponse.redirect(url); |
|||
} |
|||
|
|||
export const config = { |
|||
matcher: ["/((?!api|_next|favicon.ico|assets|fonts).*)"], |
|||
}; |
|||
|
After Width: 20 | Height: 20 | Size: 1.4 KiB |
|
After Width: 75 | Height: 75 | Size: 1.3 KiB |
@ -0,0 +1,5 @@ |
|||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M15.3943 15.6807L10.1485 12.1556C9.98247 12.0478 9.79678 11.9739 9.6021 11.9383C9.40741 11.9026 9.2076 11.9058 9.01416 11.9477C8.82072 11.9896 8.63749 12.0694 8.47502 12.1824C8.31255 12.2955 8.17406 12.4395 8.06753 12.6063C7.84811 12.9414 7.77032 13.3497 7.85116 13.742C7.93199 14.1343 8.16487 14.4786 8.49888 14.6997L17.9311 20.9617L18.5759 21.395L15.9531 22.4139C15.5557 22.5735 15.2336 22.8779 15.0518 23.2656C14.87 23.6534 14.842 24.0957 14.9734 24.5033C15.1049 24.9109 15.386 25.2534 15.7601 25.4619C16.1342 25.6704 16.5734 25.7292 16.9892 25.6266L21.8664 24.4287C22.9735 24.1566 23.527 24.0206 24.0137 23.7812C24.7574 23.4153 25.3984 22.8703 25.8792 22.1953C26.1936 21.7531 26.4169 21.2286 26.8635 20.1798L27.8046 17.9698C28.2565 16.9083 28.3274 15.7233 28.0054 14.6155C27.6833 13.5077 26.9881 12.5454 26.0376 11.8916L22.4371 9.41503L22.018 9.14092C21.852 9.03315 21.6663 8.95929 21.4716 8.92362C21.2769 8.88794 21.0771 8.89115 20.8836 8.93305C20.6902 8.97495 20.507 9.05472 20.3445 9.16776C20.182 9.2808 20.0435 9.42487 19.937 9.59168C19.7173 9.92679 19.6392 10.3353 19.72 10.7278C19.8008 11.1203 20.0338 11.4648 20.368 11.6859L20.7871 11.96M15.3943 15.6807L16.6502 16.5013M15.3943 15.6807C15.0601 15.4596 14.8271 15.1151 14.7464 14.7226C14.6656 14.3301 14.7436 13.9216 14.9634 13.5864C15.0699 13.4196 15.2084 13.2756 15.3708 13.1625C15.5333 13.0495 15.7165 12.9697 15.91 12.9278C16.1034 12.8859 16.3032 12.8827 16.4979 12.9184C16.6926 12.9541 16.8783 13.0279 17.0443 13.1357L17.8816 13.6835M17.8816 13.6835L18.2998 13.9572M17.8816 13.6835C17.5476 13.4622 17.3148 13.1177 17.234 12.7253C17.1533 12.3329 17.2312 11.9244 17.4506 11.5893C17.5571 11.4225 17.6956 11.2785 17.8579 11.1655C18.0203 11.0525 18.2035 10.9728 18.3968 10.9308C18.5901 10.8889 18.7898 10.8856 18.9844 10.9212C19.179 10.9568 19.3647 11.0305 19.5307 11.1381L19.9498 11.4122" stroke="#36363C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|||
<path d="M7.42188 19.9629C7.42188 19.9629 5.42188 22.4629 4.92188 22.4629M4.92188 22.4629C4.42188 22.4629 2.42188 19.9629 2.42188 19.9629M4.92188 22.4629C4.92188 19.9245 4.92188 15.9629 4.92188 15.9629" stroke="#36363C" stroke-width="1.5" stroke-linecap="round"/> |
|||
<path d="M7.42188 6.46289C7.42188 6.46289 5.42188 3.96289 4.92188 3.96289M4.92188 3.96289C4.42188 3.96289 2.42188 6.46289 2.42188 6.46289M4.92188 3.96289C4.92188 6.5013 4.92188 10.4629 4.92188 10.4629" stroke="#36363C" stroke-width="1.5" stroke-linecap="round"/> |
|||
</svg> |
|||
@ -0,0 +1,5 @@ |
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M11.5163 20.8158H5.25543C4.42519 20.8158 3.62895 20.486 3.04188 19.8989C2.45481 19.3119 2.125 18.5156 2.125 17.6854V16.6419H9.95109M16.7337 9.33755V4.12016C16.7337 3.7074 16.8561 3.30391 17.0854 2.96071C17.3147 2.61751 17.6407 2.35002 18.022 2.19206C18.4033 2.03411 18.823 1.99278 19.2278 2.0733C19.6326 2.15383 20.0045 2.35259 20.2964 2.64446C20.5882 2.93633 20.787 3.30819 20.8675 3.71302C20.948 4.11785 20.9067 4.53746 20.7487 4.9188C20.5908 5.30015 20.3233 5.62608 19.9801 5.8554C19.6369 6.08472 19.2334 6.20712 18.8207 6.20712H16.7337" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|||
<path d="M18.8196 2.0332H7.34137C6.51113 2.0332 5.71489 2.36302 5.12782 2.95009C4.54075 3.53716 4.21094 4.33339 4.21094 5.16364V16.6419M8.38485 6.20712H12.5588M8.38485 10.381H12.5588" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9022 11.7255C17.3097 11.318 17.9703 11.318 18.3778 11.7255L19.0067 12.3535C19.2023 12.5491 19.4673 12.6591 19.744 12.6591H20.6229C21.1992 12.6591 21.6668 13.1268 21.6668 13.7031V14.582C21.6669 14.8586 21.7769 15.1237 21.9725 15.3193L22.6072 15.955C23.0146 16.3626 23.0147 17.0232 22.6072 17.4306L21.9725 18.0654C21.7768 18.2611 21.6668 18.5269 21.6668 18.8037V19.6845C21.6666 20.2607 21.1991 20.7275 20.6229 20.7275H19.742C19.4655 20.7276 19.2003 20.8377 19.0047 21.0332L18.3778 21.6601C17.9703 22.0673 17.3096 22.0674 16.9022 21.6601L16.2762 21.0332C16.0805 20.8375 15.8147 20.7275 15.5379 20.7275H14.6414C14.0653 20.7274 13.5986 20.2606 13.5985 19.6845V18.788C13.5985 18.5113 13.4884 18.2455 13.2928 18.0498L12.6727 17.4306C12.2653 17.0232 12.2655 16.3626 12.6727 15.955L13.2928 15.3349C13.4882 15.1394 13.5983 14.8741 13.5985 14.5976V13.7031C13.5985 13.1268 14.0652 12.6592 14.6414 12.6591H15.536C15.8127 12.6591 16.0785 12.5492 16.2742 12.3535L16.9022 11.7255ZM20.0438 14.6914C19.8224 14.5069 19.4939 14.5364 19.3094 14.7578L17.0555 17.4619L15.911 16.2988C15.7089 16.0936 15.378 16.0909 15.1727 16.2929C14.9674 16.495 14.9648 16.8259 15.1668 17.0312L16.7156 18.6045C16.8189 18.7092 16.9622 18.7658 17.1092 18.7597C17.2562 18.7536 17.3939 18.6853 17.4881 18.5722L20.1112 15.4257C20.2954 15.2045 20.2648 14.8759 20.0438 14.6914Z" fill="black"/> |
|||
</svg> |
|||
@ -0,0 +1,6 @@ |
|||
<svg width="86" height="92" viewBox="0 0 86 92" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<rect width="86" height="85" rx="42.5" fill="#D7DBE2"/> |
|||
<path d="M56.2838 53.9596C55.5062 52.1052 54.3777 50.4208 52.9612 49.0002C51.5491 47.5754 49.8762 46.4395 48.0351 45.6551C48.0186 45.6468 48.0021 45.6427 47.9856 45.6344C50.5538 43.7668 52.2233 40.7247 52.2233 37.2925C52.2233 31.6067 47.6476 27 42 27C36.3524 27 31.7767 31.6067 31.7767 37.2925C31.7767 40.7247 33.4462 43.7668 36.0144 45.6385C35.9979 45.6468 35.9814 45.651 35.9649 45.6593C34.1181 46.4436 32.461 47.5683 31.0388 49.0043C29.6236 50.426 28.4953 52.1102 27.7162 53.9638C26.9508 55.7785 26.538 57.7241 26.5001 59.6952C26.499 59.7395 26.5067 59.7836 26.5228 59.8248C26.5389 59.8661 26.563 59.9036 26.5937 59.9354C26.6245 59.9671 26.6612 59.9923 26.7018 60.0095C26.7423 60.0267 26.7859 60.0355 26.8299 60.0355H29.3033C29.4847 60.0355 29.6289 59.8903 29.6331 59.7118C29.7155 56.5079 30.9934 53.5073 33.2525 51.233C35.5898 48.8798 38.6939 47.5849 42 47.5849C45.3061 47.5849 48.4102 48.8798 50.7475 51.233C53.0066 53.5073 54.2845 56.5079 54.3669 59.7118C54.3711 59.8944 54.5153 60.0355 54.6967 60.0355H57.1701C57.2141 60.0355 57.2577 60.0267 57.2983 60.0095C57.3388 59.9923 57.3755 59.9671 57.4063 59.9354C57.437 59.9036 57.4611 59.8661 57.4772 59.8248C57.4933 59.7836 57.501 59.7395 57.4999 59.6952C57.4587 57.7114 57.0506 55.7816 56.2838 53.9596ZM42 44.4308C40.1079 44.4308 38.327 43.6879 36.9873 42.3391C35.6475 40.9903 34.9096 39.1974 34.9096 37.2925C34.9096 35.3875 35.6475 33.5947 36.9873 32.2458C38.327 30.897 40.1079 30.1541 42 30.1541C43.8921 30.1541 45.673 30.897 47.0127 32.2458C48.3525 33.5947 49.0904 35.3875 49.0904 37.2925C49.0904 39.1974 48.3525 40.9903 47.0127 42.3391C45.673 43.6879 43.8921 44.4308 42 44.4308Z" fill="#36363C"/> |
|||
<rect x="54" y="64" width="28" height="28" rx="14" fill="#F0445B"/> |
|||
<path d="M69.4248 73.3809C69.797 73.3809 70.1406 73.7278 71.0518 74.748C71.1512 74.8569 71.2891 74.9209 71.4365 74.9209H73.1855C73.75 74.9211 74.1641 75.3449 74.1641 75.9062V81.5527C74.1639 82.114 73.75 82.6209 73.1855 82.6211H62.9189C62.3544 82.6211 61.8439 82.1141 61.8438 81.5527V75.9062C61.8438 75.3448 62.354 74.9209 62.9219 74.9209H63.1621V74.6641C63.1622 74.523 63.2779 74.4073 63.4189 74.4072H64.2529C64.3941 74.4072 64.5097 74.523 64.5098 74.6641V74.9209H64.6416C64.7859 74.9208 64.9272 74.86 65.0234 74.751C65.9376 73.7278 66.3064 73.381 66.6816 73.3809H69.4248ZM67.875 75.8701C66.4667 75.9345 65.3278 77.0741 65.2637 78.4824C65.1901 80.096 66.5183 81.4242 68.1318 81.3506C69.5401 81.2864 70.6797 80.1475 70.7441 78.7393C70.8179 77.1255 69.4888 75.7963 67.875 75.8701ZM68.0039 76.7656C69.0227 76.7657 69.8486 77.5916 69.8486 78.6104C69.8486 79.6292 69.0227 80.455 68.0039 80.4551C66.9851 80.4551 66.1592 79.6292 66.1592 78.6104C66.1592 77.5915 66.9851 76.7656 68.0039 76.7656ZM71.084 75.9473C70.8531 75.9473 70.6672 76.1334 70.667 76.3643C70.667 76.5953 70.853 76.7812 71.084 76.7812C71.3149 76.7811 71.501 76.5952 71.501 76.3643C71.5008 76.1335 71.3148 75.9474 71.084 75.9473Z" fill="white"/> |
|||
</svg> |
|||
|
After Width: 375 | Height: 813 | Size: 427 KiB |
27
public/assets/images/Group 1597880466.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,20 @@ |
|||
<svg width="131" height="125" viewBox="0 0 131 125" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<circle cx="67.1442" cy="64.6749" r="47.1404" transform="rotate(0.232334 67.1442 64.6749)" fill="url(#paint0_radial_2285_16401)" fill-opacity="0.2"/> |
|||
<circle opacity="0.3" cx="58.9541" cy="58.9541" r="58.9541" transform="matrix(-0.999992 -0.00405498 -0.00405498 0.999992 125.922 6.0957)" stroke="#F0445B" stroke-width="2" stroke-dasharray="8 8"/> |
|||
<circle cx="67.1484" cy="64.5645" r="33.625" fill="#FF6175"/> |
|||
<path d="M67.0278 77.9618C75.1893 77.9618 81.8056 71.3456 81.8056 63.184C81.8056 55.0225 75.1893 48.4062 67.0278 48.4062C58.8662 48.4062 52.25 55.0225 52.25 63.184C52.25 71.3456 58.8662 77.9618 67.0278 77.9618Z" fill="white"/> |
|||
<path d="M67.0208 49.4627C74.5997 49.4627 80.7431 55.606 80.7431 63.1849C80.7431 70.7638 74.5997 76.9071 67.0208 76.9071C59.4419 76.9071 53.2986 70.7638 53.2986 63.1849C53.2986 55.606 59.4419 49.4627 67.0208 49.4627ZM67.0208 47.3516C58.2903 47.3516 51.1875 54.4544 51.1875 63.1849C51.1875 71.9154 58.2903 79.0182 67.0208 79.0182C75.7513 79.0182 82.8542 71.9154 82.8542 63.1849C82.8542 54.4544 75.7513 47.3516 67.0208 47.3516Z" fill="#FFDEE2"/> |
|||
<path d="M68.0816 51.5731C68.0816 51.2932 67.9704 51.0247 67.7724 50.8267C67.5745 50.6288 67.306 50.5176 67.026 50.5176C66.7461 50.5176 66.4776 50.6288 66.2797 50.8267C66.0817 51.0247 65.9705 51.2932 65.9705 51.5731V52.6287C65.9705 52.9086 66.0817 53.1771 66.2797 53.3751C66.4776 53.573 66.7461 53.6842 67.026 53.6842C67.306 53.6842 67.5745 53.573 67.7724 53.3751C67.9704 53.1771 68.0816 52.9086 68.0816 52.6287V51.5731ZM68.0816 73.7398C68.0816 73.4599 67.9704 73.1914 67.7724 72.9934C67.5745 72.7955 67.306 72.6842 67.026 72.6842C66.7461 72.6842 66.4776 72.7955 66.2797 72.9934C66.0817 73.1914 65.9705 73.4599 65.9705 73.7398V74.7954C65.9705 75.0753 66.0817 75.3438 66.2797 75.5417C66.4776 75.7397 66.7461 75.8509 67.026 75.8509C67.306 75.8509 67.5745 75.7397 67.7724 75.5417C67.9704 75.3438 68.0816 75.0753 68.0816 74.7954V73.7398ZM56.4705 62.1287H55.4149C55.135 62.1287 54.8665 62.2399 54.6685 62.4379C54.4706 62.6358 54.3594 62.9043 54.3594 63.1842C54.3594 63.4642 54.4706 63.7327 54.6685 63.9306C54.8665 64.1286 55.135 64.2398 55.4149 64.2398H56.4705C56.7504 64.2398 57.0189 64.1286 57.2169 63.9306C57.4148 63.7327 57.526 63.4642 57.526 63.1842C57.526 62.9043 57.4148 62.6358 57.2169 62.4379C57.0189 62.2399 56.7504 62.1287 56.4705 62.1287ZM78.6372 62.1287H77.5816C77.3016 62.1287 77.0332 62.2399 76.8352 62.4379C76.6373 62.6358 76.526 62.9043 76.526 63.1842C76.526 63.4642 76.6373 63.7327 76.8352 63.9306C77.0332 64.1286 77.3016 64.2398 77.5816 64.2398H78.6372C78.9171 64.2398 79.1856 64.1286 79.3835 63.9306C79.5815 63.7327 79.6927 63.4642 79.6927 63.1842C79.6927 62.9043 79.5815 62.6358 79.3835 62.4379C79.1856 62.2399 78.9171 62.1287 78.6372 62.1287ZM76.2168 70.8824L75.4705 70.1361C75.3725 70.0381 75.2561 69.9604 75.1281 69.9074C75 69.8543 74.8628 69.827 74.7242 69.827C74.5856 69.827 74.4484 69.8543 74.3203 69.9074C74.1923 69.9604 74.0759 70.0381 73.9779 70.1361C73.8799 70.2341 73.8022 70.3505 73.7492 70.4785C73.6961 70.6066 73.6688 70.7438 73.6688 70.8824C73.6688 71.021 73.6961 71.1582 73.7492 71.2863C73.8022 71.4143 73.8799 71.5307 73.9779 71.6287L74.7242 72.375C74.9221 72.5729 75.1906 72.6841 75.4705 72.6841C75.7504 72.6841 76.0188 72.5729 76.2168 72.375C76.4147 72.177 76.5259 71.9086 76.5259 71.6287C76.5259 71.3488 76.4147 71.0803 76.2168 70.8824ZM58.5816 70.1361L57.8353 70.8824C57.6374 71.0803 57.5262 71.3488 57.5262 71.6287C57.5262 71.9086 57.6374 72.177 57.8353 72.375C58.0332 72.5729 58.3017 72.6841 58.5816 72.6841C58.8615 72.6841 59.1299 72.5729 59.3279 72.375L60.0742 71.6287C60.1722 71.5307 60.2499 71.4143 60.3029 71.2863C60.356 71.1582 60.3833 71.021 60.3833 70.8824C60.3833 70.7438 60.356 70.6066 60.3029 70.4785C60.2499 70.3505 60.1722 70.2341 60.0742 70.1361C59.9762 70.0381 59.8598 69.9604 59.7318 69.9074C59.6037 69.8543 59.4665 69.827 59.3279 69.827C59.1893 69.827 59.052 69.8543 58.924 69.9074C58.7959 69.9604 58.6796 70.0381 58.5816 70.1361ZM57.8353 55.4861L58.5816 56.2324C58.6796 56.3304 58.7959 56.4081 58.924 56.4611C59.052 56.5142 59.1893 56.5415 59.3279 56.5415C59.4665 56.5415 59.6037 56.5142 59.7318 56.4611C59.8598 56.4081 59.9762 56.3304 60.0742 56.2324C60.1722 56.1344 60.2499 56.018 60.3029 55.89C60.356 55.7619 60.3833 55.6247 60.3833 55.4861C60.3833 55.3475 60.356 55.2102 60.3029 55.0822C60.2499 54.9541 60.1722 54.8378 60.0742 54.7398L59.3279 53.9935C59.1299 53.7956 58.8615 53.6844 58.5816 53.6844C58.3017 53.6844 58.0332 53.7956 57.8353 53.9935C57.6374 54.1914 57.5262 54.4599 57.5262 54.7398C57.5262 55.0197 57.6374 55.2882 57.8353 55.4861ZM75.4705 56.2324L76.2168 55.4861C76.4147 55.2882 76.5259 55.0197 76.5259 54.7398C76.5259 54.4599 76.4147 54.1914 76.2168 53.9935C76.0188 53.7956 75.7504 53.6844 75.4705 53.6844C75.1906 53.6844 74.9221 53.7956 74.7242 53.9935L73.9779 54.7398C73.8799 54.8378 73.8022 54.9541 73.7492 55.0822C73.6961 55.2102 73.6688 55.3475 73.6688 55.4861C73.6688 55.6247 73.6961 55.7619 73.7492 55.89C73.8022 56.018 73.8799 56.1344 73.9779 56.2324C74.0759 56.3304 74.1923 56.4081 74.3203 56.4611C74.4484 56.5142 74.5856 56.5415 74.7242 56.5415C74.8628 56.5415 75 56.5142 75.1281 56.4611C75.2561 56.4081 75.3725 56.3304 75.4705 56.2324Z" fill="#292F33"/> |
|||
<path d="M67.0244 68.2128L66.2279 67.4877C63.3989 64.9224 61.5312 63.225 61.5312 61.1541C61.5312 59.4567 62.8606 58.1328 64.5525 58.1328C65.5083 58.1328 66.4257 58.5778 67.0244 59.2754C67.6232 58.5778 68.5406 58.1328 69.4964 58.1328C71.1883 58.1328 72.5176 59.4567 72.5176 61.1541C72.5176 63.225 70.65 64.9224 67.821 67.4877L67.0244 68.2128Z" fill="#F0445B"/> |
|||
<rect x="110.578" y="84.0781" width="20" height="20" rx="10" fill="#FF6175"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.014 96.8593C120.453 96.8593 120.887 96.7729 121.292 96.6051C121.697 96.4373 122.065 96.1914 122.375 95.8813C122.686 95.5713 122.931 95.2032 123.099 94.7981C123.267 94.393 123.353 93.9588 123.353 93.5203C123.353 93.0818 123.267 92.6476 123.099 92.2425C122.931 91.8374 122.686 91.4694 122.375 91.1593C122.065 90.8493 121.697 90.6033 121.292 90.4355C120.887 90.2677 120.453 90.1813 120.014 90.1813C119.129 90.1813 118.28 90.5331 117.653 91.1593C117.027 91.7855 116.675 92.6348 116.675 93.5203C116.675 94.4059 117.027 95.2551 117.653 95.8813C118.28 96.5075 119.129 96.8593 120.014 96.8593ZM120.014 97.9723C121.195 97.9723 122.328 97.5032 123.162 96.6683C123.997 95.8334 124.466 94.701 124.466 93.5203C124.466 92.3396 123.997 91.2072 123.162 90.3723C122.328 89.5374 121.195 89.0684 120.014 89.0684C118.834 89.0684 117.701 89.5374 116.866 90.3723C116.032 91.2072 115.562 92.3396 115.562 93.5203C115.562 94.701 116.032 95.8334 116.866 96.6683C117.701 97.5032 118.834 97.9723 120.014 97.9723Z" fill="white"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M122.422 95.9076C122.527 95.8037 122.668 95.7456 122.816 95.7461C122.963 95.7466 123.105 95.8057 123.209 95.9104L125.421 98.1364C125.522 98.2416 125.578 98.3824 125.576 98.5283C125.574 98.6742 125.515 98.8137 125.412 98.9166C125.308 99.0195 125.169 99.0776 125.023 99.0784C124.877 99.0793 124.736 99.0228 124.632 98.9211L122.42 96.6951C122.316 96.5904 122.257 96.4487 122.258 96.3012C122.258 96.1536 122.317 96.0123 122.422 95.9082V95.9076Z" fill="white"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.53655 40.5485C4.11831 38.9795 6.25856 38.1032 8.48647 38.1122C10.7144 38.1212 12.8475 39.0149 14.4164 40.5967C15.9854 42.1784 16.8618 44.3187 16.8528 46.5466C16.8437 48.7745 15.95 50.9076 14.3683 52.4766L8.40424 58.3925L2.48837 52.4284C1.71144 51.6452 1.09638 50.7167 0.678327 49.6958C0.26027 48.6749 0.0474006 47.5817 0.0518739 46.4785C0.0563473 45.3753 0.278076 44.2838 0.704398 43.2664C1.13072 42.2489 1.75329 41.3254 2.53655 40.5485ZM8.44268 48.9125C9.07919 48.9151 9.69066 48.6647 10.1426 48.2165C10.5945 47.7682 10.8498 47.1588 10.8524 46.5223C10.855 45.8858 10.6046 45.2743 10.1563 44.8224C9.70808 44.3705 9.09866 44.1151 8.46214 44.1126C7.82563 44.11 7.21416 44.3604 6.76225 44.8086C6.31034 45.2569 6.05501 45.8663 6.05243 46.5028C6.04985 47.1393 6.30023 47.7508 6.74849 48.2027C7.19675 48.6546 7.80616 48.9099 8.44268 48.9125Z" fill="#FF6175"/> |
|||
<path d="M91.4571 15.7671L91.5186 15.8325C91.6747 15.9634 91.8639 16.0336 92.0493 16.0344C92.2676 16.0352 92.4905 15.9363 92.6478 15.7719L99.4493 7.80591C101.089 5.8977 100.944 2.92294 99.13 1.20047C97.3816 -0.456606 94.4681 -0.303353 92.8909 1.53989L92.1043 2.46154L91.3253 1.53354C89.7672 -0.322505 86.8221 -0.499402 85.0603 1.14341C84.1794 1.96482 83.672 3.11767 83.6052 4.37234C83.5053 5.62676 83.9084 6.81809 84.6874 7.74605L91.4571 15.7671Z" fill="#FF6175"/> |
|||
<defs> |
|||
<radialGradient id="paint0_radial_2285_16401" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(67.1442 64.6749) rotate(90) scale(47.1404)"> |
|||
<stop stop-color="#FF6175"/> |
|||
<stop offset="1" stop-color="#FFA2AE"/> |
|||
</radialGradient> |
|||
</defs> |
|||
</svg> |
|||
@ -0,0 +1,39 @@ |
|||
<svg width="131" height="125" viewBox="0 0 131 125" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<circle cx="67.1442" cy="64.6749" r="47.1404" transform="rotate(0.232334 67.1442 64.6749)" fill="url(#paint0_radial_2285_15369)" fill-opacity="0.2"/> |
|||
<circle opacity="0.3" cx="58.9541" cy="58.9541" r="58.9541" transform="matrix(-0.999992 -0.00405498 -0.00405498 0.999992 125.922 6.0957)" stroke="#F0445B" stroke-width="2" stroke-dasharray="8 8"/> |
|||
<circle cx="67.1484" cy="64.5645" r="33.625" fill="white"/> |
|||
<path d="M83.426 50.0096L82.6769 49.1738L73.8239 57.5663C73.5855 57.7753 73.5855 58.1235 73.7898 58.3673C73.9941 58.611 74.3346 58.611 74.573 58.4021L83.426 50.0096Z" fill="#C89968"/> |
|||
<path opacity="0.25" d="M81.6953 50.1141L82.3082 50.2882L82.4444 50.9498L83.4319 50.0096L82.6487 49.1738L81.6953 50.1141Z" fill="black"/> |
|||
<path d="M87.3451 45.5176L78.9688 48.7562L82.7143 49.9054L83.5655 53.8056L87.3451 45.5176Z" fill="#EEBD50"/> |
|||
<path d="M87.3431 45.5176L82.3036 49.2089L80.2266 48.6865L87.3431 45.5176Z" fill="url(#paint1_radial_2285_15369)"/> |
|||
<path opacity="0.25" d="M78.9688 48.7568L83.0548 49.5925L83.7698 52.5874L86.7662 46.8066L83.5655 53.8062L82.7143 49.9059L78.9688 48.7568Z" fill="black"/> |
|||
<path opacity="0.25" d="M77.6716 53.9453L73.8239 57.6018C73.5855 57.8107 73.5855 58.159 73.7898 58.4027C73.9941 58.6465 74.3346 58.6465 74.573 58.4376L78.5228 54.6766C78.2504 54.4328 77.978 54.1543 77.6716 53.9453Z" fill="black"/> |
|||
<path d="M79.1683 55.9644C75.2525 51.8552 68.783 51.8204 64.8332 55.86C60.8493 51.7856 54.4138 51.8204 50.498 55.9644C46.6504 60.004 46.7866 66.5508 50.7023 70.5207L58.4658 78.4605C61.973 82.0474 67.6934 82.0474 71.2346 78.4605L78.998 70.5207C82.8798 66.516 83.016 60.004 79.1683 55.9644Z" fill="#EB4D63"/> |
|||
<path d="M77.4043 57.8114C74.0333 54.0853 68.3129 54.0157 64.8398 57.5677C61.3667 54.0157 55.6462 54.0853 52.2753 57.8114C49.0405 61.3635 49.3129 66.9352 52.6498 70.348L59.596 77.452C62.4903 80.412 67.2233 80.412 70.1176 77.452L77.0638 70.348C80.3667 66.9352 80.6391 61.3635 77.4043 57.8114Z" fill="url(#paint2_radial_2285_15369)"/> |
|||
<path d="M52.99 57.2886C51.7301 58.577 51.2534 60.1441 50.7086 59.6217C50.1979 59.0994 50.7767 57.6368 52.0366 56.3483C53.2965 55.0598 54.7266 54.4678 55.2373 54.9902C55.7481 55.5125 54.2158 56.0001 52.99 57.2886Z" fill="#FF7786"/> |
|||
<path d="M67.5348 57.4626C66.4112 58.6118 65.9685 60.0048 65.5259 59.5172C65.0832 59.0645 65.594 57.7412 66.7176 56.5921C67.8413 55.4429 69.1011 54.9205 69.5778 55.3732C69.9864 55.8956 68.6244 56.3483 67.5348 57.4626Z" fill="#FF7786"/> |
|||
<path d="M63.8186 68.9531C63.6824 68.1521 62.9333 67.595 62.1502 67.7343C61.367 67.8736 60.8222 68.6397 60.9584 69.4406C61.0946 70.2416 61.8437 70.7987 62.6269 70.6594C63.41 70.5202 63.9548 69.754 63.8186 68.9531Z" fill="#EB4D63"/> |
|||
<path d="M55.7839 75.9195L54.4219 80.7948L50.6083 84.4165L50.7785 80.2725L46.7266 80.1332L50.5061 76.5115L55.3753 75.4668L55.7839 75.9195Z" fill="#8B5217"/> |
|||
<path opacity="0.25" d="M50.7109 80.2725H50.779L50.7109 81.8047L55.3758 77.3821L55.7844 75.9195L55.5461 75.6758L50.7109 80.2725Z" fill="black"/> |
|||
<path d="M62.2219 69.0932L57.1484 74.9784L57.9316 75.7794L63.0391 69.8245C63.2434 69.5808 63.2094 69.2325 63.0051 69.0236C62.8008 68.8495 62.4262 68.8843 62.2219 69.0932Z" fill="#EB4D63"/> |
|||
<path d="M62.7333 68.8827C62.529 68.639 62.1885 68.639 61.9502 68.8479L49.9645 80.2353C49.7262 80.4442 49.7262 80.7924 49.9305 81.0362C50.1348 81.28 50.4753 81.28 50.7136 81.071L62.6993 69.6837C62.9376 69.4747 62.9376 69.1265 62.7333 68.8827Z" fill="#C89968"/> |
|||
<rect x="110.578" y="84.0781" width="20" height="20" rx="10" fill="#FF6175"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.014 96.8593C120.453 96.8593 120.887 96.7729 121.292 96.6051C121.697 96.4373 122.065 96.1914 122.375 95.8813C122.686 95.5713 122.931 95.2032 123.099 94.7981C123.267 94.393 123.353 93.9588 123.353 93.5203C123.353 93.0818 123.267 92.6476 123.099 92.2425C122.931 91.8374 122.686 91.4694 122.375 91.1593C122.065 90.8493 121.697 90.6033 121.292 90.4355C120.887 90.2677 120.453 90.1813 120.014 90.1813C119.129 90.1813 118.28 90.5331 117.653 91.1593C117.027 91.7855 116.675 92.6348 116.675 93.5203C116.675 94.4059 117.027 95.2551 117.653 95.8813C118.28 96.5075 119.129 96.8593 120.014 96.8593ZM120.014 97.9723C121.195 97.9723 122.328 97.5032 123.162 96.6683C123.997 95.8334 124.466 94.701 124.466 93.5203C124.466 92.3396 123.997 91.2072 123.162 90.3723C122.328 89.5374 121.195 89.0684 120.014 89.0684C118.834 89.0684 117.701 89.5374 116.866 90.3723C116.032 91.2072 115.562 92.3396 115.562 93.5203C115.562 94.701 116.032 95.8334 116.866 96.6683C117.701 97.5032 118.834 97.9723 120.014 97.9723Z" fill="white"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M122.422 95.9076C122.527 95.8037 122.668 95.7456 122.816 95.7461C122.963 95.7466 123.105 95.8057 123.209 95.9104L125.421 98.1364C125.522 98.2416 125.578 98.3824 125.576 98.5283C125.574 98.6742 125.515 98.8137 125.412 98.9166C125.308 99.0195 125.169 99.0776 125.023 99.0784C124.877 99.0793 124.736 99.0228 124.632 98.9211L122.42 96.6951C122.316 96.5904 122.257 96.4487 122.258 96.3012C122.258 96.1536 122.317 96.0123 122.422 95.9082V95.9076Z" fill="white"/> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.52873 40.5485C4.11049 38.9795 6.25075 38.1032 8.47866 38.1122C10.7066 38.1212 12.8396 39.0149 14.4086 40.5967C15.9776 42.1784 16.854 44.3187 16.845 46.5466C16.8359 48.7745 15.9422 50.9076 14.3605 52.4766L8.39642 58.3925L2.48056 52.4284C1.70363 51.6452 1.08857 50.7167 0.670514 49.6958C0.252457 48.6749 0.0395881 47.5817 0.0440614 46.4785C0.0485348 45.3753 0.270263 44.2838 0.696586 43.2664C1.12291 42.2489 1.74547 41.3254 2.52873 40.5485ZM8.43487 48.9125C9.07138 48.9151 9.68285 48.6647 10.1348 48.2165C10.5867 47.7682 10.842 47.1588 10.8446 46.5223C10.8472 45.8858 10.5968 45.2743 10.1485 44.8224C9.70026 44.3705 9.09084 44.1151 8.45433 44.1126C7.81782 44.11 7.20635 44.3604 6.75444 44.8086C6.30253 45.2569 6.0472 45.8663 6.04462 46.5028C6.04204 47.1393 6.29242 47.7508 6.74067 48.2027C7.18893 48.6546 7.79835 48.9099 8.43487 48.9125Z" fill="#FF6175"/> |
|||
<path d="M91.4571 15.7671L91.5186 15.8325C91.6747 15.9634 91.8639 16.0336 92.0493 16.0344C92.2676 16.0352 92.4905 15.9363 92.6478 15.7719L99.4493 7.80591C101.089 5.8977 100.944 2.92294 99.13 1.20047C97.3816 -0.456606 94.4681 -0.303353 92.8909 1.53989L92.1043 2.46154L91.3253 1.53354C89.7672 -0.322505 86.8221 -0.499402 85.0603 1.14341C84.1794 1.96482 83.672 3.11767 83.6052 4.37234C83.5053 5.62676 83.9084 6.81809 84.6874 7.74605L91.4571 15.7671Z" fill="#FF6175"/> |
|||
<defs> |
|||
<radialGradient id="paint0_radial_2285_15369" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(67.1442 64.6749) rotate(90) scale(47.1404)"> |
|||
<stop stop-color="#FF6175"/> |
|||
<stop offset="1" stop-color="#FFA2AE"/> |
|||
</radialGradient> |
|||
<radialGradient id="paint1_radial_2285_15369" cx="0" cy="0" r="1" gradientTransform="matrix(3.90856 -2.93589 1.93561 2.69483 84.5679 47.6044)" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#F8E6B0"/> |
|||
<stop offset="1" stop-color="#EEBD50" stop-opacity="0"/> |
|||
</radialGradient> |
|||
<radialGradient id="paint2_radial_2285_15369" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(64.8345 79.2722) scale(26.0254 26.6166)"> |
|||
<stop stop-color="#E1737F"/> |
|||
<stop offset="1" stop-color="#CD454D" stop-opacity="0"/> |
|||
</radialGradient> |
|||
</defs> |
|||
</svg> |
|||
21
public/assets/images/Group 159788fd0467.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,3 @@ |
|||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.44429 7.24086C5.49445 6.19103 6.91857 5.60126 8.40349 5.60126C9.88841 5.60126 11.3125 6.19103 12.3627 7.24086L14.0035 8.88026L15.6443 7.24086C16.1609 6.70601 16.7788 6.27939 17.462 5.9859C18.1452 5.69241 18.8801 5.53792 19.6236 5.53146C20.3672 5.525 21.1046 5.66669 21.7928 5.94826C22.4811 6.22984 23.1063 6.64565 23.6321 7.17145C24.1579 7.69725 24.5737 8.3225 24.8553 9.01072C25.1369 9.69894 25.2786 10.4363 25.2721 11.1799C25.2656 11.9235 25.1112 12.6583 24.8177 13.3415C24.5242 14.0248 24.0975 14.6427 23.5627 15.1593L14.0035 24.7199L4.44429 15.1593C3.39445 14.1091 2.80469 12.685 2.80469 11.2001C2.80469 9.71515 3.39445 8.29102 4.44429 7.24086Z" fill="white"/> |
|||
</svg> |
|||
@ -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="M17.2006 0C21.2677 0 24 2.85374 24 7.10007V16.8999C24 21.1463 21.2677 24 17.1994 24H6.79942C2.73227 24 0 21.1463 0 16.8999V7.10007C0 2.85374 2.73227 0 6.79942 0H17.2006ZM18.5239 12.6601C17.2376 11.8576 16.2445 12.9845 15.9766 13.3449C15.7184 13.6929 15.4963 14.0768 15.2622 14.4607C14.6903 15.4081 14.035 16.5003 12.9007 17.1356C11.2524 18.0483 10.001 17.2074 9.10085 16.5957C8.76298 16.3678 8.43476 16.1523 8.10775 16.0087C7.30167 15.6607 6.57645 16.057 5.50008 17.4241C4.93535 18.1387 4.37545 18.8471 3.8083 19.5529C3.46922 19.9752 3.55007 20.6266 4.0074 20.909C4.73745 21.3585 5.62799 21.6 6.63437 21.6H16.7477C17.3184 21.6 17.8904 21.522 18.4358 21.3437C19.6642 20.9424 20.6392 20.0235 21.1485 18.8099C21.5781 17.7895 21.7868 16.5511 21.385 15.5207C21.251 15.1789 21.0507 14.8607 20.7696 14.5808C20.0323 13.8489 19.3433 13.1653 18.5239 12.6601ZM7.79863 4.8C6.14426 4.8 4.8 6.14608 4.8 7.8C4.8 9.45392 6.14426 10.8 7.79863 10.8C9.4518 10.8 10.7973 9.45392 10.7973 7.8C10.7973 6.14608 9.4518 4.8 7.79863 4.8Z" fill="#FF6175"/> |
|||
</svg> |
|||
@ -0,0 +1,9 @@ |
|||
<svg width="375" height="813" viewBox="0 0 375 813" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path opacity="0.05" d="M195.036 383.227L378.37 -83.6084L542.939 11.4053L212.103 382.146L590.942 79.875L685.956 244.444L247.057 386.959L701.422 315.218V505.246L245.484 411.986L685.956 555.013L590.942 719.582L210.432 415.979L542.939 788.595L378.37 883.608L195.037 416.775L294.323 916H104.295L182.749 415.845L-0.948242 883.608L-165.518 788.595L169.082 413.633L-214.368 719.582L-309.382 555.013L129.643 412.455L-324 505.246V315.218L146.239 389.466L-309.382 241.521L-214.368 76.9521L158.347 374.335L-165.518 11.4053L-0.948242 -83.6084L182.748 384.151L104.295 -116H294.323L195.036 383.227Z" fill="url(#paint0_radial_2285_15319)"/> |
|||
<defs> |
|||
<radialGradient id="paint0_radial_2285_15319" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(189 400) rotate(90) scale(461.5 458.558)"> |
|||
<stop stop-color="#E9495F"/> |
|||
<stop offset="1" stop-color="#E9495F" stop-opacity="0"/> |
|||
</radialGradient> |
|||
</defs> |
|||
</svg> |
|||
@ -0,0 +1,3 @@ |
|||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 18C0 13.2261 1.89642 8.64773 5.27208 5.27208C8.64773 1.89642 13.2261 0 18 0C22.7739 0 27.3523 1.89642 30.7279 5.27208C34.1036 8.64773 36 13.2261 36 18C36 22.7739 34.1036 27.3523 30.7279 30.7279C27.3523 34.1036 22.7739 36 18 36C13.2261 36 8.64773 34.1036 5.27208 30.7279C1.89642 27.3523 0 22.7739 0 18ZM16.9728 25.704L27.336 12.7488L25.464 11.2512L16.6272 22.2936L10.368 17.0784L8.832 18.9216L16.9728 25.704Z" fill="#21C17D"/> |
|||
</svg> |
|||
@ -0,0 +1,11 @@ |
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_2098_12986)"> |
|||
<path d="M13.9549 7.41949C15.1208 7.41949 16.066 6.47431 16.066 5.30838C16.066 4.14244 15.1208 3.19727 13.9549 3.19727C12.7889 3.19727 11.8438 4.14244 11.8438 5.30838C11.8438 6.47431 12.7889 7.41949 13.9549 7.41949Z" fill="white"/> |
|||
<path d="M16.1076 15.9843C16.0493 15.9451 15.9837 15.9179 15.9147 15.9043C15.8457 15.8908 15.7748 15.8912 15.7059 15.9054C15.6371 15.9197 15.5718 15.9475 15.5139 15.9874C15.456 16.0272 15.4066 16.0782 15.3687 16.1374C14.8585 16.893 14.2335 17.5643 13.5162 18.1271C13.342 18.259 12.6929 18.7604 12.4184 18.6549C12.2284 18.5968 12.3392 18.2221 12.3762 18.0638L12.6559 17.2351C12.772 16.8974 14.7934 10.9018 15.0151 10.2157C15.3423 9.21292 15.1998 8.22597 13.7062 8.46347C13.2998 8.5057 9.17785 9.03875 9.10396 9.04403C9.03465 9.04854 8.96691 9.06665 8.9046 9.09733C8.84229 9.12802 8.78664 9.17068 8.74081 9.22287C8.69499 9.27506 8.6599 9.33577 8.63754 9.40153C8.61517 9.46729 8.60598 9.53681 8.61049 9.60611C8.61499 9.67542 8.63311 9.74317 8.66379 9.80547C8.69448 9.86778 8.73713 9.92344 8.78933 9.96926C8.84152 10.0151 8.90223 10.0502 8.96799 10.0725C9.03375 10.0949 9.10326 10.1041 9.17257 10.0996C9.17257 10.0996 10.7559 9.89375 10.9301 9.87792C11.0193 9.86921 11.1091 9.8865 11.1887 9.92771C11.2683 9.96891 11.3343 10.0323 11.3787 10.1101C11.4743 10.4058 11.4612 10.7258 11.3417 11.0126C11.2045 11.5404 9.03535 17.6521 8.96674 18.0057C8.89315 18.3016 8.91358 18.6131 9.02519 18.8969C9.13681 19.1806 9.33405 19.4226 9.58952 19.589C10.069 19.9086 10.6399 20.0625 11.2151 20.0271C11.7743 20.0206 12.3274 19.9096 12.8459 19.6999C14.1601 19.1721 15.5323 17.7682 16.2554 16.6651C16.3176 16.5531 16.337 16.4222 16.3098 16.2969C16.2826 16.1716 16.2107 16.0605 16.1076 15.9843Z" fill="white"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_2098_12986"> |
|||
<rect width="19" height="19" fill="white" transform="translate(3 2.14258)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
|||
@ -0,0 +1,6 @@ |
|||
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<circle cx="42.5" cy="42.5" r="41.5" fill="#EBEBEB"/> |
|||
<path d="M16.2735 69.3315C17.753 68.6628 19.2965 68.165 20.8186 67.6173C23.1018 66.7922 25.4846 66.4508 27.8816 66.1521C28.1661 66.1165 28.4506 66.1023 28.7209 66.0168C29.0908 65.9031 29.3895 65.7395 29.5673 65.2986C30.0794 63.9827 30.5489 62.6597 30.9828 61.3225C31.1961 60.6609 31.8718 60.4761 32.384 60.9811C32.6045 61.2017 32.8321 61.1519 33.0455 61.1162C33.3442 61.0735 33.1878 60.7961 33.2091 60.6255C33.2731 60.1702 33.3157 59.7079 33.3798 59.1033C35.855 61.7777 38.8922 63.1433 42.4272 63.1433C45.9765 63.1504 49.0421 61.8204 51.5316 59.082C51.6027 59.7079 51.6454 60.1987 51.7094 60.6895C51.7307 60.8459 51.6027 61.0878 51.873 61.1162C52.0793 61.1376 52.2927 61.1874 52.4989 61.0238C53.2244 60.4404 53.6797 60.5969 53.9642 61.4434C54.4194 62.7734 54.9244 64.0894 55.4009 65.4124C55.5717 65.8818 55.97 65.9742 56.3896 66.024C58.616 66.2943 60.8422 66.6145 62.9832 67.2474C64.9036 67.8164 66.8027 68.5063 68.6378 69.3245C68.6094 69.8293 68.1968 70.0713 67.8768 70.3486C65.8496 72.1196 63.7015 73.72 61.3615 75.05C57.3214 77.3333 53.0111 78.8555 48.4233 79.6094C47.3635 79.7872 46.2824 79.7231 45.2297 79.894C45.0945 79.894 44.9665 79.894 44.8313 79.894C44.7531 79.8086 44.6464 79.8298 44.5468 79.8228C44.4686 79.8228 44.3975 79.8228 44.3192 79.8228C44.2196 79.8298 44.1129 79.8015 44.0347 79.894C43.8142 79.894 43.5938 79.894 43.3732 79.894C43.1527 79.7872 42.9322 79.7872 42.7117 79.894C42.4912 79.894 42.2708 79.894 42.0502 79.894C41.7871 79.7872 41.5239 79.7872 41.2607 79.894C41.0829 79.894 40.9051 79.894 40.7344 79.894C40.6561 79.8086 40.5494 79.8298 40.4499 79.8228C40.3717 79.8228 40.3005 79.8228 40.2222 79.8228C40.1227 79.8298 40.016 79.8015 39.9378 79.894C39.7172 79.894 39.4968 79.894 39.2763 79.894C38.2165 79.709 37.1424 79.7445 36.0826 79.5596C29.0481 78.322 22.7888 75.4485 17.3902 70.7469C17.1057 70.4979 16.7927 70.2917 16.5438 70.0143C16.3802 69.8223 16.181 69.6373 16.2735 69.3315Z" fill="#36363C"/> |
|||
<path d="M42.1159 61.6344C39.9963 61.5491 37.6633 61.0796 35.5721 59.7495C34.2563 58.9173 33.4454 57.6512 32.6488 56.3567C31.7028 54.8133 31.5961 53.1062 31.5108 51.3777C31.4894 51.0008 31.4538 50.816 30.9986 50.7803C29.6258 50.6665 29.2062 49.4858 28.7509 48.49C27.8192 46.4771 27.4991 44.2935 27.3142 42.1028C27.2573 41.4555 27.3355 40.7655 27.6272 40.1182C27.7836 39.7769 28.0255 39.5208 28.3029 39.4923C28.7225 39.4497 28.7509 39.2647 28.7225 38.9517C28.7083 38.8024 28.7225 38.6459 28.7225 38.4894C28.7368 37.5221 29 37.1593 29.8676 36.8322C31.5534 36.1921 33.3387 36.0569 35.0956 35.751C36.5181 35.5021 37.9549 35.4096 39.3917 35.2815C39.9038 35.2389 40.4017 35.3456 40.8996 35.3456C42.244 35.3527 43.5811 35.3243 44.9254 35.2958C46.2982 35.2744 47.6781 35.3882 49.0366 35.6231C50.4307 35.8577 51.8391 36.0497 53.2332 36.2917C53.916 36.4125 54.549 36.7112 55.2034 36.9175C56.121 37.2021 56.1921 37.8991 56.1921 38.6744C56.1921 39.0513 56.057 39.4355 56.7185 39.5422C57.3017 39.6416 57.4226 40.3174 57.5151 40.8509C57.6857 41.8965 57.6004 42.9421 57.387 43.9805C57.0741 45.4812 56.8465 47.0035 56.2135 48.4261C55.872 49.1941 55.3812 49.8556 54.8478 50.4816C54.5988 50.7732 54.2005 50.7591 53.8591 50.7732C53.4751 50.7945 53.4252 50.951 53.4039 51.2925C53.3114 52.594 53.3186 53.9029 52.8136 55.1618C51.583 58.2203 49.4705 60.2759 46.2484 61.0938C45.0037 61.4281 43.7305 61.6487 42.1159 61.6344Z" fill="white"/> |
|||
<path d="M56.4562 29.1861C56.4847 31.2558 56.4207 33.3186 56.5842 35.3884C56.6554 36.2989 56.6127 36.2917 55.7734 36.0215C54.5785 35.6302 53.3622 35.3172 52.1174 35.1252C51.8187 35.2248 51.5911 34.9972 51.3208 34.9687C50.3748 34.6771 49.3861 34.6629 48.4188 34.5918C44.4569 34.3144 40.488 34.3144 36.5262 34.5918C35.5588 34.6629 34.5702 34.6771 33.6241 34.9687C33.3539 34.9901 33.1263 35.2248 32.8275 35.1252C31.533 35.3102 30.2811 35.6516 29.0436 36.057C28.5386 36.2206 28.2682 36.2562 28.3394 35.5876C28.5457 33.5675 28.3607 31.5333 28.5314 29.5204C28.6737 27.7777 29.2996 26.156 30.267 24.6766C31.91 22.1587 34.2928 20.6792 37.017 19.5838C39.0085 18.7801 41.0641 18.524 43.1625 18.6094C46.9749 18.7659 50.3108 20.1671 53.1132 22.7632C54.7491 24.2783 55.7378 26.1844 56.2214 28.3609C56.2357 28.4178 56.2784 28.4676 56.314 28.5246C56.5985 28.6739 56.2997 28.9798 56.4562 29.1861Z" fill="#36363C"/> |
|||
</svg> |
|||
@ -0,0 +1,33 @@ |
|||
<svg width="85" height="85" viewBox="0 0 85 85" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<circle cx="42.5" cy="42.5" r="37.5" fill="#FF6175"/> |
|||
<circle cx="42.5" cy="42.5" r="41.5" stroke="#FF6175" stroke-width="2"/> |
|||
<g transform="translate(5 5)"> |
|||
<path d="M37.5 21C44.9558 21 51 29.7304 51 40.5C51 51.2696 44.9558 60 37.5 60C30.0442 60 24 51.2696 24 40.5C24 29.7304 30.0442 21 37.5 21Z" fill="white"/> |
|||
<path d="M40.2998 74.4775H39.7739C39.7192 74.417 39.6479 74.4121 39.5852 74.4121C39.5727 74.4121 39.5601 74.4121 39.5476 74.4126C39.5413 74.4126 39.5351 74.4126 39.5288 74.4131C39.5226 74.4131 39.5164 74.4131 39.5103 74.4131C39.5042 74.4131 39.498 74.4131 39.4919 74.4131C39.4858 74.4126 39.4797 74.4126 39.4734 74.4126C39.461 74.4121 39.4485 74.4121 39.436 74.4121C39.3736 74.4121 39.3025 74.417 39.2479 74.4775H38.5865C38.4753 74.4199 38.3621 74.3911 38.2497 74.3911C38.1423 74.3911 38.033 74.4175 37.925 74.4702H37.2635C37.1317 74.417 36.9988 74.3901 36.8683 74.3901C36.738 74.3901 36.6052 74.417 36.4739 74.4702H35.9467C35.8982 74.416 35.8372 74.4048 35.7603 74.4048C35.7475 74.4048 35.7347 74.4053 35.722 74.4058C35.7156 74.4058 35.7093 74.4058 35.7029 74.4062C35.6967 74.4062 35.6905 74.4062 35.6843 74.4062C35.678 74.4062 35.6717 74.4062 35.6654 74.4062C35.659 74.4058 35.6526 74.4058 35.6462 74.4058C35.6334 74.4053 35.6206 74.4048 35.6078 74.4048C35.5311 74.4048 35.47 74.416 35.4207 74.4702H34.6311C34.1976 74.3828 33.7518 74.3677 33.3208 74.3525L33.3119 74.3521L33.2986 74.3516C33.0306 74.3423 32.7536 74.333 32.4835 74.3057C29.2732 73.9849 25.9413 73.0684 22.2971 71.5039C19.2582 70.1992 16.3987 68.479 13.7975 66.3901C13.5347 66.1802 13.2675 65.9409 13.0938 65.6152V65.4795C13.4128 64.5752 13.7449 63.6509 14.038 62.835C14.4932 61.5674 14.9639 60.2568 15.413 58.957C16.9163 54.6016 18.1444 50.6436 19.1677 46.8579C19.7749 44.6094 20.337 42.3081 20.8806 40.082L20.8826 40.0737C21.0662 39.3223 21.2742 38.4697 21.4796 37.6401C21.8463 36.1528 22.1093 34.6484 22.3685 33.1089C22.77 30.7632 23.1862 28.8374 23.678 27.0483C24.2259 25.0649 25.2548 23.1919 26.7361 21.48C27.8722 20.1733 29.4453 18.5435 31.5439 17.5249C32.6957 16.9658 33.9606 16.4482 35.405 16.4482C35.4628 16.4482 35.5209 16.4492 35.5779 16.4507C35.6622 16.4526 35.7425 16.4541 35.8167 16.4541C36.315 16.4541 36.8038 16.4121 37.2696 16.3301H37.6687C38.0451 16.4351 38.4324 16.4492 38.7616 16.4492C38.8594 16.4492 38.9584 16.4478 39.0543 16.4463L39.1032 16.4453H39.1066C39.1466 16.4448 39.1864 16.4438 39.2261 16.4434H39.2281C39.2655 16.4429 39.3028 16.4429 39.3399 16.4429C40.5859 16.4429 41.7734 16.7041 42.9705 17.2422C43.9714 17.6924 44.994 18.3477 46.0969 19.2466C47.4865 20.375 48.7036 21.7686 49.8176 23.5063C50.5447 24.6411 51.0937 25.9248 51.4961 27.4326C51.9803 29.2725 52.3304 31.1729 52.669 33.0103C52.8124 33.7881 52.9606 34.5923 53.1176 35.3848C53.6954 38.2866 54.437 41.2012 55.1541 44.0195L55.2355 44.3394C55.3144 44.6499 55.3933 44.9604 55.4719 45.2715C56.3969 48.9307 57.5742 52.8477 59.177 57.5977C59.3279 58.043 59.4792 58.4961 59.6255 58.9346L59.632 58.9541L59.6331 58.9575C60.2858 60.9141 60.9607 62.9365 61.8239 64.853C62.1686 65.6201 62.1238 65.8135 61.4818 66.3335C58.8862 68.4155 56.0582 70.1377 53.0764 71.4526C50.131 72.751 46.9549 73.687 43.6362 74.2354C43.0471 74.3311 42.4382 74.353 41.8494 74.374L41.8424 74.3745C41.3356 74.3926 40.8115 74.4116 40.2998 74.4775ZM37.5192 25.4375C34.4383 25.438 31.6838 26.7915 29.3323 29.4609C27.081 32.0186 25.9434 34.8364 25.8546 38.0742C25.7849 40.6221 25.822 42.9087 25.9683 45.0654C26.1438 47.6514 27.0604 49.9351 28.6926 51.8516C29.5684 52.873 30.5563 53.8076 31.5115 54.7114L31.5158 54.7153C31.8421 55.0239 32.2119 55.3735 32.5682 55.7202C33.8771 57.0005 35.371 57.6709 37.1354 57.7695C37.3121 57.7788 37.4739 57.7832 37.6299 57.7832C39.1785 57.7832 40.4744 57.3765 41.4813 56.5737C43.6888 54.8198 45.4221 53.1094 46.7801 51.3462C47.6002 50.2876 48.1961 49.1343 48.5515 47.918C49.0211 46.3237 49.2266 44.5654 49.1985 42.3848C49.384 40.2437 49.2404 38.3672 49.0486 36.3096C48.9515 35.2505 48.681 34.2422 48.2662 33.3931C47.1063 31.0464 45.8545 29.3604 44.3266 28.0869C42.2653 26.3672 40.1317 25.5015 37.8041 25.4414C37.7155 25.4385 37.6224 25.4375 37.5192 25.4375Z" fill="#36363C"/> |
|||
<path d="M36.4766 74.4709C36.7397 74.2718 37.0029 74.2789 37.2661 74.4709C37.0029 74.4709 36.7397 74.4709 36.4766 74.4709Z" fill="#FF6175"/> |
|||
<path d="M36.4766 74.4709C36.7397 74.2718 37.0029 74.2789 37.2661 74.4709C37.0029 74.4709 36.7397 74.4709 36.4766 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M36.4766 74.4709C36.7397 74.2718 37.0029 74.2789 37.2661 74.4709C37.0029 74.4709 36.7397 74.4709 36.4766 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M36.4766 74.4709C36.7397 74.2718 37.0029 74.2789 37.2661 74.4709C37.0029 74.4709 36.7397 74.4709 36.4766 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M36.4766 74.4709C36.7397 74.2718 37.0029 74.2789 37.2661 74.4709C37.0029 74.4709 36.7397 74.4709 36.4766 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.4141 74.4685C35.4565 74.3262 35.5632 74.3477 35.6699 74.3618C35.7271 74.3831 35.7412 74.4046 35.7271 74.433C35.7126 74.4614 35.6914 74.4756 35.6772 74.4756C35.5918 74.4685 35.5063 74.4685 35.4141 74.4685Z" fill="#FF6175"/> |
|||
<path d="M35.4141 74.4685C35.4565 74.3262 35.5632 74.3477 35.6699 74.3618C35.7271 74.3831 35.7412 74.4046 35.7271 74.433C35.7126 74.4614 35.6914 74.4756 35.6772 74.4756C35.5918 74.4685 35.5063 74.4685 35.4141 74.4685Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.4141 74.4685C35.4565 74.3262 35.5632 74.3477 35.6699 74.3618C35.7271 74.3831 35.7412 74.4046 35.7271 74.433C35.7126 74.4614 35.6914 74.4756 35.6772 74.4756C35.5918 74.4685 35.5063 74.4685 35.4141 74.4685Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.4141 74.4685C35.4565 74.3262 35.5632 74.3477 35.6699 74.3618C35.7271 74.3831 35.7412 74.4046 35.7271 74.433C35.7126 74.4614 35.6914 74.4756 35.6772 74.4756C35.5918 74.4685 35.5063 74.4685 35.4141 74.4685Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.4141 74.4685C35.4565 74.3262 35.5632 74.3477 35.6699 74.3618C35.7271 74.3831 35.7412 74.4046 35.7271 74.433C35.7126 74.4614 35.6914 74.4756 35.6772 74.4756C35.5918 74.4685 35.5063 74.4685 35.4141 74.4685Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.679 74.4697C35.679 74.434 35.6719 74.3985 35.6719 74.363C35.7856 74.3415 35.8853 74.3486 35.9421 74.4697C35.8567 74.4697 35.7715 74.4697 35.679 74.4697Z" fill="#FF6175"/> |
|||
<path d="M35.679 74.4697C35.679 74.434 35.6719 74.3985 35.6719 74.363C35.7856 74.3415 35.8853 74.3486 35.9421 74.4697C35.8567 74.4697 35.7715 74.4697 35.679 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.679 74.4697C35.679 74.434 35.6719 74.3985 35.6719 74.363C35.7856 74.3415 35.8853 74.3486 35.9421 74.4697C35.8567 74.4697 35.7715 74.4697 35.679 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.679 74.4697C35.679 74.434 35.6719 74.3985 35.6719 74.363C35.7856 74.3415 35.8853 74.3486 35.9421 74.4697C35.8567 74.4697 35.7715 74.4697 35.679 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M35.679 74.4697C35.679 74.434 35.6719 74.3985 35.6719 74.363C35.7856 74.3415 35.8853 74.3486 35.9421 74.4697C35.8567 74.4697 35.7715 74.4697 35.679 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.25 74.4697C39.3069 74.3486 39.4065 74.3415 39.5203 74.363C39.563 74.3913 39.5771 74.4128 39.563 74.4411C39.5486 74.4625 39.5344 74.4768 39.5132 74.4768C39.4277 74.4697 39.3354 74.4697 39.25 74.4697Z" fill="#FF6175"/> |
|||
<path d="M39.25 74.4697C39.3069 74.3486 39.4065 74.3415 39.5203 74.363C39.563 74.3913 39.5771 74.4128 39.563 74.4411C39.5486 74.4625 39.5344 74.4768 39.5132 74.4768C39.4277 74.4697 39.3354 74.4697 39.25 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.25 74.4697C39.3069 74.3486 39.4065 74.3415 39.5203 74.363C39.563 74.3913 39.5771 74.4128 39.563 74.4411C39.5486 74.4625 39.5344 74.4768 39.5132 74.4768C39.4277 74.4697 39.3354 74.4697 39.25 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.25 74.4697C39.3069 74.3486 39.4065 74.3415 39.5203 74.363C39.563 74.3913 39.5771 74.4128 39.563 74.4411C39.5486 74.4625 39.5344 74.4768 39.5132 74.4768C39.4277 74.4697 39.3354 74.4697 39.25 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.25 74.4697C39.3069 74.3486 39.4065 74.3415 39.5203 74.363C39.563 74.3913 39.5771 74.4128 39.563 74.4411C39.5486 74.4625 39.5344 74.4768 39.5132 74.4768C39.4277 74.4697 39.3354 74.4697 39.25 74.4697Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.5078 74.4709C39.5078 74.4355 39.5078 74.3998 39.5149 74.3642C39.6216 74.35 39.7283 74.3359 39.771 74.4709C39.6855 74.4709 39.6001 74.4709 39.5078 74.4709Z" fill="#FF6175"/> |
|||
<path d="M39.5078 74.4709C39.5078 74.4355 39.5078 74.3998 39.5149 74.3642C39.6216 74.35 39.7283 74.3359 39.771 74.4709C39.6855 74.4709 39.6001 74.4709 39.5078 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.5078 74.4709C39.5078 74.4355 39.5078 74.3998 39.5149 74.3642C39.6216 74.35 39.7283 74.3359 39.771 74.4709C39.6855 74.4709 39.6001 74.4709 39.5078 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.5078 74.4709C39.5078 74.4355 39.5078 74.3998 39.5149 74.3642C39.6216 74.35 39.7283 74.3359 39.771 74.4709C39.6855 74.4709 39.6001 74.4709 39.5078 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
<path d="M39.5078 74.4709C39.5078 74.4355 39.5078 74.3998 39.5149 74.3642C39.6216 74.35 39.7283 74.3359 39.771 74.4709C39.6855 74.4709 39.6001 74.4709 39.5078 74.4709Z" fill="black" fill-opacity="0.2"/> |
|||
</g> |
|||
</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="M8 4C6.93913 4 5.92172 4.42143 5.17157 5.17157C4.42143 5.92172 4 6.93913 4 8V16C4 17.0609 4.42143 18.0783 5.17157 18.8284C5.92172 19.5786 6.93913 20 8 20H15C15.2652 20 15.5196 19.8946 15.7071 19.7071C15.8946 19.5196 16 19.2652 16 19C16 18.7348 15.8946 18.4804 15.7071 18.2929C15.5196 18.1054 15.2652 18 15 18H8C7.46957 18 6.96086 17.7893 6.58579 17.4142C6.21071 17.0391 6 16.5304 6 16V8C6 7.46957 6.21071 6.96086 6.58579 6.58579C6.96086 6.21071 7.46957 6 8 6H15C15.2652 6 15.5196 5.89464 15.7071 5.70711C15.8946 5.51957 16 5.26522 16 5C16 4.73478 15.8946 4.48043 15.7071 4.29289C15.5196 4.10536 15.2652 4 15 4H8ZM17.708 7.292C17.615 7.19902 17.5046 7.12527 17.3832 7.07495C17.2617 7.02464 17.1315 6.99874 17 6.99874C16.8685 6.99874 16.7383 7.02464 16.6168 7.07495C16.4954 7.12527 16.385 7.19902 16.292 7.292C16.199 7.38498 16.1253 7.49535 16.075 7.61683C16.0246 7.73831 15.9987 7.86851 15.9987 8C15.9987 8.13149 16.0246 8.26169 16.075 8.38317C16.1253 8.50465 16.199 8.61502 16.292 8.708L18.586 11H10C9.73478 11 9.48043 11.1054 9.29289 11.2929C9.10536 11.4804 9 11.7348 9 12C9 12.2652 9.10536 12.5196 9.29289 12.7071C9.48043 12.8946 9.73478 13 10 13H18.586L16.292 15.292C16.199 15.385 16.1253 15.4954 16.075 15.6168C16.0246 15.7383 15.9987 15.8685 15.9987 16C15.9987 16.1315 16.0246 16.2617 16.075 16.3832C16.1253 16.5046 16.199 16.615 16.292 16.708C16.385 16.801 16.4954 16.8747 16.6168 16.925C16.7383 16.9754 16.8685 17.0013 17 17.0013C17.1315 17.0013 17.2617 16.9754 17.3832 16.925C17.5046 16.8747 17.615 16.801 17.708 16.708L21.708 12.708C21.8011 12.6151 21.875 12.5048 21.9254 12.3833C21.9758 12.2618 22.0018 12.1315 22.0018 12C22.0018 11.8685 21.9758 11.7382 21.9254 11.6167C21.875 11.4952 21.8011 11.3849 21.708 11.292L17.708 7.292Z" fill="white"/> |
|||
</svg> |
|||
@ -0,0 +1,12 @@ |
|||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<g clip-path="url(#clip0_2493_9334)"> |
|||
<path d="M5.40748 2.52134L18.7714 2.57553L22.5624 9.24096L12.0124 21.5483L1.56254 9.1558L5.40748 2.52134Z" stroke="#111111" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|||
<path d="M5.40748 2.52134L12.0124 21.5483L18.7714 2.57553M1.56254 9.1558L22.5624 9.24096" stroke="#111111" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|||
<path d="M7.76567 9.18119L12.0876 2.54867L16.3561 9.21603" stroke="#111111" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id="clip0_2493_9334"> |
|||
<rect width="24" height="24" fill="white" transform="translate(0.0973196) rotate(0.232334)"/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
|||
@ -1,15 +1,15 @@ |
|||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<mask id="mask0_2035_16285" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="2" width="32" height="34"> |
|||
<mask id="mask0_2444_11588" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="2" width="32" height="34"> |
|||
<path d="M18.7941 3.13281L22.9078 6.13372L28.0004 6.12432L29.5643 10.9703L33.6898 13.9555L32.1071 18.7952L33.6898 23.6348L29.5643 26.6201L28.0004 31.466L22.9078 31.4566L18.7941 34.4575L14.6804 31.4566L9.58779 31.466L8.0239 26.6201L3.89844 23.6348L5.48112 18.7952L3.89844 13.9555L8.0239 10.9703L9.58779 6.12432L14.6804 6.13372L18.7941 3.13281Z" fill="white" stroke="white" stroke-width="1.71553" stroke-linecap="round" stroke-linejoin="round"/> |
|||
<path d="M13.3125 18.7945L17.2281 22.7101L25.0593 14.8789" stroke="black" stroke-width="1.71553" stroke-linecap="round" stroke-linejoin="round"/> |
|||
</mask> |
|||
<g mask="url(#mask0_2035_16285)"> |
|||
<path d="M0 0L37.5896 0V37.5896H0L0 0Z" fill="url(#paint0_linear_2035_16285)"/> |
|||
<g mask="url(#mask0_2444_11588)"> |
|||
<path d="M0 0L37.5896 0V37.5896H0L0 0Z" fill="url(#paint0_linear_2444_11588)"/> |
|||
</g> |
|||
<defs> |
|||
<linearGradient id="paint0_linear_2035_16285" x1="37.8642" y1="37.6841" x2="8.92816" y2="2.01879" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#FF8DF0"/> |
|||
<stop offset="1" stop-color="#F14B46"/> |
|||
<linearGradient id="paint0_linear_2444_11588" x1="37.8642" y1="37.6841" x2="8.92816" y2="2.01879" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#FE6F82"/> |
|||
<stop offset="1" stop-color="#E03950"/> |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
@ -0,0 +1,3 @@ |
|||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M6.00434 22.0236C5.45434 22.0213 4.98447 21.8238 4.59473 21.4308C4.20498 21.0379 4.0109 20.5661 4.01246 20.0155L4.05301 10.0156C4.05524 9.46556 4.25315 8.99569 4.64673 8.60595C5.04032 8.21621 5.51177 8.02212 6.06111 8.02368L7.0611 8.02773L7.06921 6.02775C7.07482 4.64443 7.56726 3.46741 8.54654 2.49671C9.52582 1.52601 10.7068 1.04313 12.0894 1.04807C13.4721 1.05301 14.6494 1.54545 15.6215 2.5254C16.5935 3.50535 17.0761 4.68632 17.0691 6.0683L17.061 8.06828L18.061 8.07234C18.611 8.07457 19.0812 8.27248 19.4716 8.66606C19.862 9.05965 20.0558 9.53111 20.0529 10.0804L20.0123 20.0803C20.0101 20.6303 19.8125 21.1005 19.4196 21.491C19.0267 21.8814 18.5549 22.0751 18.0042 22.0722L6.00434 22.0236ZM13.4399 16.4667C13.8329 16.0763 14.0304 15.6061 14.0327 15.0561C14.0349 14.5061 13.8411 14.0346 13.4514 13.6417C13.0616 13.2488 12.5914 13.0509 12.0408 13.048C11.4901 13.0451 11.0187 13.2392 10.6264 13.6302C10.2342 14.0213 10.0362 14.4912 10.0327 15.0398C10.0291 15.5885 10.2232 16.0603 10.615 16.4552C11.0067 16.8501 11.4766 17.0477 12.0246 17.0479C12.5726 17.0482 13.0444 16.8544 13.4399 16.4667ZM9.06108 8.03584L15.061 8.06017L15.0691 6.06019C15.0725 5.22686 14.7837 4.51735 14.2028 3.93166C13.6218 3.34596 12.9147 3.05143 12.0813 3.04805C11.248 3.04467 10.5385 3.33346 9.9528 3.91443C9.36711 4.49539 9.07257 5.20253 9.06919 6.03586L9.06108 8.03584Z" fill="#747474"/> |
|||
</svg> |
|||
15
public/assets/images/noun-wedding-rings-6540466 1.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,9 +1,9 @@ |
|||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M18.7918 3.13281C20.3406 3.13281 21.8547 3.5921 23.1425 4.4526C24.4304 5.3131 25.4341 6.53617 26.0268 7.96713C26.6196 9.39809 26.7746 10.9727 26.4725 12.4918C26.1703 14.0109 25.4245 15.4063 24.3292 16.5015C23.234 17.5967 21.8387 18.3425 20.3196 18.6447C18.8005 18.9469 17.2259 18.7918 15.7949 18.1991C14.364 17.6063 13.1409 16.6026 12.2804 15.3148C11.4199 14.0269 10.9606 12.5128 10.9606 10.964L10.9684 10.6241C11.056 8.60798 11.9185 6.70348 13.3761 5.30778C14.8336 3.91208 16.7737 3.13293 18.7918 3.13281ZM21.9242 21.9276C24.0012 21.9276 25.9931 22.7527 27.4617 24.2213C28.9303 25.69 29.7554 27.6819 29.7554 29.7588V31.325C29.7554 32.1558 29.4254 32.9526 28.8379 33.54C28.2505 34.1275 27.4537 34.4575 26.6229 34.4575H10.9606C10.1298 34.4575 9.33306 34.1275 8.7456 33.54C8.15815 32.9526 7.82813 32.1558 7.82812 31.325V29.7588C7.82812 27.6819 8.65319 25.69 10.1218 24.2213C11.5905 22.7527 13.5823 21.9276 15.6593 21.9276H21.9242Z" fill="url(#paint0_linear_2035_16280)"/> |
|||
<path d="M18.7918 3.13281C20.3406 3.13281 21.8547 3.5921 23.1425 4.4526C24.4304 5.3131 25.4341 6.53617 26.0268 7.96713C26.6196 9.39809 26.7746 10.9727 26.4725 12.4918C26.1703 14.0109 25.4245 15.4063 24.3292 16.5015C23.234 17.5967 21.8387 18.3425 20.3196 18.6447C18.8005 18.9469 17.2259 18.7918 15.7949 18.1991C14.364 17.6063 13.1409 16.6026 12.2804 15.3148C11.4199 14.0269 10.9606 12.5128 10.9606 10.964L10.9684 10.6241C11.056 8.60798 11.9185 6.70348 13.3761 5.30778C14.8336 3.91208 16.7737 3.13293 18.7918 3.13281ZM21.9242 21.9276C24.0012 21.9276 25.9931 22.7527 27.4617 24.2213C28.9303 25.69 29.7554 27.6819 29.7554 29.7588V31.325C29.7554 32.1558 29.4254 32.9526 28.8379 33.54C28.2505 34.1275 27.4537 34.4575 26.6229 34.4575H10.9606C10.1298 34.4575 9.33306 34.1275 8.7456 33.54C8.15815 32.9526 7.82813 32.1558 7.82812 31.325V29.7588C7.82812 27.6819 8.65319 25.69 10.1218 24.2213C11.5905 22.7527 13.5823 21.9276 15.6593 21.9276H21.9242Z" fill="url(#paint0_linear_2444_11583)"/> |
|||
<defs> |
|||
<linearGradient id="paint0_linear_2035_16280" x1="29.9156" y1="34.5362" x2="5.53913" y2="13.5045" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#FF8DF0"/> |
|||
<stop offset="1" stop-color="#F14B46"/> |
|||
<linearGradient id="paint0_linear_2444_11583" x1="29.9156" y1="34.5362" x2="5.53913" y2="13.5045" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#FE6F82"/> |
|||
<stop offset="1" stop-color="#E03950"/> |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
@ -1,9 +1,9 @@ |
|||
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|||
<path d="M3.44531 14.7233C3.44531 16.7594 3.75856 19.8919 6.57778 22.7111C9.08376 25.2171 17.3848 30.8555 17.6981 31.1688C18.0113 31.3254 18.3245 31.482 18.6378 31.482C18.951 31.482 19.2643 31.3254 19.5775 31.1688C19.8908 30.8555 28.1918 25.3737 30.6978 22.7111C33.517 19.8919 33.8303 16.7594 33.8303 14.7233C33.8303 10.0246 30.0713 6.26562 25.3726 6.26562C22.8666 6.26562 20.3607 7.67524 18.7944 9.86797C17.2282 7.67524 14.7222 6.26562 11.903 6.26562C7.3609 6.26562 3.44531 10.0246 3.44531 14.7233Z" fill="url(#paint0_linear_2035_16295)"/> |
|||
<path d="M3.44531 14.7233C3.44531 16.7594 3.75856 19.8919 6.57778 22.7111C9.08376 25.2171 17.3848 30.8555 17.6981 31.1688C18.0113 31.3254 18.3245 31.482 18.6378 31.482C18.951 31.482 19.2643 31.3254 19.5775 31.1688C19.8908 30.8555 28.1918 25.3737 30.6978 22.7111C33.517 19.8919 33.8303 16.7594 33.8303 14.7233C33.8303 10.0246 30.0713 6.26562 25.3726 6.26562C22.8666 6.26562 20.3607 7.67524 18.7944 9.86797C17.2282 7.67524 14.7222 6.26562 11.903 6.26562C7.3609 6.26562 3.44531 10.0246 3.44531 14.7233Z" fill="url(#paint0_linear_2444_11598)"/> |
|||
<defs> |
|||
<linearGradient id="paint0_linear_2035_16295" x1="34.0522" y1="31.5454" x2="15.6718" y2="4.24691" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#FF8DF0"/> |
|||
<stop offset="1" stop-color="#F14B46"/> |
|||
<linearGradient id="paint0_linear_2444_11598" x1="34.0522" y1="31.5454" x2="15.6718" y2="4.24691" gradientUnits="userSpaceOnUse"> |
|||
<stop stop-color="#FE6F82"/> |
|||
<stop offset="1" stop-color="#E03950"/> |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/candidate-contact/page"; |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/finding-match/page"; |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/intro/page"; |
|||
@ -0,0 +1,31 @@ |
|||
import { notFound } from "next/navigation"; |
|||
import LanguageSwitcher from "@/components/ui/language-switcher"; |
|||
import { isLocale, localeDirections } from "@/i18n/config"; |
|||
import { I18nProvider } from "@/i18n/provider"; |
|||
|
|||
export function generateStaticParams() { |
|||
return [{ lang: "en" }, { lang: "fa" }]; |
|||
} |
|||
|
|||
export default async function LocaleLayout({ |
|||
children, |
|||
params, |
|||
}: { |
|||
children: React.ReactNode; |
|||
params: Promise<{ lang: string }>; |
|||
}) { |
|||
const { lang } = await params; |
|||
|
|||
if (!isLocale(lang)) { |
|||
notFound(); |
|||
} |
|||
|
|||
return ( |
|||
<I18nProvider locale={lang}> |
|||
<div dir={localeDirections[lang]}> |
|||
<LanguageSwitcher /> |
|||
{children} |
|||
</div> |
|||
</I18nProvider> |
|||
); |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/new-match/page"; |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/intro/page"; |
|||
@ -0,0 +1,4 @@ |
|||
export { |
|||
default, |
|||
generateStaticParams, |
|||
} from "@/app/questions-list/[slug]/page"; |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/questions-list/page"; |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/request-accepted/page"; |
|||
@ -0,0 +1 @@ |
|||
export { default } from "@/app/slider/page"; |
|||
@ -0,0 +1,303 @@ |
|||
import type { NextRequest } from "next/server"; |
|||
|
|||
export const dynamic = "force-dynamic"; |
|||
export const runtime = "nodejs"; |
|||
|
|||
const PROXY_PATH_PARAM = "__proxyPath"; |
|||
|
|||
const REQUEST_HEADERS_TO_FORWARD = [ |
|||
"accept", |
|||
"accept-language", |
|||
"authorization", |
|||
"content-type", |
|||
"x-csrf-token", |
|||
"x-csrftoken", |
|||
"x-requested-with", |
|||
"x-xsrf-token", |
|||
]; |
|||
|
|||
const RESPONSE_HEADERS_TO_DROP = [ |
|||
"connection", |
|||
"content-encoding", |
|||
"content-length", |
|||
"keep-alive", |
|||
"proxy-authenticate", |
|||
"proxy-authorization", |
|||
"te", |
|||
"trailer", |
|||
"transfer-encoding", |
|||
"upgrade", |
|||
]; |
|||
|
|||
const MAX_LOG_BODY_LENGTH = 10_000; |
|||
const shouldLogProxy = |
|||
process.env.LOG_API_PROXY === "true" || |
|||
(process.env.LOG_API_PROXY !== "false" && |
|||
process.env.NODE_ENV !== "production"); |
|||
|
|||
class ProxyError extends Error { |
|||
constructor( |
|||
message: string, |
|||
readonly status: number, |
|||
) { |
|||
super(message); |
|||
} |
|||
} |
|||
|
|||
function getApiBaseUrl() { |
|||
const apiBaseUrl = |
|||
process.env.API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL; |
|||
|
|||
if (!apiBaseUrl) { |
|||
throw new ProxyError( |
|||
"API_BASE_URL or NEXT_PUBLIC_API_BASE_URL is required", |
|||
500, |
|||
); |
|||
} |
|||
|
|||
try { |
|||
return new URL(apiBaseUrl); |
|||
} catch { |
|||
throw new ProxyError("API base URL is invalid", 500); |
|||
} |
|||
} |
|||
|
|||
function getProxyPath(request: NextRequest) { |
|||
const proxyPath = request.nextUrl.searchParams.get(PROXY_PATH_PARAM); |
|||
|
|||
if (!proxyPath) { |
|||
throw new ProxyError("Proxy path is required", 400); |
|||
} |
|||
|
|||
if (/^[a-z][a-z\d+\-.]*:/i.test(proxyPath) || proxyPath.startsWith("//")) { |
|||
throw new ProxyError("Proxy path must be relative", 400); |
|||
} |
|||
|
|||
return proxyPath.startsWith("/") ? proxyPath : `/${proxyPath}`; |
|||
} |
|||
|
|||
function getTargetUrl(request: NextRequest) { |
|||
const targetUrl = getApiBaseUrl(); |
|||
const proxyUrl = new URL(getProxyPath(request), targetUrl.origin); |
|||
const basePath = targetUrl.pathname.replace(/\/$/, ""); |
|||
|
|||
targetUrl.pathname = `${basePath}${proxyUrl.pathname}`; |
|||
|
|||
const searchParams = new URLSearchParams(proxyUrl.search); |
|||
request.nextUrl.searchParams.forEach((value, key) => { |
|||
if (key !== PROXY_PATH_PARAM) { |
|||
searchParams.append(key, value); |
|||
} |
|||
}); |
|||
targetUrl.search = searchParams.toString(); |
|||
|
|||
return targetUrl; |
|||
} |
|||
|
|||
function getRequestHeaders(request: NextRequest, targetUrl: URL) { |
|||
const headers = new Headers(); |
|||
const authKey = process.env.NEXT_PUBLIC_AUTH_KEY; |
|||
|
|||
for (const header of REQUEST_HEADERS_TO_FORWARD) { |
|||
const value = request.headers.get(header); |
|||
|
|||
if (value) { |
|||
headers.set(header, value); |
|||
} |
|||
} |
|||
|
|||
if (authKey) { |
|||
headers.set("authorization", `token ${authKey}`); |
|||
} |
|||
|
|||
headers.set("accept-encoding", "identity"); |
|||
headers.set("http_x_user_language", "en"); |
|||
headers.set("origin", targetUrl.origin); |
|||
headers.set("platform", "android"); |
|||
headers.set("referer", `${targetUrl.origin}/`); |
|||
headers.set("user-agent", "dart:io"); |
|||
|
|||
return headers; |
|||
} |
|||
|
|||
function getResponseHeaders(upstreamHeaders: Headers) { |
|||
const headers = new Headers(upstreamHeaders); |
|||
|
|||
for (const header of RESPONSE_HEADERS_TO_DROP) { |
|||
headers.delete(header); |
|||
} |
|||
|
|||
const setCookieHeaders = |
|||
( |
|||
upstreamHeaders as Headers & { getSetCookie?: () => string[] } |
|||
).getSetCookie?.() ?? []; |
|||
|
|||
if (setCookieHeaders.length > 0) { |
|||
headers.delete("set-cookie"); |
|||
|
|||
for (const cookie of setCookieHeaders) { |
|||
headers.append("set-cookie", cookie); |
|||
} |
|||
} |
|||
|
|||
return headers; |
|||
} |
|||
|
|||
function getBodyLogValue(body: ArrayBuffer | undefined, contentType?: string) { |
|||
if (!body || body.byteLength === 0) { |
|||
return null; |
|||
} |
|||
|
|||
if (contentType && !isTextContentType(contentType)) { |
|||
return `[${body.byteLength} bytes; ${contentType}]`; |
|||
} |
|||
|
|||
const text = new TextDecoder().decode(body); |
|||
|
|||
if (text.length <= MAX_LOG_BODY_LENGTH) { |
|||
return parseJsonForLog(text, text); |
|||
} |
|||
|
|||
return `${text.slice(0, MAX_LOG_BODY_LENGTH)}... [truncated ${text.length - MAX_LOG_BODY_LENGTH} chars]`; |
|||
} |
|||
|
|||
function isTextContentType(contentType: string) { |
|||
return ( |
|||
contentType.includes("application/json") || |
|||
contentType.includes("application/problem+json") || |
|||
contentType.startsWith("text/") || |
|||
contentType.includes("+json") || |
|||
contentType.includes("+xml") |
|||
); |
|||
} |
|||
|
|||
function parseJsonForLog(text: string, fallback: string) { |
|||
try { |
|||
return JSON.parse(text); |
|||
} catch { |
|||
return fallback; |
|||
} |
|||
} |
|||
|
|||
function headersToObject(headers: Headers) { |
|||
return Object.fromEntries(headers.entries()); |
|||
} |
|||
|
|||
function logProxyRequest( |
|||
request: NextRequest, |
|||
targetUrl: URL, |
|||
requestHeaders: Headers, |
|||
requestBody: ArrayBuffer | undefined, |
|||
) { |
|||
if (!shouldLogProxy) { |
|||
return; |
|||
} |
|||
|
|||
writeProxyLog("request", { |
|||
incomingRequest: { |
|||
method: request.method, |
|||
url: request.url, |
|||
nextUrl: request.nextUrl.toString(), |
|||
headers: headersToObject(request.headers), |
|||
body: getBodyLogValue( |
|||
requestBody, |
|||
request.headers.get("content-type") ?? undefined, |
|||
), |
|||
}, |
|||
upstreamRequest: { |
|||
method: request.method, |
|||
url: targetUrl.toString(), |
|||
headers: headersToObject(requestHeaders), |
|||
body: getBodyLogValue( |
|||
requestBody, |
|||
requestHeaders.get("content-type") ?? undefined, |
|||
), |
|||
}, |
|||
payload: getBodyLogValue( |
|||
requestBody, |
|||
request.headers.get("content-type") ?? undefined, |
|||
), |
|||
}); |
|||
} |
|||
|
|||
function logProxyResponse( |
|||
upstreamResponse: Response, |
|||
responseBody: ArrayBuffer, |
|||
) { |
|||
if (!shouldLogProxy) { |
|||
return; |
|||
} |
|||
|
|||
writeProxyLog("response", { |
|||
status: upstreamResponse.status, |
|||
statusText: upstreamResponse.statusText, |
|||
headers: headersToObject(upstreamResponse.headers), |
|||
body: getBodyLogValue( |
|||
responseBody, |
|||
upstreamResponse.headers.get("content-type") ?? undefined, |
|||
), |
|||
response: { |
|||
status: upstreamResponse.status, |
|||
statusText: upstreamResponse.statusText, |
|||
headers: headersToObject(upstreamResponse.headers), |
|||
body: getBodyLogValue( |
|||
responseBody, |
|||
upstreamResponse.headers.get("content-type") ?? undefined, |
|||
), |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
function writeProxyLog(label: string, value: unknown) { |
|||
console.log(`[api-proxy] ${label} ${JSON.stringify(value)}`); |
|||
} |
|||
|
|||
async function proxyRequest(request: NextRequest) { |
|||
try { |
|||
const targetUrl = getTargetUrl(request); |
|||
const requestBody = |
|||
request.method === "GET" || request.method === "HEAD" |
|||
? undefined |
|||
: await request.arrayBuffer(); |
|||
const requestHeaders = getRequestHeaders(request, targetUrl); |
|||
|
|||
logProxyRequest(request, targetUrl, requestHeaders, requestBody); |
|||
|
|||
const upstreamResponse = await fetch(targetUrl, { |
|||
method: request.method, |
|||
headers: requestHeaders, |
|||
body: requestBody, |
|||
cache: "no-store", |
|||
}); |
|||
const responseBody = await upstreamResponse.arrayBuffer(); |
|||
|
|||
logProxyResponse(upstreamResponse, responseBody); |
|||
|
|||
return new Response(responseBody, { |
|||
status: upstreamResponse.status, |
|||
statusText: upstreamResponse.statusText, |
|||
headers: getResponseHeaders(upstreamResponse.headers), |
|||
}); |
|||
} catch (error) { |
|||
if (error instanceof ProxyError) { |
|||
return Response.json({ error: error.message }, { status: error.status }); |
|||
} |
|||
|
|||
console.error("API proxy request failed", error); |
|||
|
|||
return Response.json( |
|||
{ error: "API proxy request failed" }, |
|||
{ status: 502 }, |
|||
); |
|||
} |
|||
} |
|||
|
|||
export { |
|||
proxyRequest as DELETE, |
|||
proxyRequest as GET, |
|||
proxyRequest as HEAD, |
|||
proxyRequest as OPTIONS, |
|||
proxyRequest as PATCH, |
|||
proxyRequest as POST, |
|||
proxyRequest as PUT, |
|||
}; |
|||
@ -0,0 +1,64 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import { useState } from "react"; |
|||
import Button from "@/components/ui/button"; |
|||
import CallResultSheet from "@/components/ui/call-result-sheet"; |
|||
import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet"; |
|||
import NavigationButton from "@/components/ui/navigation-button"; |
|||
import { useI18n } from "@/i18n/provider"; |
|||
|
|||
export default function CandidateContactPage() { |
|||
const { dictionary: t } = useI18n(); |
|||
const [isCallResultSheetOpen, setIsCallResultSheetOpen] = useState(false); |
|||
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = |
|||
useState(false); |
|||
|
|||
return ( |
|||
<> |
|||
{isCallResultSheetOpen ? ( |
|||
<CallResultSheet |
|||
onClose={() => setIsCallResultSheetOpen(false)} |
|||
onOtherReasonsClick={() => setIsDismissReasonSheetOpen(true)} |
|||
/> |
|||
) : null} |
|||
{isDismissReasonSheetOpen ? ( |
|||
<DismissReasonSheet |
|||
onClose={() => setIsDismissReasonSheetOpen(false)} |
|||
/> |
|||
) : null} |
|||
|
|||
<main className="-mx-[17px] flex min-h-screen flex-col px-[17px] pt-10 pb-36"> |
|||
<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> |
|||
|
|||
<section className="flex flex-1 items-center justify-center"> |
|||
<div className="flex max-w-[300px] flex-col items-center"> |
|||
<Image |
|||
src="/assets/images/Group 1597880467.svg" |
|||
alt={t.candidateContact.imageAlt} |
|||
width={131} |
|||
height={125} |
|||
/> |
|||
|
|||
<h1 className="mt-10 text-center text-[18px] leading-[1.2] font-bold text-[#1A1A1A]"> |
|||
{t.candidateContact.title} |
|||
</h1> |
|||
</div> |
|||
</section> |
|||
|
|||
<section className="fixed right-0 bottom-0 left-0 z-20 mx-auto flex max-w-[375px] gap-3 rounded-t-[30px] bg-white px-[17px] pt-6 pb-8 shadow-[0_-18px_50px_rgba(15,23,42,0.08)]"> |
|||
<Button className="" onClick={() => setIsCallResultSheetOpen(true)}> |
|||
{t.candidateContact.contacted} |
|||
</Button> |
|||
<Button className="" description={t.candidateContact.afterTwoDays}> |
|||
{t.candidateContact.noContactYet} |
|||
</Button> |
|||
</section> |
|||
</main> |
|||
</> |
|||
); |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import Link from "next/link"; |
|||
import { FaPen } from "react-icons/fa6"; |
|||
import Button from "@/components/ui/button"; |
|||
import NavigationButton from "@/components/ui/navigation-button"; |
|||
import { PageBackground } from "@/components/utils/page-background"; |
|||
import { localizePath } from "@/i18n/config"; |
|||
import { useI18n } from "@/i18n/provider"; |
|||
|
|||
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 FindingMatchPage() { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const copy = t.findingMatch; |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
|
|||
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-7 pb-5 text-center"> |
|||
<header className="-mx-[6px] flex items-center justify-between"> |
|||
<NavigationButton icon="back" /> |
|||
<h1 className="font-faminela">{t.common.appName}</h1> |
|||
<NavigationButton icon="subscription" iconLabel="Subscribe" /> |
|||
</header> |
|||
|
|||
<section className="flex flex-1 flex-col items-center mt-32"> |
|||
<div className="relative h-[120px] w-[124px]" aria-hidden="true"> |
|||
<Image |
|||
src="/assets/images/Group 159788fd0467.svg" |
|||
alt="" |
|||
fill |
|||
sizes="58px" |
|||
className="object-cover" |
|||
priority |
|||
/> |
|||
</div> |
|||
|
|||
<h1 className="mt-5 text-[22px] font-bold leading-none tracking-[0.02em] text-[#171717] uppercase"> |
|||
{copy.title} |
|||
</h1> |
|||
|
|||
<p className="mt-3 max-w-[320px] text-[14px] leading-[1.35] font-semibold text-[#747474]"> |
|||
{copy.description} |
|||
</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> |
|||
|
|||
<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> |
|||
</main> |
|||
</> |
|||
); |
|||
} |
|||
@ -0,0 +1,193 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import { useRouter } from "next/navigation"; |
|||
import { useState } from "react"; |
|||
import { IoClose } from "react-icons/io5"; |
|||
import Button from "@/components/ui/button"; |
|||
import InformationSheet from "@/components/ui/information-sheet"; |
|||
import NavigationButton from "@/components/ui/navigation-button"; |
|||
import StickyHeader from "@/components/ui/sticky-header"; |
|||
import { localizePath } from "@/i18n/config"; |
|||
import { useI18n } from "@/i18n/provider"; |
|||
|
|||
type MatchFieldProps = { |
|||
label: string; |
|||
value: string; |
|||
hint?: string; |
|||
}; |
|||
|
|||
type MatchSplitFieldProps = { |
|||
label: string; |
|||
hint?: string; |
|||
values: [string, string]; |
|||
}; |
|||
|
|||
function MatchField({ label, value, hint }: MatchFieldProps) { |
|||
return ( |
|||
<div className="space-y-2.5"> |
|||
<p className="text-[15px] leading-5 font-semibold text-[#1A1A1A]"> |
|||
{label} |
|||
{hint ? ( |
|||
<span className="ml-1.5 text-[12px] font-normal text-[#7A7A7A]"> |
|||
{hint} |
|||
</span> |
|||
) : null} |
|||
</p> |
|||
<div |
|||
aria-label={`${label}: ${value}`} |
|||
className="rounded-[15px] border border-white/90 bg-[#F7F1F1]/95 px-4 py-[17px] text-[15px] text-[#2C2C2C] shadow-[0_8px_24px_rgba(224,57,80,0.05)]" |
|||
role="note" |
|||
> |
|||
{value} |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function MatchSplitField({ label, hint, values }: MatchSplitFieldProps) { |
|||
return ( |
|||
<div className="space-y-2.5"> |
|||
<p className="text-[15px] leading-5 font-semibold text-[#1A1A1A]"> |
|||
{label} |
|||
{hint ? ( |
|||
<span className="ml-1.5 text-[12px] font-normal text-[#7A7A7A]"> |
|||
{hint} |
|||
</span> |
|||
) : null} |
|||
</p> |
|||
<div className="grid grid-cols-2 gap-3"> |
|||
{values.map((value) => ( |
|||
<div |
|||
key={`${label}-${value}`} |
|||
aria-label={`${label}: ${value}`} |
|||
className="rounded-[15px] border border-white/90 bg-[#F7F1F1]/95 px-4 py-[17px] text-[15px] text-[#2C2C2C] shadow-[0_8px_24px_rgba(224,57,80,0.05)]" |
|||
role="note" |
|||
> |
|||
{value} |
|||
</div> |
|||
))} |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default function NewMatchPage() { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const [isAcceptSheetOpen, setIsAcceptSheetOpen] = useState(false); |
|||
const [shouldNavigateToRequestAccepted, setShouldNavigateToRequestAccepted] = |
|||
useState(false); |
|||
const router = useRouter(); |
|||
|
|||
return ( |
|||
<> |
|||
{isAcceptSheetOpen ? ( |
|||
<InformationSheet |
|||
icon="check" |
|||
title={t.match.acceptProfile} |
|||
description={t.match.acceptDescription} |
|||
onClose={() => { |
|||
setIsAcceptSheetOpen(false); |
|||
|
|||
if (shouldNavigateToRequestAccepted) { |
|||
setShouldNavigateToRequestAccepted(false); |
|||
router.push(localizePath("/request-accepted", locale)); |
|||
} |
|||
}} |
|||
buttons={({ close }) => ( |
|||
<div className="flex w-full gap-3"> |
|||
<Button |
|||
className="border border-[#D7D7D7] bg-transparent text-[#6B6B6B]" |
|||
onClick={close} |
|||
variant="outlined" |
|||
> |
|||
{t.common.cancel} |
|||
</Button> |
|||
<Button |
|||
className="border-none bg-[#21C17D] bg-none from-[#21C17D] to-[#21C17D] shadow-none" |
|||
onClick={() => { |
|||
setShouldNavigateToRequestAccepted(true); |
|||
close(); |
|||
}} |
|||
> |
|||
{t.common.confirm} |
|||
</Button> |
|||
</div> |
|||
)} |
|||
/> |
|||
) : null} |
|||
|
|||
<main className="-mx-[17px] flex min-h-screen flex-col overflow-hidden"> |
|||
<StickyHeader className="rounded-b-[32px] px-[17px] pt-7 pb-6"> |
|||
<div className="flex items-center justify-between"> |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="close" |
|||
iconLabel={t.match.goBack} |
|||
/> |
|||
<h1 className="text-[26px] text-white">{t.match.title}</h1> |
|||
<NavigationButton |
|||
variant="transparent" |
|||
icon="info" |
|||
iconLabel={t.common.support} |
|||
/> |
|||
</div> |
|||
</StickyHeader> |
|||
|
|||
<section className="px-[17px] pt-5 pb-36"> |
|||
<div className="space-y-5"> |
|||
<MatchField label={t.match.fields.birthYear} value="2024/12/04" /> |
|||
<MatchField |
|||
label={t.match.fields.nationality} |
|||
value={t.match.values.iranian} |
|||
/> |
|||
<MatchSplitField |
|||
label={t.match.fields.residence} |
|||
values={[t.match.values.iran, t.match.values.tehran]} |
|||
/> |
|||
<MatchSplitField |
|||
label={t.match.fields.futureResidence} |
|||
values={[t.match.values.iran, t.match.values.tehran]} |
|||
/> |
|||
<MatchField |
|||
label={t.match.fields.religion} |
|||
value={t.match.values.muslim} |
|||
/> |
|||
<MatchField |
|||
label={t.match.fields.countryCity} |
|||
hint={t.match.fields.currentlyLivingIn} |
|||
value={`${t.match.values.iran} / ${t.match.values.tehran}`} |
|||
/> |
|||
<MatchField |
|||
label={t.match.fields.education} |
|||
value={t.match.values.education} |
|||
/> |
|||
<MatchField |
|||
label={t.match.fields.occupation} |
|||
value={t.match.values.occupation} |
|||
/> |
|||
</div> |
|||
</section> |
|||
|
|||
<section className="fixed right-0 bottom-0 left-0 z-20 mx-auto flex max-w-[375px] justify-evenly rounded-t-[30px] bg-white px-[17px] pt-6 pb-8 shadow-[0_-18px_50px_rgba(15,23,42,0.08)]"> |
|||
<div className="rounded-full bg-white p-3.5 shadow-md"> |
|||
<IoClose size={28} /> |
|||
</div> |
|||
<button |
|||
aria-label={t.match.acceptProfile} |
|||
className="rounded-full bg-[#F0445B] p-3.5 shadow-md" |
|||
onClick={() => setIsAcceptSheetOpen(true)} |
|||
type="button" |
|||
> |
|||
<Image |
|||
src={"/assets/images/Icon.svg"} |
|||
width={28} |
|||
height={28} |
|||
alt="heart" |
|||
/> |
|||
</button> |
|||
</section> |
|||
</main> |
|||
</> |
|||
); |
|||
} |
|||
@ -1,44 +1 @@ |
|||
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="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 10–15 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 |
|||
key={question.slug} |
|||
slug={question.slug} |
|||
title={question.title} |
|||
requiredLabel={question.required} |
|||
estimate="30 min" |
|||
progress={question.progress} |
|||
tooltip={question.tooltip} |
|||
/> |
|||
)) |
|||
} |
|||
</div> |
|||
</main> |
|||
); |
|||
} |
|||
export { default } from "@/app/intro/page"; |
|||
@ -0,0 +1,26 @@ |
|||
"use client"; |
|||
|
|||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; |
|||
import { type ReactNode, useState } from "react"; |
|||
|
|||
type ProvidersProps = { |
|||
children: ReactNode; |
|||
}; |
|||
|
|||
export default function Providers({ children }: ProvidersProps) { |
|||
const [queryClient] = useState( |
|||
() => |
|||
new QueryClient({ |
|||
defaultOptions: { |
|||
queries: { |
|||
refetchOnWindowFocus: false, |
|||
retry: 1, |
|||
}, |
|||
}, |
|||
}), |
|||
); |
|||
|
|||
return ( |
|||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> |
|||
); |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
"use client"; |
|||
|
|||
import { useEffect, useMemo, useState } from "react"; |
|||
import { |
|||
getQuestionAnswersStorageKey, |
|||
hasQuestionAnswerValue, |
|||
} from "@/components/questions/question-answer-storage"; |
|||
import InformationSheet from "@/components/ui/information-sheet"; |
|||
import type { MarriageField } from "@/hooks/marriage/types"; |
|||
import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; |
|||
|
|||
type AnswerPaceSheetProps = { |
|||
continueLabel: string; |
|||
description: string; |
|||
slug: string; |
|||
title: string; |
|||
}; |
|||
|
|||
type StoredQuestionAnswers = { |
|||
fields?: unknown; |
|||
}; |
|||
|
|||
function isMarriageField(value: unknown): value is MarriageField { |
|||
if (!value || typeof value !== "object") { |
|||
return false; |
|||
} |
|||
|
|||
const field = value as Partial<MarriageField>; |
|||
|
|||
return ( |
|||
typeof field.key === "string" && |
|||
typeof field.label === "string" && |
|||
typeof field.type === "string" && |
|||
(field.value === null || |
|||
typeof field.value === "string" || |
|||
typeof field.value === "number" || |
|||
typeof field.value === "boolean") |
|||
); |
|||
} |
|||
|
|||
function hasStoredQuestionProgress(slugs: readonly string[]) { |
|||
try { |
|||
return slugs.some((slug) => { |
|||
const rawValue = window.localStorage.getItem( |
|||
getQuestionAnswersStorageKey(slug), |
|||
); |
|||
|
|||
if (!rawValue) { |
|||
return false; |
|||
} |
|||
|
|||
const storedValue = JSON.parse(rawValue) as StoredQuestionAnswers; |
|||
const fields = Array.isArray(storedValue.fields) |
|||
? storedValue.fields.filter(isMarriageField) |
|||
: []; |
|||
|
|||
return fields.some((field) => hasQuestionAnswerValue(field.value)); |
|||
}); |
|||
} catch { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
export default function AnswerPaceSheet({ |
|||
continueLabel, |
|||
description, |
|||
slug, |
|||
title, |
|||
}: AnswerPaceSheetProps) { |
|||
const { data: sections, isSuccess } = useMarriageSectionsQuery(); |
|||
const [hasSeenSheet, setHasSeenSheet] = useState(true); |
|||
const [hasLocalProgress, setHasLocalProgress] = useState(true); |
|||
const storageKey = `marriage:sections:${slug}:answer-pace-sheet-seen`; |
|||
const sectionSlugs = useMemo( |
|||
() => sections?.map((section) => section.slug) ?? [slug], |
|||
[sections, slug], |
|||
); |
|||
|
|||
const hasAnySectionProgress = useMemo(() => { |
|||
if (!sections?.length) { |
|||
return true; |
|||
} |
|||
|
|||
return sections.some( |
|||
(section) => section.current_step > 0 || section.completion_percent >= 1, |
|||
); |
|||
}, [sections]); |
|||
|
|||
const isOpen = |
|||
isSuccess && !hasSeenSheet && !hasLocalProgress && !hasAnySectionProgress; |
|||
|
|||
useEffect(() => { |
|||
setHasSeenSheet(window.sessionStorage.getItem(storageKey) === "true"); |
|||
setHasLocalProgress(hasStoredQuestionProgress(sectionSlugs)); |
|||
}, [sectionSlugs, storageKey]); |
|||
|
|||
useEffect(() => { |
|||
if (!isOpen) { |
|||
return; |
|||
} |
|||
|
|||
window.sessionStorage.setItem(storageKey, "true"); |
|||
}, [isOpen, storageKey]); |
|||
|
|||
if (!isOpen) { |
|||
return null; |
|||
} |
|||
|
|||
return ( |
|||
<InformationSheet |
|||
icon="play" |
|||
title={title} |
|||
description={description} |
|||
buttons={continueLabel} |
|||
/> |
|||
); |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
"use client"; |
|||
|
|||
import { useEffect, useMemo, useState } from "react"; |
|||
import { IoClose } from "react-icons/io5"; |
|||
import Button from "@/components/ui/button"; |
|||
import InformationSheet from "@/components/ui/information-sheet"; |
|||
import { bookingTerms } from "@/data/question-data"; |
|||
import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; |
|||
|
|||
const FIRST_ENTRY_TERMS_SEEN_KEY = "marriage:first-entry-terms-seen"; |
|||
|
|||
const FIRST_ENTRY_TERMS = [ |
|||
...bookingTerms, |
|||
"Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?", |
|||
"Taking this test is not mandatory, but it will help you better search for a spouse. The Kettle test is a test for self-knowledge and better understanding of your spouse.", |
|||
'Do you operate based on superficial behavioral adaptations, or are you aware of the deep "source traits" that fundamentally control your decision-making processes?', |
|||
"Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?", |
|||
"Taking this test is not mandatory, but it will help you better search for a spouse. The Kettle test is a test for self-knowledge and better understanding of your spouse.", |
|||
'Do you operate based on superficial behavioral adaptations, or are you aware of the deep "source traits" that fundamentally control your decision-making processes?', |
|||
] as const; |
|||
|
|||
export default function SectionsRequest() { |
|||
const { data: sections, isSuccess } = useMarriageSectionsQuery(); |
|||
const [hasSeenSheet, setHasSeenSheet] = useState(true); |
|||
|
|||
const hasNoProgression = useMemo(() => { |
|||
if (!sections?.length) { |
|||
return false; |
|||
} |
|||
|
|||
return sections.every( |
|||
(section) => section.current_step <= 0 && section.completion_percent <= 0, |
|||
); |
|||
}, [sections]); |
|||
|
|||
const isOpen = isSuccess && hasNoProgression && !hasSeenSheet; |
|||
|
|||
useEffect(() => { |
|||
setHasSeenSheet( |
|||
window.sessionStorage.getItem(FIRST_ENTRY_TERMS_SEEN_KEY) === "true", |
|||
); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
if (!isOpen) { |
|||
return; |
|||
} |
|||
|
|||
window.sessionStorage.setItem(FIRST_ENTRY_TERMS_SEEN_KEY, "true"); |
|||
}, [isOpen]); |
|||
|
|||
if (!isOpen) { |
|||
return null; |
|||
} |
|||
|
|||
return ( |
|||
<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]"> |
|||
Bookings Terms & Conditions |
|||
</span> |
|||
<button |
|||
type="button" |
|||
aria-label="Close booking terms" |
|||
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={ |
|||
<ul className="max-h-[60dvh] space-y-2 overflow-y-auto pl-4 text-left text-[11px] leading-[1.35] font-medium list-disc text-[#4C4C4C] marker:text-[#2B2B2B]"> |
|||
{FIRST_ENTRY_TERMS.map((item, index) => ( |
|||
<li key={`${index}-${item}`}>{item}</li> |
|||
))} |
|||
</ul> |
|||
} |
|||
buttons={({ close }) => ( |
|||
<Button className="rounded-[8px] py-[14px]" onClick={close}> |
|||
Got it |
|||
</Button> |
|||
)} |
|||
className="text-left" |
|||
/> |
|||
); |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import { useRouter } from "next/navigation"; |
|||
import Button from "@/components/ui/button"; |
|||
import NavigationButton from "@/components/ui/navigation-button"; |
|||
import { PageBackground } from "@/components/utils/page-background"; |
|||
import { localizePath } from "@/i18n/config"; |
|||
import { useI18n } from "@/i18n/provider"; |
|||
|
|||
export default function RequestAcceptedPage() { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const router = useRouter(); |
|||
|
|||
return ( |
|||
<> |
|||
<PageBackground /> |
|||
|
|||
<main className="-mx-[17px] flex min-h-screen flex-col px-[23px] pt-7 pb-10 text-center"> |
|||
<header className="-mx-[6px] 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="flex flex-1 flex-col justify-end gap-20 pt-[26px]"> |
|||
<section className="flex flex-col items-center"> |
|||
<div className="relative isolate flex items-center justify-center"> |
|||
<Image |
|||
src="/assets/images/Union.svg" |
|||
alt="" |
|||
width={375} |
|||
height={813} |
|||
aria-hidden="true" |
|||
className="pointer-events-none absolute top-1/2 left-1/2 -z-10 max-w-none -translate-x-1/2 -translate-y-1/2" |
|||
/> |
|||
|
|||
<Image |
|||
src="/assets/images/Group 1597880468.svg" |
|||
alt={t.requestAccepted.imageAlt} |
|||
width={131} |
|||
height={125} |
|||
priority |
|||
className="relative z-10" |
|||
/> |
|||
</div> |
|||
|
|||
<h1 className="mt-11 text-[22px] leading-none font-black tracking-[0.02em] text-[#171717] uppercase"> |
|||
{t.requestAccepted.title} |
|||
</h1> |
|||
|
|||
<p className="mt-4 max-w-[315px] text-[16px] leading-[1.45] font-semibold text-[#777777]"> |
|||
{t.requestAccepted.description} |
|||
</p> |
|||
|
|||
<div className="mt-9 w-full max-w-[212px]"> |
|||
<Button |
|||
className="border-none bg-[linear-gradient(180deg,#FF7387_0%,#F0445B_100%)] py-[18px] shadow-[0_14px_32px_rgba(240,68,91,0.3)]" |
|||
onClick={() => |
|||
router.push(localizePath("/candidate-contact", locale)) |
|||
} |
|||
> |
|||
{t.requestAccepted.viewContact} |
|||
</Button> |
|||
</div> |
|||
</section> |
|||
|
|||
<div className="space-y-8"> |
|||
<div className="rounded-[12px] border border-[#F0445B] bg-[#F0445B]/10 px-3 py-2.5 text-xs leading-4 font-medium text-[#F0445B]"> |
|||
{t.requestAccepted.penalty} |
|||
</div> |
|||
|
|||
<section className="flex flex-col items-center text-center"> |
|||
<div className="flex items-center gap-1"> |
|||
<Image |
|||
src={"/assets/images/material-symbols_lock.svg"} |
|||
width={24} |
|||
height={24} |
|||
alt="lock" |
|||
/> |
|||
|
|||
<h2 className="text-[17px] leading-none font-semibold text-[#747474]"> |
|||
{t.requestAccepted.profileLocked} |
|||
</h2> |
|||
</div> |
|||
|
|||
<p className="mt-1 text-[10px] font-semibold text-[#8B8B8B]"> |
|||
{t.requestAccepted.lockedDescription} |
|||
</p> |
|||
</section> |
|||
</div> |
|||
</div> |
|||
</main> |
|||
</> |
|||
); |
|||
} |
|||
@ -0,0 +1,445 @@ |
|||
"use client"; |
|||
|
|||
import { |
|||
createContext, |
|||
type ReactNode, |
|||
useCallback, |
|||
useContext, |
|||
useEffect, |
|||
useMemo, |
|||
useRef, |
|||
useState, |
|||
} from "react"; |
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { pathParam } from "@/hooks/marriage/path-param"; |
|||
import type { |
|||
MarriageField, |
|||
MarriageFieldValue, |
|||
UpdateMarriageSectionDataPayload, |
|||
} from "@/hooks/marriage/types"; |
|||
import { useUpdateMarriageSectionDataMutation } from "@/hooks/marriage/use-section-data"; |
|||
|
|||
const STORAGE_VERSION = 1; |
|||
const PROXY_PATH_PARAM = "__proxyPath"; |
|||
|
|||
type QuestionAnswersByKey = Record<string, MarriageField>; |
|||
|
|||
type StoredQuestionAnswers = { |
|||
version: typeof STORAGE_VERSION; |
|||
slug: string; |
|||
current_step: number; |
|||
fields: MarriageField[]; |
|||
pending_sync: boolean; |
|||
updated_at: string; |
|||
}; |
|||
|
|||
type FlushAnswersOptions = { |
|||
force?: boolean; |
|||
}; |
|||
|
|||
type QuestionAnswersContextValue = { |
|||
flushAnswers: (options?: FlushAnswersOptions) => Promise<void>; |
|||
getAnswerValue: ( |
|||
question: QuestionField, |
|||
questionIndex: number, |
|||
) => MarriageFieldValue | undefined; |
|||
hasPendingSync: boolean; |
|||
isSaving: boolean; |
|||
setAnswerValue: ( |
|||
question: QuestionField, |
|||
questionIndex: number, |
|||
value: MarriageFieldValue, |
|||
) => void; |
|||
}; |
|||
|
|||
type QuestionAnswersProviderProps = { |
|||
children: ReactNode; |
|||
questions: readonly QuestionField[]; |
|||
slug: string; |
|||
}; |
|||
|
|||
const QuestionAnswersContext = |
|||
createContext<QuestionAnswersContextValue | null>(null); |
|||
|
|||
function hashString(value: string) { |
|||
let hash = 0; |
|||
|
|||
for (let index = 0; index < value.length; index += 1) { |
|||
hash = (hash * 31 + value.charCodeAt(index)) >>> 0; |
|||
} |
|||
|
|||
return hash.toString(36); |
|||
} |
|||
|
|||
function slugifyQuestionTitle(title: string) { |
|||
const slug = title |
|||
.normalize("NFKD") |
|||
.replace(/[\u0300-\u036f]/g, "") |
|||
.toLowerCase() |
|||
.replace(/[^a-z0-9]+/g, "_") |
|||
.replace(/^_+|_+$/g, ""); |
|||
|
|||
return slug || `field_${hashString(title)}`; |
|||
} |
|||
|
|||
function getQuestionFieldKey(question: QuestionField, questionIndex: number) { |
|||
return `q${questionIndex + 1}_${slugifyQuestionTitle(question.title)}`; |
|||
} |
|||
|
|||
export function getQuestionAnswersStorageKey(slug: string) { |
|||
return `marriage:sections:${slug}:answers`; |
|||
} |
|||
|
|||
export function hasQuestionAnswerValue(value: MarriageFieldValue) { |
|||
if (value === null) { |
|||
return false; |
|||
} |
|||
|
|||
if (typeof value === "string") { |
|||
return value.trim().length > 0; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
function isMarriageField(value: unknown): value is MarriageField { |
|||
if (!value || typeof value !== "object") { |
|||
return false; |
|||
} |
|||
|
|||
const field = value as Partial<MarriageField>; |
|||
|
|||
return ( |
|||
typeof field.key === "string" && |
|||
typeof field.label === "string" && |
|||
typeof field.type === "string" && |
|||
(field.value === null || |
|||
typeof field.value === "string" || |
|||
typeof field.value === "number" || |
|||
typeof field.value === "boolean") |
|||
); |
|||
} |
|||
|
|||
function createQuestionField( |
|||
question: QuestionField, |
|||
questionIndex: number, |
|||
value: MarriageFieldValue, |
|||
): MarriageField { |
|||
return { |
|||
key: getQuestionFieldKey(question, questionIndex), |
|||
label: question.title, |
|||
type: question.type, |
|||
value, |
|||
}; |
|||
} |
|||
|
|||
function getOrderedFields( |
|||
answers: QuestionAnswersByKey, |
|||
questions: readonly QuestionField[], |
|||
) { |
|||
const orderedFields: MarriageField[] = []; |
|||
const orderedKeys = new Set<string>(); |
|||
|
|||
questions.forEach((question, index) => { |
|||
const key = getQuestionFieldKey(question, index); |
|||
const field = answers[key]; |
|||
|
|||
if (field) { |
|||
orderedFields.push(field); |
|||
orderedKeys.add(key); |
|||
} |
|||
}); |
|||
|
|||
Object.entries(answers).forEach(([key, field]) => { |
|||
if (!orderedKeys.has(key)) { |
|||
orderedFields.push(field); |
|||
} |
|||
}); |
|||
|
|||
return orderedFields; |
|||
} |
|||
|
|||
function getCurrentStep(fields: MarriageField[]) { |
|||
return fields.filter((field) => hasQuestionAnswerValue(field.value)).length; |
|||
} |
|||
|
|||
function createPayload( |
|||
answers: QuestionAnswersByKey, |
|||
questions: readonly QuestionField[], |
|||
): UpdateMarriageSectionDataPayload { |
|||
const fields = getOrderedFields(answers, questions); |
|||
|
|||
return { |
|||
current_step: getCurrentStep(fields), |
|||
fields, |
|||
}; |
|||
} |
|||
|
|||
function fieldsToAnswers(fields: MarriageField[]) { |
|||
return fields.reduce<QuestionAnswersByKey>((nextAnswers, field) => { |
|||
nextAnswers[field.key] = field; |
|||
return nextAnswers; |
|||
}, {}); |
|||
} |
|||
|
|||
function readStoredAnswers(storageKey: string, slug: string) { |
|||
try { |
|||
const rawValue = window.localStorage.getItem(storageKey); |
|||
|
|||
if (!rawValue) { |
|||
return { |
|||
answers: {}, |
|||
pendingSync: false, |
|||
}; |
|||
} |
|||
|
|||
const storedValue = JSON.parse(rawValue) as Partial<StoredQuestionAnswers>; |
|||
const fields = Array.isArray(storedValue.fields) |
|||
? storedValue.fields.filter(isMarriageField) |
|||
: []; |
|||
|
|||
return { |
|||
answers: fieldsToAnswers(fields), |
|||
pendingSync: |
|||
storedValue.slug === slug && |
|||
(storedValue.pending_sync ?? fields.length > 0), |
|||
}; |
|||
} catch { |
|||
return { |
|||
answers: {}, |
|||
pendingSync: false, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
function writeStoredAnswers( |
|||
storageKey: string, |
|||
slug: string, |
|||
questions: readonly QuestionField[], |
|||
answers: QuestionAnswersByKey, |
|||
pendingSync: boolean, |
|||
) { |
|||
try { |
|||
const payload = createPayload(answers, questions); |
|||
|
|||
if (payload.fields.length === 0) { |
|||
window.localStorage.removeItem(storageKey); |
|||
return; |
|||
} |
|||
|
|||
const storedValue: StoredQuestionAnswers = { |
|||
version: STORAGE_VERSION, |
|||
slug, |
|||
current_step: payload.current_step, |
|||
fields: payload.fields, |
|||
pending_sync: pendingSync, |
|||
updated_at: new Date().toISOString(), |
|||
}; |
|||
|
|||
window.localStorage.setItem(storageKey, JSON.stringify(storedValue)); |
|||
} catch { |
|||
// localStorage can fail in private mode or when storage quota is exhausted.
|
|||
} |
|||
} |
|||
|
|||
function getKeepalivePatchUrl(slug: string) { |
|||
const searchParams = new URLSearchParams({ |
|||
[PROXY_PATH_PARAM]: `/api/marriage/sections/${pathParam(slug)}/data/`, |
|||
}); |
|||
|
|||
return `/api/proxy?${searchParams.toString()}`; |
|||
} |
|||
|
|||
export function QuestionAnswersProvider({ |
|||
children, |
|||
questions, |
|||
slug, |
|||
}: QuestionAnswersProviderProps) { |
|||
const storageKey = useMemo(() => getQuestionAnswersStorageKey(slug), [slug]); |
|||
const [answers, setAnswers] = useState<QuestionAnswersByKey>({}); |
|||
const [hasPendingSync, setHasPendingSync] = useState(false); |
|||
const { isPending: isSaving, mutateAsync } = |
|||
useUpdateMarriageSectionDataMutation(slug); |
|||
const answersRef = useRef<QuestionAnswersByKey>({}); |
|||
const hasPendingSyncRef = useRef(false); |
|||
const questionsRef = useRef(questions); |
|||
const storageKeyRef = useRef(storageKey); |
|||
const slugRef = useRef(slug); |
|||
|
|||
useEffect(() => { |
|||
questionsRef.current = questions; |
|||
}, [questions]); |
|||
|
|||
useEffect(() => { |
|||
storageKeyRef.current = storageKey; |
|||
slugRef.current = slug; |
|||
|
|||
const stored = readStoredAnswers(storageKey, slug); |
|||
|
|||
answersRef.current = stored.answers; |
|||
hasPendingSyncRef.current = stored.pendingSync; |
|||
setAnswers(stored.answers); |
|||
setHasPendingSync(stored.pendingSync); |
|||
}, [slug, storageKey]); |
|||
|
|||
const getAnswerValue = useCallback( |
|||
(question: QuestionField, questionIndex: number) => |
|||
answers[getQuestionFieldKey(question, questionIndex)]?.value, |
|||
[answers], |
|||
); |
|||
|
|||
const setAnswerValue = useCallback( |
|||
( |
|||
question: QuestionField, |
|||
questionIndex: number, |
|||
value: MarriageFieldValue, |
|||
) => { |
|||
const field = createQuestionField(question, questionIndex, value); |
|||
|
|||
setAnswers((currentAnswers) => { |
|||
const nextAnswers = { |
|||
...currentAnswers, |
|||
[field.key]: field, |
|||
}; |
|||
|
|||
answersRef.current = nextAnswers; |
|||
hasPendingSyncRef.current = true; |
|||
writeStoredAnswers( |
|||
storageKeyRef.current, |
|||
slugRef.current, |
|||
questionsRef.current, |
|||
nextAnswers, |
|||
true, |
|||
); |
|||
|
|||
return nextAnswers; |
|||
}); |
|||
setHasPendingSync(true); |
|||
}, |
|||
[], |
|||
); |
|||
|
|||
const flushAnswers = useCallback( |
|||
async (options?: FlushAnswersOptions) => { |
|||
if (!hasPendingSyncRef.current && !options?.force) { |
|||
return; |
|||
} |
|||
|
|||
const payload = createPayload(answersRef.current, questionsRef.current); |
|||
|
|||
if (payload.fields.length === 0) { |
|||
return; |
|||
} |
|||
|
|||
await mutateAsync(payload); |
|||
|
|||
hasPendingSyncRef.current = false; |
|||
setHasPendingSync(false); |
|||
writeStoredAnswers( |
|||
storageKeyRef.current, |
|||
slugRef.current, |
|||
questionsRef.current, |
|||
answersRef.current, |
|||
false, |
|||
); |
|||
}, |
|||
[mutateAsync], |
|||
); |
|||
const flushAnswersRef = useRef(flushAnswers); |
|||
|
|||
useEffect(() => { |
|||
flushAnswersRef.current = flushAnswers; |
|||
}, [flushAnswers]); |
|||
|
|||
useEffect(() => { |
|||
const flushWithKeepalive = () => { |
|||
if (!hasPendingSyncRef.current) { |
|||
return; |
|||
} |
|||
|
|||
const payload = createPayload(answersRef.current, questionsRef.current); |
|||
|
|||
if (payload.fields.length === 0) { |
|||
return; |
|||
} |
|||
|
|||
fetch(getKeepalivePatchUrl(slugRef.current), { |
|||
body: JSON.stringify(payload), |
|||
credentials: "include", |
|||
headers: { |
|||
Accept: "application/json", |
|||
"Content-Type": "application/json", |
|||
}, |
|||
keepalive: true, |
|||
method: "PATCH", |
|||
}) |
|||
.then((response) => { |
|||
if (!response.ok) { |
|||
return; |
|||
} |
|||
|
|||
hasPendingSyncRef.current = false; |
|||
writeStoredAnswers( |
|||
storageKeyRef.current, |
|||
slugRef.current, |
|||
questionsRef.current, |
|||
answersRef.current, |
|||
false, |
|||
); |
|||
}) |
|||
.catch(() => { |
|||
// The local draft stays marked pending so a later exit can retry.
|
|||
}); |
|||
}; |
|||
|
|||
window.addEventListener("pagehide", flushWithKeepalive); |
|||
|
|||
return () => { |
|||
window.removeEventListener("pagehide", flushWithKeepalive); |
|||
void flushAnswersRef.current(); |
|||
}; |
|||
}, []); |
|||
|
|||
const contextValue = useMemo<QuestionAnswersContextValue>( |
|||
() => ({ |
|||
flushAnswers, |
|||
getAnswerValue, |
|||
hasPendingSync, |
|||
isSaving, |
|||
setAnswerValue, |
|||
}), |
|||
[flushAnswers, getAnswerValue, hasPendingSync, isSaving, setAnswerValue], |
|||
); |
|||
|
|||
return ( |
|||
<QuestionAnswersContext.Provider value={contextValue}> |
|||
{children} |
|||
</QuestionAnswersContext.Provider> |
|||
); |
|||
} |
|||
|
|||
export function useQuestionAnswers() { |
|||
const context = useContext(QuestionAnswersContext); |
|||
|
|||
if (!context) { |
|||
throw new Error( |
|||
"useQuestionAnswers must be used inside QuestionAnswersProvider", |
|||
); |
|||
} |
|||
|
|||
return context; |
|||
} |
|||
|
|||
export function useQuestionAnswer( |
|||
question: QuestionField, |
|||
questionIndex: number, |
|||
) { |
|||
const context = useContext(QuestionAnswersContext); |
|||
|
|||
return { |
|||
setValue: (value: MarriageFieldValue) => { |
|||
context?.setAnswerValue(question, questionIndex, value); |
|||
}, |
|||
value: context?.getAnswerValue(question, questionIndex), |
|||
}; |
|||
} |
|||
@ -1,11 +1,34 @@ |
|||
"use client"; |
|||
|
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
import QuestionTitle from "./question-title"; |
|||
|
|||
type QuestionDateProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
}; |
|||
|
|||
export function QuestionDate({ question }: QuestionDateProps) { |
|||
return <div data-question-type={question.type} />; |
|||
export function QuestionDate({ question, questionIndex }: QuestionDateProps) { |
|||
const { setValue, value } = useQuestionAnswer(question, questionIndex); |
|||
const dateValue = typeof value === "string" ? value : ""; |
|||
|
|||
return ( |
|||
<label data-question-type={question.type} className="block space-y-3"> |
|||
<QuestionTitle question={question} /> |
|||
<input |
|||
type="date" |
|||
required={question.required} |
|||
value={dateValue} |
|||
onChange={(event) => { |
|||
const nextValue = event.target.value; |
|||
|
|||
setValue(nextValue.length > 0 ? nextValue : null); |
|||
}} |
|||
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 text-[15px] text-[#181818] outline-none focus:border-[#F43F5E]" |
|||
/> |
|||
</label> |
|||
); |
|||
} |
|||
|
|||
export default QuestionDate; |
|||
@ -1,11 +1,45 @@ |
|||
"use client"; |
|||
|
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
import QuestionTitle from "./question-title"; |
|||
|
|||
type QuestionDropdownProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
}; |
|||
|
|||
export function QuestionDropdown({ question }: QuestionDropdownProps) { |
|||
return <div data-question-type={question.type} />; |
|||
export function QuestionDropdown({ |
|||
question, |
|||
questionIndex, |
|||
}: QuestionDropdownProps) { |
|||
const { setValue, value } = useQuestionAnswer(question, questionIndex); |
|||
const selectValue = typeof value === "string" ? value : ""; |
|||
|
|||
return ( |
|||
<label data-question-type={question.type} className="block space-y-3"> |
|||
<QuestionTitle question={question} /> |
|||
<select |
|||
required={question.required} |
|||
value={selectValue} |
|||
onChange={(event) => { |
|||
const nextValue = event.target.value; |
|||
|
|||
setValue(nextValue.length > 0 ? nextValue : null); |
|||
}} |
|||
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 text-[15px] text-[#181818] outline-none focus:border-[#F43F5E]" |
|||
> |
|||
<option value="" disabled> |
|||
{question.extras.placeHolder || "Select an option"} |
|||
</option> |
|||
{question.extras.options.map((option) => ( |
|||
<option key={option} value={option}> |
|||
{option} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</label> |
|||
); |
|||
} |
|||
|
|||
export default QuestionDropdown; |
|||
@ -0,0 +1,39 @@ |
|||
"use client"; |
|||
|
|||
import { useRouter } from "next/navigation"; |
|||
import { useState } from "react"; |
|||
import NavigationButton, { |
|||
type NavigationButtonProps, |
|||
} from "@/components/ui/navigation-button"; |
|||
import { useQuestionAnswers } from "./question-answer-storage"; |
|||
|
|||
export function QuestionExitNavigationButton(props: NavigationButtonProps) { |
|||
const router = useRouter(); |
|||
const { flushAnswers } = useQuestionAnswers(); |
|||
const [isLeaving, setIsLeaving] = useState(false); |
|||
|
|||
return ( |
|||
<NavigationButton |
|||
{...props} |
|||
disabled={props.disabled || isLeaving} |
|||
onClick={async (event) => { |
|||
props.onClick?.(event); |
|||
|
|||
if (event.defaultPrevented) { |
|||
return; |
|||
} |
|||
|
|||
event.preventDefault(); |
|||
setIsLeaving(true); |
|||
|
|||
try { |
|||
await flushAnswers({ force: true }); |
|||
} finally { |
|||
router.back(); |
|||
} |
|||
}} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
export default QuestionExitNavigationButton; |
|||
@ -1,11 +1,80 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import { useState } from "react"; |
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
import QuestionTitle from "./question-title"; |
|||
|
|||
type QuestionFileProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
}; |
|||
|
|||
export function QuestionFile({ question }: QuestionFileProps) { |
|||
return <div data-question-type={question.type} />; |
|||
export function QuestionFile({ question, questionIndex }: QuestionFileProps) { |
|||
const [selectedFileName, setSelectedFileName] = useState<string | null>(null); |
|||
const acceptedFiles = question.extras.options |
|||
.map((option) => option.replace(/^\./, "")) |
|||
.join(", "); |
|||
const { setValue } = useQuestionAnswer(question, questionIndex); |
|||
const uploadTmpMediaMutation = useUploadTmpMediaMutation({ |
|||
onSuccess: (response) => { |
|||
setValue(response.path); |
|||
}, |
|||
onError: () => { |
|||
setValue(null); |
|||
}, |
|||
}); |
|||
|
|||
function handleFileChange(files: FileList | null) { |
|||
const file = files?.[0]; |
|||
|
|||
if (!file) { |
|||
setSelectedFileName(null); |
|||
setValue(null); |
|||
return; |
|||
} |
|||
|
|||
setSelectedFileName(file.name); |
|||
uploadTmpMediaMutation.mutate(file); |
|||
} |
|||
|
|||
return ( |
|||
<label data-question-type={question.type} className="block space-y-3"> |
|||
<QuestionTitle question={question} /> |
|||
<span className="relative flex aspect-[727/330] min-h-[156px] w-full cursor-pointer flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#F26C85] hover:border-[#777777]"> |
|||
<input |
|||
type="file" |
|||
required={question.required} |
|||
accept={question.extras.options.join(",")} |
|||
disabled={uploadTmpMediaMutation.isPending} |
|||
onChange={(event) => handleFileChange(event.target.files)} |
|||
className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" |
|||
/> |
|||
<Image |
|||
src="/assets/images/Image.svg" |
|||
alt="Upload" |
|||
width={24} |
|||
height={24} |
|||
/> |
|||
<span className="mt-3 block text-sm leading-none font-normal text-[#111111]"> |
|||
{uploadTmpMediaMutation.isPending |
|||
? "uploading..." |
|||
: (selectedFileName ?? "uplaod certifacates")} |
|||
</span> |
|||
{uploadTmpMediaMutation.isError ? ( |
|||
<span className="mt-2 block text-[10px] leading-none font-bold text-[#D44747]"> |
|||
Upload failed. Please try again. |
|||
</span> |
|||
) : acceptedFiles ? ( |
|||
<span className="mt-2 block text-[10px] leading-none font-bold text-[#8B8B8B]"> |
|||
{acceptedFiles} |
|||
</span> |
|||
) : null} |
|||
</span> |
|||
</label> |
|||
); |
|||
} |
|||
|
|||
export default QuestionFile; |
|||
@ -1,11 +1,49 @@ |
|||
"use client"; |
|||
|
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
import QuestionTitle from "./question-title"; |
|||
|
|||
type QuestionNumberProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
}; |
|||
|
|||
export function QuestionNumber({ question }: QuestionNumberProps) { |
|||
return <div data-question-type={question.type} />; |
|||
export function QuestionNumber({ |
|||
question, |
|||
questionIndex, |
|||
}: QuestionNumberProps) { |
|||
const [min, max] = question.extras.range; |
|||
const { setValue, value } = useQuestionAnswer(question, questionIndex); |
|||
const inputValue = |
|||
typeof value === "number" || typeof value === "string" ? String(value) : ""; |
|||
|
|||
return ( |
|||
<label data-question-type={question.type} className="block space-y-3"> |
|||
<QuestionTitle question={question} /> |
|||
<input |
|||
type="number" |
|||
required={question.required} |
|||
min={min || undefined} |
|||
max={max || undefined} |
|||
placeholder={question.extras.placeHolder} |
|||
value={inputValue} |
|||
onChange={(event) => { |
|||
const nextValue = event.target.value; |
|||
|
|||
if (nextValue.length === 0) { |
|||
setValue(null); |
|||
return; |
|||
} |
|||
|
|||
const parsedValue = event.target.valueAsNumber; |
|||
|
|||
setValue(Number.isNaN(parsedValue) ? nextValue : parsedValue); |
|||
}} |
|||
className="h-[54px] w-full rounded-[15px] border border-[#E7D8D5] bg-white px-4 text-[15px] text-[#181818] outline-none placeholder:text-[#9D8F8C] focus:border-[#F43F5E]" |
|||
/> |
|||
</label> |
|||
); |
|||
} |
|||
|
|||
export default QuestionNumber; |
|||
@ -0,0 +1,66 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import type { ReactNode } from "react"; |
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
|
|||
type QuestionPhotoProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
description?: ReactNode; |
|||
}; |
|||
|
|||
function getFileInputValue(files: FileList | null) { |
|||
const fileNames = Array.from(files ?? []).map((file) => file.name); |
|||
|
|||
return fileNames.length > 0 ? fileNames.join(", ") : null; |
|||
} |
|||
|
|||
export function QuestionPhoto({ |
|||
question, |
|||
questionIndex, |
|||
description, |
|||
}: QuestionPhotoProps) { |
|||
const acceptedFiles = question.extras.options.join(","); |
|||
const descriptionContent = description ?? question.description; |
|||
const { setValue } = useQuestionAnswer(question, questionIndex); |
|||
|
|||
return ( |
|||
<label |
|||
data-question-type={question.type} |
|||
className="block cursor-pointer text-center" |
|||
> |
|||
<input |
|||
type="file" |
|||
required={question.required} |
|||
accept={acceptedFiles} |
|||
onChange={(event) => setValue(getFileInputValue(event.target.files))} |
|||
className="sr-only" |
|||
/> |
|||
<span className="flex w-full flex-col items-center"> |
|||
<Image |
|||
src="/assets/images/Frame 2095586679.svg" |
|||
alt="" |
|||
aria-hidden="true" |
|||
width={86} |
|||
height={92} |
|||
className="h-[92px] w-[86px]" |
|||
/> |
|||
<span className="mt-4 block text-xs leading-none font-semibold text-[#36363C]"> |
|||
{question.title} |
|||
{question.required ? ( |
|||
<span className="ml-1 text-[#F33D52]">*</span> |
|||
) : null} |
|||
</span> |
|||
{descriptionContent ? ( |
|||
<span className="mt-4 block max-w-[350px] text-xs leading-[1.35] font-semibold text-[#D44747]"> |
|||
{descriptionContent} |
|||
</span> |
|||
) : null} |
|||
</span> |
|||
</label> |
|||
); |
|||
} |
|||
|
|||
export default QuestionPhoto; |
|||
@ -0,0 +1,128 @@ |
|||
"use client"; |
|||
|
|||
import { |
|||
type ReactNode, |
|||
useCallback, |
|||
useEffect, |
|||
useRef, |
|||
useState, |
|||
} from "react"; |
|||
|
|||
type QuestionProgressTrackerProps = { |
|||
children: ReactNode; |
|||
total: number; |
|||
}; |
|||
|
|||
function isQuestionAnswered(question: Element) { |
|||
const inputs = Array.from( |
|||
question.querySelectorAll< |
|||
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement |
|||
>("input, select, textarea"), |
|||
); |
|||
|
|||
if (inputs.length === 0) { |
|||
return question.getAttribute("data-question-answered") === "true"; |
|||
} |
|||
|
|||
return inputs.some((input) => { |
|||
if (input instanceof HTMLInputElement) { |
|||
if (input.type === "checkbox" || input.type === "radio") { |
|||
return input.checked; |
|||
} |
|||
|
|||
if (input.type === "file") { |
|||
return input.files !== null && input.files.length > 0; |
|||
} |
|||
} |
|||
|
|||
return input.value.trim().length > 0; |
|||
}); |
|||
} |
|||
|
|||
export function QuestionProgressTracker({ |
|||
children, |
|||
total, |
|||
}: QuestionProgressTrackerProps) { |
|||
const containerRef = useRef<HTMLDivElement>(null); |
|||
const [answered, setAnswered] = useState(0); |
|||
const safeTotal = Math.max(total, 0); |
|||
const progress = safeTotal > 0 ? (answered / safeTotal) * 100 : 0; |
|||
|
|||
const updateProgress = useCallback(() => { |
|||
const container = containerRef.current; |
|||
|
|||
if (!container) { |
|||
return; |
|||
} |
|||
|
|||
const questions = Array.from( |
|||
container.querySelectorAll("[data-question-type]"), |
|||
); |
|||
const nextAnswered = questions.filter(isQuestionAnswered).length; |
|||
|
|||
setAnswered(Math.min(nextAnswered, safeTotal)); |
|||
}, [safeTotal]); |
|||
|
|||
useEffect(() => { |
|||
const container = containerRef.current; |
|||
|
|||
if (!container) { |
|||
return; |
|||
} |
|||
|
|||
const observer = new MutationObserver(updateProgress); |
|||
|
|||
observer.observe(container, { |
|||
attributeFilter: ["data-question-answered"], |
|||
attributes: true, |
|||
subtree: true, |
|||
}); |
|||
|
|||
updateProgress(); |
|||
|
|||
return () => observer.disconnect(); |
|||
}, [updateProgress]); |
|||
|
|||
return ( |
|||
<div |
|||
ref={containerRef} |
|||
className="flex flex-col" |
|||
onChange={updateProgress} |
|||
onInput={updateProgress} |
|||
> |
|||
<div className="mb-5 h-[68px]" aria-hidden="true" /> |
|||
<div className="fixed top-[100px] left-1/2 z-20 w-full max-w-[375px] -translate-x-1/2 px-[17px]"> |
|||
<div |
|||
aria-label={`Answered questions: ${answered} of ${safeTotal}`} |
|||
aria-valuemax={safeTotal} |
|||
aria-valuemin={0} |
|||
aria-valuenow={answered} |
|||
role="progressbar" |
|||
className="w-full rounded-none bg-[#F7F1F0] px-[9px] pt-[25px] pb-[16px]" |
|||
> |
|||
<div className="mb-[7px] flex items-center justify-between text-xs leading-none font-normal text-[#747474]"> |
|||
<span>fields to complete</span> |
|||
<span> |
|||
{answered} /{safeTotal} |
|||
</span> |
|||
</div> |
|||
<div className="relative h-[6px] rounded-full bg-[#D8D8D8]"> |
|||
<div |
|||
className={[ |
|||
"absolute top-1/2 h-[10px] -translate-y-1/2 rounded-full bg-[#F2465F] transition-[width] duration-200", |
|||
answered > 0 ? "min-w-[37px]" : "", |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
style={{ width: `${progress}%` }} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default QuestionProgressTracker; |
|||
@ -1,11 +1,65 @@ |
|||
"use client"; |
|||
|
|||
import { useId } from "react"; |
|||
|
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
import QuestionTitle from "./question-title"; |
|||
|
|||
type QuestionRadioProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
}; |
|||
|
|||
export function QuestionRadio({ question }: QuestionRadioProps) { |
|||
return <div data-question-type={question.type} />; |
|||
export function QuestionRadio({ question, questionIndex }: QuestionRadioProps) { |
|||
const groupId = useId(); |
|||
const options = question.extras.options; |
|||
const { setValue, value } = useQuestionAnswer(question, questionIndex); |
|||
const selectedOption = typeof value === "string" ? value : (options[0] ?? ""); |
|||
|
|||
if (options.length === 0) { |
|||
return null; |
|||
} |
|||
|
|||
return ( |
|||
<fieldset data-question-type={question.type} className="space-y-7"> |
|||
<legend> |
|||
<QuestionTitle question={question} /> |
|||
</legend> |
|||
|
|||
<div className="flex flex-wrap gap-x-5 gap-y-4"> |
|||
{options.map((option) => { |
|||
const optionId = `${groupId}-${option}`; |
|||
const isSelected = selectedOption === option; |
|||
|
|||
return ( |
|||
<label |
|||
key={option} |
|||
htmlFor={optionId} |
|||
className={[ |
|||
"cursor-pointer rounded-[10px] px-3.5 py-2", |
|||
"text-center leading-none font-semibold transition-colors", |
|||
isSelected |
|||
? "bg-[#F0445B] text-white" |
|||
: "bg-[#DBDBDB] text-[#181818]", |
|||
].join(" ")} |
|||
> |
|||
<input |
|||
id={optionId} |
|||
className="sr-only" |
|||
type="radio" |
|||
name={groupId} |
|||
value={option} |
|||
checked={isSelected} |
|||
onChange={() => setValue(option)} |
|||
/> |
|||
{option} |
|||
</label> |
|||
); |
|||
})} |
|||
</div> |
|||
</fieldset> |
|||
); |
|||
} |
|||
|
|||
export default QuestionRadio; |
|||
@ -0,0 +1,70 @@ |
|||
"use client"; |
|||
|
|||
import Image from "next/image"; |
|||
import { useRouter } from "next/navigation"; |
|||
import type { ReactNode } from "react"; |
|||
import { useCallback, useState } from "react"; |
|||
import Button from "@/components/ui/button"; |
|||
import { useQuestionAnswers } from "./question-answer-storage"; |
|||
import QuestionProgressTracker from "./question-progress-tracker"; |
|||
import QuestionSnapList from "./question-snap-list"; |
|||
|
|||
type QuestionSectionFlowProps = { |
|||
children: ReactNode; |
|||
continueLabel: string; |
|||
exitHref: string; |
|||
total: number; |
|||
}; |
|||
|
|||
export function QuestionSectionFlow({ |
|||
children, |
|||
continueLabel, |
|||
exitHref, |
|||
total, |
|||
}: QuestionSectionFlowProps) { |
|||
const router = useRouter(); |
|||
const { flushAnswers, isSaving } = useQuestionAnswers(); |
|||
const [isLeaving, setIsLeaving] = useState(false); |
|||
const handleQuestionExit = useCallback(() => { |
|||
void flushAnswers({ force: true }); |
|||
}, [flushAnswers]); |
|||
const handleContinue = useCallback(async () => { |
|||
setIsLeaving(true); |
|||
|
|||
try { |
|||
await flushAnswers({ force: true }); |
|||
} finally { |
|||
router.replace(exitHref); |
|||
} |
|||
}, [exitHref, flushAnswers, router]); |
|||
|
|||
return ( |
|||
<QuestionProgressTracker total={total}> |
|||
<QuestionSnapList |
|||
firstQuestionHint={ |
|||
<Image |
|||
src="/assets/images/Frame 1597880476.svg" |
|||
alt="" |
|||
aria-hidden="true" |
|||
width={31} |
|||
height={31} |
|||
/> |
|||
} |
|||
footer={ |
|||
<Button |
|||
className="rounded-[14px] from-[#F29BAB] to-[#E88597] py-[16px] shadow-[0_16px_30px_rgba(232,133,151,0.34)]" |
|||
disabled={isSaving || isLeaving} |
|||
onClick={() => void handleContinue()} |
|||
> |
|||
{continueLabel} |
|||
</Button> |
|||
} |
|||
onQuestionExit={handleQuestionExit} |
|||
> |
|||
{children} |
|||
</QuestionSnapList> |
|||
</QuestionProgressTracker> |
|||
); |
|||
} |
|||
|
|||
export default QuestionSectionFlow; |
|||
@ -1,11 +1,105 @@ |
|||
"use client"; |
|||
|
|||
import { useLayoutEffect, useRef, useState } from "react"; |
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
import QuestionTitle from "./question-title"; |
|||
|
|||
type QuestionSliderProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
}; |
|||
|
|||
export function QuestionSlider({ question }: QuestionSliderProps) { |
|||
return <div data-question-type={question.type} />; |
|||
export function QuestionSlider({ |
|||
question, |
|||
questionIndex, |
|||
}: QuestionSliderProps) { |
|||
const [min, max] = question.extras.range; |
|||
const initialValue = Math.round((min + max) / 2); |
|||
const { setValue, value: storedValue } = useQuestionAnswer( |
|||
question, |
|||
questionIndex, |
|||
); |
|||
const value = typeof storedValue === "number" ? storedValue : initialValue; |
|||
const progress = max === min ? 0 : ((value - min) / (max - min)) * 100; |
|||
const sliderWrapperRef = useRef<HTMLDivElement>(null); |
|||
const sliderRef = useRef<HTMLInputElement>(null); |
|||
const thumbWidth = 18; |
|||
const bubbleHalfWidth = 20; |
|||
const [bubblePosition, setBubblePosition] = useState(bubbleHalfWidth); |
|||
const steps = Array.from( |
|||
{ length: max - min + 1 }, |
|||
(_, index) => min + index, |
|||
); |
|||
|
|||
useLayoutEffect(() => { |
|||
const wrapper = sliderWrapperRef.current; |
|||
const slider = sliderRef.current; |
|||
|
|||
if (!wrapper || !slider) { |
|||
return; |
|||
} |
|||
|
|||
const updateBubblePosition = () => { |
|||
const wrapperRect = wrapper.getBoundingClientRect(); |
|||
const sliderRect = slider.getBoundingClientRect(); |
|||
const sliderLeft = sliderRect.left - wrapperRect.left; |
|||
const thumbCenter = |
|||
sliderLeft + |
|||
thumbWidth / 2 + |
|||
(progress / 100) * (sliderRect.width - thumbWidth); |
|||
|
|||
setBubblePosition( |
|||
Math.min( |
|||
Math.max(thumbCenter, bubbleHalfWidth), |
|||
wrapperRect.width - bubbleHalfWidth, |
|||
), |
|||
); |
|||
}; |
|||
|
|||
updateBubblePosition(); |
|||
|
|||
const resizeObserver = new ResizeObserver(updateBubblePosition); |
|||
resizeObserver.observe(wrapper); |
|||
resizeObserver.observe(slider); |
|||
|
|||
return () => resizeObserver.disconnect(); |
|||
}, [progress]); |
|||
|
|||
return ( |
|||
<label data-question-type={question.type} className="block space-y-6"> |
|||
<QuestionTitle question={question} /> |
|||
<div className="pt-9"> |
|||
<div ref={sliderWrapperRef} className="relative"> |
|||
<div |
|||
className="-translate-x-1/2 absolute -top-6.5 z-10 flex items-center justify-center rounded-[7px] bg-[#F43F5E] px-2.5 font-semibold text-white shadow-sm after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F43F5E] after:content-['']" |
|||
style={{ left: `${bubblePosition}px` }} |
|||
> |
|||
{value} |
|||
</div> |
|||
<input |
|||
ref={sliderRef} |
|||
type="range" |
|||
required={question.required} |
|||
min={min} |
|||
max={max} |
|||
step={1} |
|||
value={value} |
|||
onChange={(event) => setValue(Number(event.target.value))} |
|||
className="question-slider-range w-full" |
|||
style={{ |
|||
background: `linear-gradient(to right, #F43F5E 0%, #F43F5E ${progress}%, #FAD1D8 ${progress}%, #FAD1D8 100%)`, |
|||
}} |
|||
/> |
|||
</div> |
|||
<div className="mt-2 grid grid-flow-col auto-cols-fr text-center text-[8.5px] text-[#B7B7B7]"> |
|||
{steps.map((step) => ( |
|||
<span key={step}>{step}</span> |
|||
))} |
|||
</div> |
|||
</div> |
|||
</label> |
|||
); |
|||
} |
|||
|
|||
export default QuestionSlider; |
|||
@ -0,0 +1,199 @@ |
|||
"use client"; |
|||
|
|||
import { |
|||
Children, |
|||
isValidElement, |
|||
type ReactNode, |
|||
useCallback, |
|||
useEffect, |
|||
useRef, |
|||
useState, |
|||
} from "react"; |
|||
|
|||
const WHEEL_GESTURE_IDLE_MS = 320; |
|||
const TOUCH_MIN_DISTANCE = 8; |
|||
|
|||
type QuestionSnapListProps = { |
|||
children: ReactNode; |
|||
className?: string; |
|||
footer?: ReactNode; |
|||
firstQuestionHint?: ReactNode; |
|||
onQuestionExit?: (currentIndex: number, nextIndex: number) => void; |
|||
}; |
|||
|
|||
export function QuestionSnapList({ |
|||
children, |
|||
className, |
|||
footer, |
|||
firstQuestionHint, |
|||
onQuestionExit, |
|||
}: QuestionSnapListProps) { |
|||
const questions = Children.toArray(children); |
|||
const wheelLockedRef = useRef(false); |
|||
const wheelUnlockTimeoutRef = useRef<number | null>(null); |
|||
const touchStartYRef = useRef<number | null>(null); |
|||
const [activeIndex, setActiveIndex] = useState(0); |
|||
|
|||
const stepQuestion = useCallback( |
|||
(direction: 1 | -1) => { |
|||
const nextIndex = Math.max( |
|||
0, |
|||
Math.min(questions.length - 1, activeIndex + direction), |
|||
); |
|||
|
|||
if (nextIndex === activeIndex) { |
|||
return; |
|||
} |
|||
|
|||
onQuestionExit?.(activeIndex, nextIndex); |
|||
setActiveIndex(nextIndex); |
|||
}, |
|||
[activeIndex, onQuestionExit, questions.length], |
|||
); |
|||
|
|||
const scheduleWheelUnlock = useCallback(() => { |
|||
if (wheelUnlockTimeoutRef.current !== null) { |
|||
window.clearTimeout(wheelUnlockTimeoutRef.current); |
|||
} |
|||
|
|||
wheelUnlockTimeoutRef.current = window.setTimeout(() => { |
|||
wheelLockedRef.current = false; |
|||
wheelUnlockTimeoutRef.current = null; |
|||
}, WHEEL_GESTURE_IDLE_MS); |
|||
}, []); |
|||
|
|||
useEffect(() => { |
|||
return () => { |
|||
if (wheelUnlockTimeoutRef.current !== null) { |
|||
window.clearTimeout(wheelUnlockTimeoutRef.current); |
|||
} |
|||
}; |
|||
}, []); |
|||
|
|||
const handleWheel = useCallback( |
|||
(event: React.WheelEvent<HTMLElement>) => { |
|||
const delta = event.deltaY || event.deltaX; |
|||
|
|||
if (delta === 0 || questions.length < 2) { |
|||
return; |
|||
} |
|||
|
|||
event.preventDefault(); |
|||
|
|||
if (!wheelLockedRef.current) { |
|||
wheelLockedRef.current = true; |
|||
stepQuestion(delta > 0 ? 1 : -1); |
|||
} |
|||
|
|||
scheduleWheelUnlock(); |
|||
}, |
|||
[questions.length, scheduleWheelUnlock, stepQuestion], |
|||
); |
|||
|
|||
const handleTouchStart = useCallback( |
|||
(event: React.TouchEvent<HTMLElement>) => { |
|||
touchStartYRef.current = event.touches[0]?.clientY ?? null; |
|||
}, |
|||
[], |
|||
); |
|||
|
|||
const handleTouchEnd = useCallback( |
|||
(event: React.TouchEvent<HTMLElement>) => { |
|||
const startY = touchStartYRef.current; |
|||
const endY = event.changedTouches[0]?.clientY; |
|||
|
|||
touchStartYRef.current = null; |
|||
|
|||
if (startY === null || endY === undefined || questions.length < 2) { |
|||
return; |
|||
} |
|||
|
|||
const distance = startY - endY; |
|||
|
|||
if (Math.abs(distance) < TOUCH_MIN_DISTANCE) { |
|||
return; |
|||
} |
|||
|
|||
stepQuestion(distance > 0 ? 1 : -1); |
|||
}, |
|||
[questions.length, stepQuestion], |
|||
); |
|||
|
|||
const handleTouchMove = useCallback( |
|||
(event: React.TouchEvent<HTMLElement>) => { |
|||
event.preventDefault(); |
|||
}, |
|||
[], |
|||
); |
|||
|
|||
if (questions.length === 0) { |
|||
return null; |
|||
} |
|||
|
|||
return ( |
|||
<section |
|||
aria-label="Questions" |
|||
className={[ |
|||
"relative touch-none overflow-hidden focus-visible:outline-none", |
|||
"h-[calc(100svh-150px)] min-h-[430px]", |
|||
className, |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
onTouchEnd={handleTouchEnd} |
|||
onTouchMove={handleTouchMove} |
|||
onTouchStart={handleTouchStart} |
|||
onWheel={handleWheel} |
|||
> |
|||
{questions.map((question, index) => { |
|||
const offset = index - activeIndex; |
|||
const isActive = offset === 0; |
|||
const isAdjacent = Math.abs(offset) === 1; |
|||
const showFooter = Boolean( |
|||
footer && isActive && index === questions.length - 1, |
|||
); |
|||
const questionKey = |
|||
isValidElement(question) && question.key !== null |
|||
? question.key |
|||
: String(question); |
|||
|
|||
return ( |
|||
<div |
|||
key={questionKey} |
|||
aria-current={isActive ? "step" : undefined} |
|||
aria-hidden={isActive ? undefined : true} |
|||
inert={isActive ? undefined : true} |
|||
className={[ |
|||
"absolute inset-0 flex w-full items-center transition-all duration-300 ease-out", |
|||
isActive |
|||
? "pointer-events-auto z-10 scale-100 opacity-100" |
|||
: "pointer-events-none z-0 scale-[0.96]", |
|||
!isActive && isAdjacent ? "opacity-20" : "", |
|||
!isActive && !isAdjacent ? "opacity-0" : "", |
|||
].join(" ")} |
|||
style={{ transform: `translateY(${offset * 50}%)` }} |
|||
> |
|||
<div className="w-full"> |
|||
{question} |
|||
{showFooter ? <div className="mt-6">{footer}</div> : null} |
|||
</div> |
|||
</div> |
|||
); |
|||
})} |
|||
{firstQuestionHint ? ( |
|||
<div |
|||
aria-hidden="true" |
|||
className={[ |
|||
"pointer-events-none absolute bottom-6 left-1/2 -translate-x-1/2", |
|||
"transition-opacity duration-500 motion-safe:animate-bounce", |
|||
activeIndex === 0 ? "opacity-100" : "opacity-0", |
|||
].join(" ")} |
|||
> |
|||
{firstQuestionHint} |
|||
</div> |
|||
) : null} |
|||
</section> |
|||
); |
|||
} |
|||
|
|||
export default QuestionSnapList; |
|||
@ -1,11 +1,46 @@ |
|||
"use client"; |
|||
|
|||
import type { QuestionField } from "@/data/question-data"; |
|||
import { useQuestionAnswer } from "./question-answer-storage"; |
|||
import QuestionTitle from "./question-title"; |
|||
|
|||
type QuestionTextProps = { |
|||
question: QuestionField; |
|||
questionIndex: number; |
|||
description?: string; |
|||
heightClassName?: string; |
|||
}; |
|||
|
|||
export function QuestionText({ question }: QuestionTextProps) { |
|||
return <div data-question-type={question.type} />; |
|||
export function QuestionText({ |
|||
question, |
|||
questionIndex, |
|||
description, |
|||
heightClassName = "min-h-[116px]", |
|||
}: QuestionTextProps) { |
|||
const { setValue, value } = useQuestionAnswer(question, questionIndex); |
|||
const textValue = typeof value === "string" ? value : ""; |
|||
|
|||
return ( |
|||
<label data-question-type={question.type} className="block space-y-3"> |
|||
<QuestionTitle question={question} /> |
|||
<textarea |
|||
required={question.required} |
|||
placeholder={question.extras.placeHolder} |
|||
value={textValue} |
|||
onChange={(event) => { |
|||
const nextValue = event.target.value; |
|||
|
|||
setValue(nextValue.length > 0 ? nextValue : null); |
|||
}} |
|||
className={`${heightClassName} w-full resize-none rounded-[15px] border border-[#8B8B8B] bg-white px-4 py-3 text-[#181818] outline-none placeholder:text-[#8B8B8B] focus:border-[#F43F5E]`} |
|||
/> |
|||
{description ? ( |
|||
<span className="block text-[10px] font-semibold text-[#747474]"> |
|||
{description} |
|||
</span> |
|||
) : null} |
|||
</label> |
|||
); |
|||
} |
|||
|
|||
export default QuestionText; |
|||
@ -0,0 +1,44 @@ |
|||
import { IoEyeOff } from "react-icons/io5"; |
|||
import type { QuestionField } from "@/data/question-data"; |
|||
|
|||
type QuestionTitleProps = { |
|||
question: QuestionField; |
|||
className?: string; |
|||
}; |
|||
|
|||
export function QuestionTitle({ question, className }: QuestionTitleProps) { |
|||
return ( |
|||
<span className="block"> |
|||
{question.private ? ( |
|||
<span className="mb-3 flex min-h-[42px] w-full items-center gap-3 rounded-[22px] bg-[#F6D7D8] px-5 py-2 text-[14px] leading-tight font-semibold text-[#DF4146]"> |
|||
<IoEyeOff |
|||
aria-hidden="true" |
|||
className="shrink-0 text-[24px] text-[#DF4146]" |
|||
/> |
|||
<span>Private Field (Visible to Habib Marriage advisors only)</span> |
|||
</span> |
|||
) : null} |
|||
|
|||
<span |
|||
className={[ |
|||
"block text-[12px] leading-tight font-semibold text-[#181818]", |
|||
className, |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
> |
|||
{question.title} |
|||
{question.description ? ( |
|||
<span className="ml-1 text-[10px] font-semibold text-[#747474]"> |
|||
({question.description}) |
|||
</span> |
|||
) : null} |
|||
{question.required ? ( |
|||
<span className="ml-1 text-[14px] text-[#F33D52]">*</span> |
|||
) : null} |
|||
</span> |
|||
</span> |
|||
); |
|||
} |
|||
|
|||
export default QuestionTitle; |
|||
@ -0,0 +1,82 @@ |
|||
"use client"; |
|||
|
|||
import { IoAlert } from "react-icons/io5"; |
|||
import { getQuestionListItems } from "@/data/question-data"; |
|||
import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; |
|||
import { useI18n } from "@/i18n/provider"; |
|||
|
|||
type RequiredStep = { |
|||
slug: string; |
|||
required: boolean; |
|||
progress: number; |
|||
}; |
|||
|
|||
function getRequiredStepStats(steps: RequiredStep[]) { |
|||
const requiredSteps = steps.filter((step) => step.required); |
|||
const completedSteps = requiredSteps.filter((step) => step.progress >= 100); |
|||
|
|||
return { |
|||
completed: completedSteps.length, |
|||
total: requiredSteps.length, |
|||
}; |
|||
} |
|||
|
|||
export default function RequiredStepsCard() { |
|||
const { dictionary: t, locale } = useI18n(); |
|||
const { data: sections } = useMarriageSectionsQuery(); |
|||
const fallbackRequiredSteps: RequiredStep[] = getQuestionListItems( |
|||
locale, |
|||
).map((item) => ({ |
|||
slug: item.slug, |
|||
required: Boolean(item.required), |
|||
progress: item.progress, |
|||
})); |
|||
const steps = |
|||
sections?.map((section) => ({ |
|||
slug: section.slug, |
|||
required: section.is_required, |
|||
progress: section.completion_percent, |
|||
})) ?? fallbackRequiredSteps; |
|||
const { completed, total } = getRequiredStepStats(steps); |
|||
const completion = total > 0 ? Math.round((completed / total) * 100) : 0; |
|||
|
|||
return ( |
|||
<section |
|||
aria-label="Required steps progress" |
|||
className="rounded-[15px] bg-[#40506A] p-4 text-white shadow-[0_18px_34px_rgba(38,52,73,0.16)]" |
|||
> |
|||
<div className="flex items-center justify-between gap-5"> |
|||
<div className="min-w-0 flex-1"> |
|||
<div className="flex items-center gap-3"> |
|||
<span className="flex h-[24px] w-[24px] shrink-0 items-center justify-center rounded-full bg-white text-[#40506A]"> |
|||
<IoAlert aria-hidden="true" className="text-[16px]" /> |
|||
</span> |
|||
<h2 className="leading-none font-bold tracking-[-0.02em]"> |
|||
{t.questions.requiredSteps} |
|||
</h2> |
|||
</div> |
|||
|
|||
<p className="mt-2.5 max-w-[220px] text-[10px] leading-[1.18] font-semibold text-white/70"> |
|||
{t.questions.requiredStepsDescription} |
|||
</p> |
|||
</div> |
|||
|
|||
<div |
|||
role="img" |
|||
aria-label={t.questions.requiredStepsProgress |
|||
.replace("{completed}", String(completed)) |
|||
.replace("{total}", String(total))} |
|||
className="relative flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-full" |
|||
style={{ |
|||
background: `conic-gradient(#FFFFFF ${completion}%,rgba(255,255,255,0.24) 0)`, |
|||
}} |
|||
> |
|||
<div className="absolute inset-[6px] rounded-full bg-[#40506A]" /> |
|||
<span className="relative leading-none font-bold tracking-[-0.02em]"> |
|||
{completed}/{total} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
import type { SliderSlideProps } from "@/components/sliders/slider-slide"; |
|||
|
|||
const NOTICE_ITEMS = [ |
|||
"Your information is kept strictly confidential.", |
|||
"Your details are only used for the matching process.", |
|||
"Nothing is shared without your consent.", |
|||
"You are always in control of what happens next.", |
|||
"Contact details are shared only after your approval.", |
|||
"We provide a safe and respectful environment at every step.", |
|||
]; |
|||
|
|||
export function SliderSlideFive({ index }: SliderSlideProps) { |
|||
return ( |
|||
<section |
|||
aria-label={`Slide ${index + 1}`} |
|||
className="flex h-full min-h-0 w-full shrink-0 flex-col" |
|||
> |
|||
<div className="min-h-0 flex-1 overflow-y-auto pb-28"> |
|||
<div className="text-center"> |
|||
<p className="text-[#747474] text-xs font-semibold">Submit Process</p> |
|||
<p className="text-[#111111] font-bold">5/5 Final Notice</p> |
|||
</div> |
|||
|
|||
<div className="mt-6 space-y-5"> |
|||
<p className="text-[#111111] text-[13px] 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"> |
|||
{NOTICE_ITEMS.map((item) => ( |
|||
<li key={item}>{item}</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
); |
|||
} |
|||
@ -0,0 +1,212 @@ |
|||
"use client"; |
|||
|
|||
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; |
|||
|
|||
export type CallResultSheetProps = Omit< |
|||
HTMLAttributes<HTMLDivElement>, |
|||
"title" |
|||
> & { |
|||
closeOnOutside?: boolean; |
|||
onClose?: () => void; |
|||
onSubmit?: (value: string) => void; |
|||
onOtherReasonsClick?: () => void; |
|||
}; |
|||
|
|||
export function CallResultSheet({ |
|||
closeOnOutside = true, |
|||
onClose, |
|||
onSubmit, |
|||
onOtherReasonsClick, |
|||
className, |
|||
...props |
|||
}: CallResultSheetProps) { |
|||
const { dictionary: t } = useI18n(); |
|||
const callResultOptions = t.sheets.callOptions; |
|||
const groupId = useId(); |
|||
const [isVisible, setIsVisible] = useState(true); |
|||
const [isEntering, setIsEntering] = useState(true); |
|||
const [isClosing, setIsClosing] = useState(false); |
|||
const [selectedReason, setSelectedReason] = useState<string>( |
|||
callResultOptions[0], |
|||
); |
|||
|
|||
const closeSheet = () => { |
|||
if (isClosing) { |
|||
return; |
|||
} |
|||
|
|||
setIsClosing(true); |
|||
}; |
|||
|
|||
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.callResult} |
|||
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-[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", |
|||
isClosing || isEntering ? "translate-y-full" : "translate-y-0", |
|||
className, |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
> |
|||
<div className="mx-auto flex flex-col items-center"> |
|||
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]"> |
|||
{t.sheets.callResult} |
|||
</h2> |
|||
|
|||
<div |
|||
className="mt-5 w-full" |
|||
role="radiogroup" |
|||
aria-labelledby={groupId} |
|||
> |
|||
<span id={groupId} className="sr-only"> |
|||
{t.sheets.selectCallResult} |
|||
</span> |
|||
|
|||
<div className="flex flex-col gap-[14px]"> |
|||
{callResultOptions.map((option) => { |
|||
const checked = selectedReason === option; |
|||
|
|||
return ( |
|||
<label |
|||
key={option} |
|||
className="flex cursor-pointer items-center gap-3 rounded-[14px] bg-[#ECECEC] px-[14px] py-[18px] text-left" |
|||
> |
|||
<input |
|||
checked={checked} |
|||
className="sr-only" |
|||
name="call-result" |
|||
type="radio" |
|||
value={option} |
|||
onChange={() => { |
|||
setSelectedReason(option); |
|||
|
|||
if ( |
|||
option === |
|||
callResultOptions[callResultOptions.length - 1] |
|||
) { |
|||
onOtherReasonsClick?.(); |
|||
closeSheet(); |
|||
} |
|||
}} |
|||
/> |
|||
<span |
|||
aria-hidden="true" |
|||
className={[ |
|||
"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border", |
|||
checked ? "border-[#F0445B]" : "border-[#9E9E9E]", |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
> |
|||
{checked ? ( |
|||
<span className="h-3 w-3 rounded-full bg-[#F0445B]" /> |
|||
) : null} |
|||
</span> |
|||
<span className="text-[16px] leading-none font-semibold text-[#262626]"> |
|||
{option} |
|||
</span> |
|||
</label> |
|||
); |
|||
})} |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="mt-8 w-full"> |
|||
<Button |
|||
className="rounded-[14px] py-[18px] shadow-none" |
|||
onClick={() => { |
|||
onSubmit?.(selectedReason); |
|||
closeSheet(); |
|||
}} |
|||
> |
|||
{t.common.submit} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default CallResultSheet; |
|||
@ -0,0 +1,159 @@ |
|||
"use client"; |
|||
|
|||
import type { HTMLAttributes } from "react"; |
|||
import { useEffect, useState } from "react"; |
|||
import Button from "@/components/ui/button"; |
|||
import { useI18n } from "@/i18n/provider"; |
|||
|
|||
const EXIT_ANIMATION_MS = 220; |
|||
|
|||
export type DismissReasonSheetProps = Omit< |
|||
HTMLAttributes<HTMLDivElement>, |
|||
"title" |
|||
> & { |
|||
closeOnOutside?: boolean; |
|||
onClose?: () => void; |
|||
onSubmit?: (value: string) => void; |
|||
}; |
|||
|
|||
export function DismissReasonSheet({ |
|||
closeOnOutside = true, |
|||
onClose, |
|||
onSubmit, |
|||
className, |
|||
...props |
|||
}: DismissReasonSheetProps) { |
|||
const { dictionary: t } = useI18n(); |
|||
const [isVisible, setIsVisible] = useState(true); |
|||
const [isEntering, setIsEntering] = useState(true); |
|||
const [isClosing, setIsClosing] = useState(false); |
|||
const [reasonText, setReasonText] = useState(""); |
|||
|
|||
const closeSheet = () => { |
|||
if (isClosing) { |
|||
return; |
|||
} |
|||
|
|||
setIsClosing(true); |
|||
}; |
|||
|
|||
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.dismissReasons} |
|||
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-[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", |
|||
isClosing || isEntering ? "translate-y-full" : "translate-y-0", |
|||
className, |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
> |
|||
<div className="mx-auto flex flex-col items-center"> |
|||
<h2 className="text-[18px] leading-[1.2] font-bold tracking-[-0.03em] text-[#171717]"> |
|||
{t.sheets.dismissReasons} |
|||
</h2> |
|||
|
|||
<p className="mt-4 w-full text-left text-[14px] leading-[1.45] text-[#2C2C2C]"> |
|||
{t.sheets.dismissDescription} |
|||
</p> |
|||
|
|||
<textarea |
|||
className="mt-4 min-h-[356px] w-full resize-none rounded-[16px] border border-[#454545] bg-transparent px-3 py-3 text-[16px] text-[#171717] outline-none placeholder:text-[#AAAAAA]" |
|||
placeholder={t.sheets.dismissPlaceholder} |
|||
value={reasonText} |
|||
onChange={(event) => setReasonText(event.target.value)} |
|||
/> |
|||
|
|||
<div className="mt-[14px] w-full"> |
|||
<Button |
|||
className="rounded-[14px] py-[18px] shadow-none" |
|||
onClick={() => { |
|||
onSubmit?.(reasonText); |
|||
closeSheet(); |
|||
}} |
|||
> |
|||
{t.common.submit} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default DismissReasonSheet; |
|||
@ -0,0 +1,37 @@ |
|||
"use client"; |
|||
|
|||
import Link from "next/link"; |
|||
import { usePathname } from "next/navigation"; |
|||
import { localeLabels, locales, localizePath } from "@/i18n/config"; |
|||
import { useI18n } from "@/i18n/provider"; |
|||
|
|||
export default function LanguageSwitcher() { |
|||
const pathname = usePathname(); |
|||
const { locale } = useI18n(); |
|||
|
|||
return ( |
|||
<nav aria-label="Language" className="fixed right-3 top-3 z-40"> |
|||
<div className="flex overflow-hidden rounded-full border border-[#F2D4DA] bg-white/90 text-[11px] font-semibold shadow-[0_8px_22px_rgba(15,23,42,0.08)] backdrop-blur"> |
|||
{locales.map((nextLocale) => { |
|||
const isActive = locale === nextLocale; |
|||
|
|||
return ( |
|||
<Link |
|||
key={nextLocale} |
|||
href={localizePath(pathname, nextLocale)} |
|||
aria-current={isActive ? "page" : undefined} |
|||
className={[ |
|||
"px-3 py-2 leading-none", |
|||
isActive ? "bg-[#F0445B] text-white" : "text-[#4B5563]", |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
> |
|||
{localeLabels[nextLocale]} |
|||
</Link> |
|||
); |
|||
})} |
|||
</div> |
|||
</nav> |
|||
); |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
import type { ReactNode } from "react"; |
|||
|
|||
type StickyHeaderProps = { |
|||
children: ReactNode; |
|||
className?: string; |
|||
}; |
|||
|
|||
export default function StickyHeader({ |
|||
children, |
|||
className, |
|||
}: StickyHeaderProps) { |
|||
return ( |
|||
<header |
|||
className={[ |
|||
"sticky top-0 z-30 rounded-b-[15px] bg-[linear-gradient(135deg,#E03950_0%,#FE6F82_100%)] px-[17px] pt-7 pb-5", |
|||
className, |
|||
] |
|||
.filter(Boolean) |
|||
.join(" ")} |
|||
> |
|||
{children} |
|||
</header> |
|||
); |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
import type { |
|||
UseMutationOptions, |
|||
UseQueryOptions, |
|||
} from "@tanstack/react-query"; |
|||
import type { ApiError } from "./types"; |
|||
|
|||
export type QueryOptions<TQueryFnData, TData = TQueryFnData> = Omit< |
|||
UseQueryOptions<TQueryFnData, ApiError, TData>, |
|||
"queryFn" | "queryKey" |
|||
>; |
|||
|
|||
export type MutationOptions< |
|||
TData, |
|||
TVariables = void, |
|||
TOnMutateResult = unknown, |
|||
> = Omit< |
|||
UseMutationOptions<TData, ApiError, TVariables, TOnMutateResult>, |
|||
"mutationFn" |
|||
>; |
|||
@ -0,0 +1,3 @@ |
|||
import type { CaseId } from "./types"; |
|||
|
|||
export const pathParam = (value: CaseId) => encodeURIComponent(String(value)); |
|||
@ -0,0 +1,16 @@ |
|||
import type { CaseId } from "./types"; |
|||
|
|||
export const marriageQueryKeys = { |
|||
all: ["marriage"] as const, |
|||
contactInfo: (caseId: CaseId | "") => |
|||
[ |
|||
...marriageQueryKeys.all, |
|||
"cases", |
|||
String(caseId), |
|||
"contact-info", |
|||
] as const, |
|||
profile: () => [...marriageQueryKeys.all, "profile"] as const, |
|||
sectionData: (slug: string) => |
|||
[...marriageQueryKeys.sections(), slug, "data"] as const, |
|||
sections: () => [...marriageQueryKeys.all, "sections"] as const, |
|||
}; |
|||
@ -0,0 +1,158 @@ |
|||
import type { AxiosError } from "axios"; |
|||
|
|||
export type ApiError = AxiosError<unknown>; |
|||
|
|||
export type CaseId = number | string; |
|||
|
|||
export type MarriageGender = "male" | "female"; |
|||
|
|||
export type MarriageProfileStatus = |
|||
| "pending_onboarding" |
|||
| "pending_info" |
|||
| "waiting" |
|||
| "in_case" |
|||
| "suspended" |
|||
| "matched"; |
|||
|
|||
export type MarriageCaseStatus = |
|||
| "introduced" |
|||
| "male_accepted" |
|||
| "male_rejected" |
|||
| "female_rejected" |
|||
| "payment_pending" |
|||
| "payment_done" |
|||
| "finalized" |
|||
| "dismissed"; |
|||
|
|||
export type MarriageFieldValue = string | number | boolean | null; |
|||
|
|||
export type MarriageField = { |
|||
key: string; |
|||
label: string; |
|||
type: string; |
|||
value: MarriageFieldValue; |
|||
}; |
|||
|
|||
export type MarriageRecommendedPlan = { |
|||
id: number; |
|||
title: string; |
|||
price: string; |
|||
}; |
|||
|
|||
export type MarriageSubscriptionPlan = MarriageRecommendedPlan & { |
|||
plan_type: string; |
|||
duration_days: number | null; |
|||
usage_limit: number | null; |
|||
}; |
|||
|
|||
export type MarriageActiveSubscription = { |
|||
id: number; |
|||
plan: MarriageSubscriptionPlan; |
|||
start_date: string; |
|||
end_date: string | null; |
|||
total_usages: number; |
|||
is_active: boolean; |
|||
is_valid: boolean; |
|||
}; |
|||
|
|||
export type MarriageActiveCase = { |
|||
case_id: number; |
|||
status: MarriageCaseStatus; |
|||
introduced_at: string | null; |
|||
}; |
|||
|
|||
export type MarriageMatchSummary = { |
|||
id: number; |
|||
gender: MarriageGender; |
|||
overall_completion_percent: number; |
|||
public_info: MarriageField[]; |
|||
}; |
|||
|
|||
export type MarriageProfile = { |
|||
id: number; |
|||
status: MarriageProfileStatus; |
|||
gender: MarriageGender | null; |
|||
is_registering_for_self: boolean | null; |
|||
can_edit_profile: boolean; |
|||
can_message_expert: boolean; |
|||
active_subscription: MarriageActiveSubscription | null; |
|||
is_ready_for_match: boolean; |
|||
active_case: MarriageActiveCase | null; |
|||
needs_subscription: boolean; |
|||
recommended_plan?: MarriageRecommendedPlan | null; |
|||
match_summary: MarriageMatchSummary | null; |
|||
}; |
|||
|
|||
export type UpdateMarriageProfileBasicPayload = { |
|||
gender: MarriageGender; |
|||
is_registering_for_self: boolean; |
|||
}; |
|||
|
|||
export type MarriageSection = { |
|||
id: number; |
|||
slug: string; |
|||
title: string; |
|||
is_required: boolean; |
|||
importance_weight: number; |
|||
estimated_minutes: number | null; |
|||
total_steps: number; |
|||
order: number; |
|||
current_step: number; |
|||
completion_percent: number; |
|||
}; |
|||
|
|||
export type MarriageSectionData = { |
|||
id?: number; |
|||
slug: string; |
|||
data: MarriageField[] | null; |
|||
current_step: number; |
|||
total_steps: number; |
|||
completion_percent: number; |
|||
updated_at: string | null; |
|||
}; |
|||
|
|||
export type UpdateMarriageSectionDataPayload = { |
|||
current_step: number; |
|||
fields: MarriageField[]; |
|||
}; |
|||
|
|||
export type StartMarriageMatchResponse = { |
|||
detail: string; |
|||
}; |
|||
|
|||
export type MarriageCase = { |
|||
id: number; |
|||
status: MarriageCaseStatus; |
|||
introduced_at: string | null; |
|||
male_responded_at?: string | null; |
|||
female_responded_at?: string | null; |
|||
payment_done_at?: string | null; |
|||
contact_shared_at?: string | null; |
|||
}; |
|||
|
|||
export type RespondMarriageCasePayload = { |
|||
action: "accept" | "reject"; |
|||
reason_code?: string; |
|||
custom_note?: string; |
|||
}; |
|||
|
|||
export type SubmitMarriageContactStatusPayload = { |
|||
action: "contacted" | "no_contact"; |
|||
}; |
|||
|
|||
export type MarriageCaseActionResponse = { |
|||
detail: string; |
|||
case: MarriageCase; |
|||
}; |
|||
|
|||
export type MarriageContactInfoResponse = { |
|||
contact_info: MarriageField[] | null; |
|||
}; |
|||
|
|||
export type UploadTmpMediaResponse = { |
|||
success: boolean; |
|||
path: string; |
|||
apath: string; |
|||
name: string; |
|||
size: string; |
|||
}; |
|||
@ -0,0 +1,50 @@ |
|||
"use client"; |
|||
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { MutationOptions } from "./options"; |
|||
import { pathParam } from "./path-param"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { |
|||
CaseId, |
|||
MarriageCaseActionResponse, |
|||
RespondMarriageCasePayload, |
|||
} from "./types"; |
|||
|
|||
export async function respondToMarriageCase( |
|||
caseId: CaseId, |
|||
payload: RespondMarriageCasePayload, |
|||
) { |
|||
const { data } = await http.post<MarriageCaseActionResponse>( |
|||
`/api/marriage/cases/${pathParam(caseId)}/respond/`, |
|||
payload, |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useRespondToMarriageCaseMutation( |
|||
caseId: CaseId, |
|||
options?: MutationOptions< |
|||
MarriageCaseActionResponse, |
|||
RespondMarriageCasePayload |
|||
>, |
|||
) { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
return useMutation({ |
|||
...options, |
|||
mutationFn: (payload) => respondToMarriageCase(caseId, payload), |
|||
onSuccess: async (data, variables, onMutateResult, context) => { |
|||
await Promise.all([ |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.profile(), |
|||
}), |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.contactInfo(caseId), |
|||
}), |
|||
]); |
|||
await options?.onSuccess?.(data, variables, onMutateResult, context); |
|||
}, |
|||
}); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
"use client"; |
|||
|
|||
import { useQuery } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { QueryOptions } from "./options"; |
|||
import { pathParam } from "./path-param"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { CaseId, MarriageContactInfoResponse } from "./types"; |
|||
|
|||
export async function getMarriageContactInfo(caseId: CaseId) { |
|||
const { data } = await http.get<MarriageContactInfoResponse>( |
|||
`/api/marriage/cases/${pathParam(caseId)}/contact-info/`, |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useMarriageContactInfoQuery< |
|||
TData = MarriageContactInfoResponse, |
|||
>( |
|||
caseId: CaseId | null | undefined, |
|||
options?: QueryOptions<MarriageContactInfoResponse, TData>, |
|||
) { |
|||
const normalizedCaseId = caseId ?? ""; |
|||
|
|||
return useQuery({ |
|||
...options, |
|||
enabled: Boolean(normalizedCaseId) && options?.enabled !== false, |
|||
queryFn: () => getMarriageContactInfo(normalizedCaseId), |
|||
queryKey: marriageQueryKeys.contactInfo(normalizedCaseId), |
|||
}); |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
"use client"; |
|||
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { MutationOptions } from "./options"; |
|||
import { pathParam } from "./path-param"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { |
|||
CaseId, |
|||
MarriageCaseActionResponse, |
|||
SubmitMarriageContactStatusPayload, |
|||
} from "./types"; |
|||
|
|||
export async function submitMarriageContactStatus( |
|||
caseId: CaseId, |
|||
payload: SubmitMarriageContactStatusPayload, |
|||
) { |
|||
const { data } = await http.post<MarriageCaseActionResponse>( |
|||
`/api/marriage/cases/${pathParam(caseId)}/contact-status/`, |
|||
payload, |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useSubmitMarriageContactStatusMutation( |
|||
caseId: CaseId, |
|||
options?: MutationOptions< |
|||
MarriageCaseActionResponse, |
|||
SubmitMarriageContactStatusPayload |
|||
>, |
|||
) { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
return useMutation({ |
|||
...options, |
|||
mutationFn: (payload) => submitMarriageContactStatus(caseId, payload), |
|||
onSuccess: async (data, variables, onMutateResult, context) => { |
|||
await Promise.all([ |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.profile(), |
|||
}), |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.contactInfo(caseId), |
|||
}), |
|||
]); |
|||
await options?.onSuccess?.(data, variables, onMutateResult, context); |
|||
}, |
|||
}); |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
"use client"; |
|||
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { MutationOptions } from "./options"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { StartMarriageMatchResponse } from "./types"; |
|||
|
|||
export async function startMarriageMatch() { |
|||
const { data } = await http.post<StartMarriageMatchResponse>( |
|||
"/api/marriage/match/start/", |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useStartMarriageMatchMutation( |
|||
options?: MutationOptions<StartMarriageMatchResponse>, |
|||
) { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
return useMutation({ |
|||
...options, |
|||
mutationFn: startMarriageMatch, |
|||
onSuccess: async (data, variables, onMutateResult, context) => { |
|||
await Promise.all([ |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.profile(), |
|||
}), |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.sections(), |
|||
}), |
|||
]); |
|||
await options?.onSuccess?.(data, variables, onMutateResult, context); |
|||
}, |
|||
}); |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
"use client"; |
|||
|
|||
import { useMutation, useQueryClient } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { MutationOptions } from "./options"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { |
|||
MarriageProfile, |
|||
UpdateMarriageProfileBasicPayload, |
|||
} from "./types"; |
|||
|
|||
export async function updateMarriageProfileBasic( |
|||
payload: UpdateMarriageProfileBasicPayload, |
|||
) { |
|||
const { data } = await http.patch<MarriageProfile>( |
|||
"/api/marriage/profile/basic/", |
|||
payload, |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useUpdateMarriageProfileBasicMutation( |
|||
options?: MutationOptions<MarriageProfile, UpdateMarriageProfileBasicPayload>, |
|||
) { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
return useMutation({ |
|||
...options, |
|||
mutationFn: updateMarriageProfileBasic, |
|||
onSuccess: async (data, variables, onMutateResult, context) => { |
|||
await queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.profile(), |
|||
}); |
|||
await options?.onSuccess?.(data, variables, onMutateResult, context); |
|||
}, |
|||
}); |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
"use client"; |
|||
|
|||
import { useQuery } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { QueryOptions } from "./options"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { MarriageProfile } from "./types"; |
|||
|
|||
export async function getMarriageProfile() { |
|||
const { data } = await http.get<MarriageProfile>( |
|||
"/api/marriage/profile/main/", |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useMarriageProfileQuery<TData = MarriageProfile>( |
|||
options?: QueryOptions<MarriageProfile, TData>, |
|||
) { |
|||
return useQuery({ |
|||
...options, |
|||
queryFn: getMarriageProfile, |
|||
queryKey: marriageQueryKeys.profile(), |
|||
}); |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
"use client"; |
|||
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { MutationOptions, QueryOptions } from "./options"; |
|||
import { pathParam } from "./path-param"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { |
|||
MarriageSectionData, |
|||
UpdateMarriageSectionDataPayload, |
|||
} from "./types"; |
|||
|
|||
export async function getMarriageSectionData(slug: string) { |
|||
const { data } = await http.get<MarriageSectionData>( |
|||
`/api/marriage/sections/${pathParam(slug)}/data/`, |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export async function updateMarriageSectionData( |
|||
slug: string, |
|||
payload: UpdateMarriageSectionDataPayload, |
|||
) { |
|||
const { data } = await http.patch<MarriageSectionData>( |
|||
`/api/marriage/sections/${pathParam(slug)}/data/`, |
|||
payload, |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useMarriageSectionDataQuery<TData = MarriageSectionData>( |
|||
slug: string | null | undefined, |
|||
options?: QueryOptions<MarriageSectionData, TData>, |
|||
) { |
|||
const normalizedSlug = slug ?? ""; |
|||
|
|||
return useQuery({ |
|||
...options, |
|||
enabled: Boolean(normalizedSlug) && options?.enabled !== false, |
|||
queryFn: () => getMarriageSectionData(normalizedSlug), |
|||
queryKey: marriageQueryKeys.sectionData(normalizedSlug), |
|||
}); |
|||
} |
|||
|
|||
export function useUpdateMarriageSectionDataMutation( |
|||
slug: string, |
|||
options?: MutationOptions< |
|||
MarriageSectionData, |
|||
UpdateMarriageSectionDataPayload |
|||
>, |
|||
) { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
return useMutation({ |
|||
...options, |
|||
mutationFn: (payload) => updateMarriageSectionData(slug, payload), |
|||
onSuccess: async (data, variables, onMutateResult, context) => { |
|||
await Promise.all([ |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.profile(), |
|||
}), |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.sections(), |
|||
}), |
|||
queryClient.invalidateQueries({ |
|||
queryKey: marriageQueryKeys.sectionData(slug), |
|||
}), |
|||
]); |
|||
await options?.onSuccess?.(data, variables, onMutateResult, context); |
|||
}, |
|||
}); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
"use client"; |
|||
|
|||
import { useQuery } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { QueryOptions } from "./options"; |
|||
import { marriageQueryKeys } from "./query-keys"; |
|||
import type { MarriageSection } from "./types"; |
|||
|
|||
export async function getMarriageSections() { |
|||
const { data } = await http.get<MarriageSection[]>("/api/marriage/sections/"); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useMarriageSectionsQuery<TData = MarriageSection[]>( |
|||
options?: QueryOptions<MarriageSection[], TData>, |
|||
) { |
|||
return useQuery({ |
|||
...options, |
|||
queryFn: getMarriageSections, |
|||
queryKey: marriageQueryKeys.sections(), |
|||
}); |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
"use client"; |
|||
|
|||
import { useMutation } from "@tanstack/react-query"; |
|||
import { http } from "@/lib/http"; |
|||
import type { MutationOptions } from "./options"; |
|||
import type { UploadTmpMediaResponse } from "./types"; |
|||
|
|||
const CSRF_TOKEN = |
|||
"53kqNKySTv3q4K3OolQqLEgaeF9pdPdAEnxrMARaUfvFrIGK57Qje67ifYUDMUQP"; |
|||
|
|||
export async function uploadTmpMedia(file: File) { |
|||
const formData = new FormData(); |
|||
formData.append("file", file); |
|||
|
|||
const { data } = await http.post<UploadTmpMediaResponse>( |
|||
"/upload-tmp-media/", |
|||
formData, |
|||
{ |
|||
headers: { |
|||
Accept: "application/json", |
|||
"Content-Type": "multipart/form-data", |
|||
"X-CSRFToken": CSRF_TOKEN, |
|||
}, |
|||
}, |
|||
); |
|||
|
|||
return data; |
|||
} |
|||
|
|||
export function useUploadTmpMediaMutation( |
|||
options?: MutationOptions<UploadTmpMediaResponse, File>, |
|||
) { |
|||
return useMutation({ |
|||
...options, |
|||
mutationFn: uploadTmpMedia, |
|||
}); |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue