35 changed files with 1128 additions and 574 deletions
-
72proxy.test.ts
-
42proxy.ts
-
5src/app/[lang]/layout.tsx
-
129src/app/globals.css
-
133src/app/layout.tsx
-
8src/app/providers.tsx
-
8src/app/questions-list/[slug]/question-detail-client.tsx
-
130src/app/questions-list/page.tsx
-
5src/app/questions-list/sections-request.tsx
-
49src/components/Componentes/flutter-locale-sync.tsx
-
28src/components/Componentes/info-progress-card.tsx
-
50src/components/Componentes/navigation-button.tsx
-
51src/components/Componentes/page-background.tsx
-
67src/components/Componentes/question-answer-storage.tsx
-
59src/components/Componentes/question-card.tsx
-
4src/components/Componentes/question-exit-navigation-button.tsx
-
6src/components/Componentes/question-section-flow.tsx
-
65src/components/Componentes/silent-reloader.tsx
-
3src/components/Componentes/test-questions-flow.tsx
-
62src/components/Componentes/token-switcher.tsx
-
335src/components/Componentes/ui-icon.tsx
-
1src/hooks/marriage/types.ts
-
30src/hooks/marriage/use-cattell.ts
-
21src/hooks/marriage/use-form-schema.ts
-
30src/hooks/marriage/use-glasser.ts
-
118src/hooks/marriage/use-section-data.ts
-
71src/hooks/marriage/use-sections.ts
-
9src/hooks/useFlutterBridge.ts
-
14src/lib/assessment-progress.test.ts
-
17src/lib/assessment-progress.ts
-
3src/lib/auth-bridge.ts
-
14src/lib/http.ts
-
42src/lib/view-paddings.ts
-
19src/translations/provider.tsx
-
2src/types/window.d.ts
@ -0,0 +1,72 @@ |
|||
import { NextRequest } from "next/server"; |
|||
import { describe, expect, it } from "vitest"; |
|||
import { config, proxy } from "./proxy"; |
|||
|
|||
function request(path: string, cookie?: string, acceptLanguage?: string) { |
|||
return new NextRequest(`https://example.test${path}`, { |
|||
headers: { |
|||
...(cookie ? { cookie } : {}), |
|||
...(acceptLanguage ? { "accept-language": acceptLanguage } : {}), |
|||
}, |
|||
}); |
|||
} |
|||
|
|||
describe("locale proxy", () => { |
|||
it("redirects a localized path when the authoritative cookie differs", () => { |
|||
const response = proxy(request("/en/questions-list", "HABIB_LANGUAGE=fa")); |
|||
expect(response.status).toBe(307); |
|||
expect(response.headers.get("location")).toBe( |
|||
"https://example.test/fa/questions-list", |
|||
); |
|||
}); |
|||
|
|||
it("passes the path locale to the root layout", () => { |
|||
const response = proxy(request("/fa/questions-list", "HABIB_LANGUAGE=fa")); |
|||
expect(response.status).toBe(200); |
|||
expect(response.headers.get("x-middleware-request-x-habib-locale")).toBe( |
|||
"fa", |
|||
); |
|||
}); |
|||
|
|||
it("excludes health checks from locale routing", () => { |
|||
expect(config.matcher[0]).toContain("healthz"); |
|||
}); |
|||
|
|||
it("falls back to the first supported accept-language entry", () => { |
|||
const response = proxy( |
|||
request("/questions-list", undefined, "fa-IR, fa;q=0.9, en;q=0.8"), |
|||
); |
|||
expect(response.status).toBe(307); |
|||
expect(response.headers.get("location")).toBe( |
|||
"https://example.test/fa/questions-list", |
|||
); |
|||
}); |
|||
|
|||
it("normalizes region subtags in accept-language", () => { |
|||
const response = proxy( |
|||
request("/questions-list", undefined, "en-US, en;q=0.9"), |
|||
); |
|||
expect(response.status).toBe(307); |
|||
expect(response.headers.get("location")).toBe( |
|||
"https://example.test/en/questions-list", |
|||
); |
|||
}); |
|||
|
|||
it("skips wildcard and unsupported accept-language entries", () => { |
|||
const response = proxy(request("/questions-list", undefined, "*, und")); |
|||
expect(response.status).toBe(307); |
|||
expect(response.headers.get("location")).toBe( |
|||
"https://example.test/en/questions-list", |
|||
); |
|||
}); |
|||
|
|||
it("keeps the cookie authoritative over accept-language", () => { |
|||
const response = proxy( |
|||
request("/en/questions-list", "HABIB_LANGUAGE=fa", "en-US, en;q=0.9"), |
|||
); |
|||
expect(response.status).toBe(307); |
|||
expect(response.headers.get("location")).toBe( |
|||
"https://example.test/fa/questions-list", |
|||
); |
|||
}); |
|||
}); |
|||
@ -1,74 +1,11 @@ |
|||
"use client"; |
|||
|
|||
import { useQueryClient } from "@tanstack/react-query"; |
|||
import { type ReactNode, useEffect } from "react"; |
|||
import { type ReactNode } from "react"; |
|||
|
|||
type SilentReloaderProps = { |
|||
children: ReactNode; |
|||
}; |
|||
|
|||
export default function SilentReloader({ children }: SilentReloaderProps) { |
|||
const queryClient = useQueryClient(); |
|||
|
|||
useEffect(() => { |
|||
if (typeof window !== "undefined") { |
|||
(window as any).__queryClient = queryClient; |
|||
} |
|||
|
|||
let lastReloadTime = 0; |
|||
const handleReload = () => { |
|||
const now = Date.now(); |
|||
if (now - lastReloadTime < 5000) { |
|||
return; |
|||
} |
|||
lastReloadTime = now; |
|||
// Invalidate all active queries so fresh data is reloaded from backend
|
|||
queryClient.invalidateQueries(); |
|||
}; |
|||
|
|||
const handleVisibilityChange = () => { |
|||
if (document.visibilityState === "visible") { |
|||
handleReload(); |
|||
} |
|||
}; |
|||
|
|||
const handlePageShow = () => { |
|||
handleReload(); |
|||
}; |
|||
|
|||
window.addEventListener("focus", handleReload); |
|||
window.addEventListener("pageshow", handlePageShow); |
|||
document.addEventListener("visibilitychange", handleVisibilityChange); |
|||
|
|||
// Listen for Flutter response events if available
|
|||
const win = window as any; |
|||
const unbindFlutter = |
|||
typeof win.addFlutterResponseListener === "function" |
|||
? win.addFlutterResponseListener(() => handleReload()) |
|||
: null; |
|||
|
|||
return () => { |
|||
window.removeEventListener("focus", handleReload); |
|||
window.removeEventListener("pageshow", handlePageShow); |
|||
document.removeEventListener("visibilitychange", handleVisibilityChange); |
|||
if (typeof unbindFlutter === "function") { |
|||
unbindFlutter(); |
|||
} |
|||
}; |
|||
}, [queryClient]); |
|||
|
|||
return <>{children}</>; |
|||
} |
|||
|
|||
export function triggerSilentReload(queryClient?: any) { |
|||
if (queryClient && typeof queryClient.invalidateQueries === "function") { |
|||
void queryClient.invalidateQueries(); |
|||
} else if ( |
|||
typeof window !== "undefined" && |
|||
(window as any).__queryClient && |
|||
typeof (window as any).__queryClient.invalidateQueries === "function" |
|||
) { |
|||
void (window as any).__queryClient.invalidateQueries(); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,335 @@ |
|||
"use client"; |
|||
|
|||
import type { ReactNode, SVGProps } from "react"; |
|||
import { useId } from "react"; |
|||
|
|||
/** |
|||
* Inline UI icons (pixel-identical to the SVGs previously served from |
|||
* `public/assets/images/*.svg` via `next/image`). |
|||
* |
|||
* Rendering icons as inline SVG removes the per-icon network request, the |
|||
* `next/image` lazy-loading behaviour and the `public/` cache revalidation |
|||
* from the critical path — icons are present in the first rendered markup. |
|||
* Gradient/clip-path ids are scope-prefixed with `useId` so multiple |
|||
* instances on one page never collide. |
|||
*/ |
|||
export type UiIconName = |
|||
| "support" |
|||
| "info" |
|||
| "document" |
|||
| "question" |
|||
| "person" |
|||
| "education" |
|||
| "details" |
|||
| "checklist" |
|||
| "contact" |
|||
| "success" |
|||
| "diamond"; |
|||
|
|||
type UiIconProps = SVGProps<SVGSVGElement> & { |
|||
name: UiIconName; |
|||
}; |
|||
|
|||
const PINK_GRADIENT_STOPS: ReactNode = ( |
|||
<> |
|||
<stop stopColor="#FE6F82" /> |
|||
<stop offset="1" stopColor="#E03950" /> |
|||
</> |
|||
); |
|||
|
|||
export function UiIcon({ name, ...props }: UiIconProps) { |
|||
const uid = useId().replace(/[^a-zA-Z0-9_-]/g, ""); |
|||
const gradientId = `ui-icon-grad-${uid}`; |
|||
const clipId = `ui-icon-clip-${uid}`; |
|||
|
|||
switch (name) { |
|||
case "support": |
|||
return ( |
|||
<svg viewBox="0 0 25 25" fill="none" aria-hidden="true" {...props}> |
|||
<path |
|||
d="M22.0473 12.4463C22.0536 10.9147 21.0054 9.62813 19.5981 9.29067C19.3513 5.27489 16.0816 2.06455 12.0894 2.04836C8.09724 2.03217 4.80167 5.20521 4.52225 9.22954C3.11229 9.55458 2.05372 10.8336 2.04751 12.3652L2.03945 14.3517C2.03223 16.1316 3.44442 17.5962 5.19784 17.6033L6.61595 17.609C7.60852 17.6131 8.41256 16.7986 8.41667 15.785L8.4361 10.9943C8.44021 9.98069 7.6428 9.15975 6.65023 9.15572L6.04233 9.15326C6.35895 6.03022 8.95241 3.5983 12.0831 3.61099C15.2138 3.62369 17.7874 6.07557 18.0787 9.20207L17.4708 9.1996C16.4782 9.19558 15.6742 10.01 15.6701 11.0236L15.6507 15.8143C15.6465 16.8279 16.444 17.6489 17.4365 17.6529L17.9228 17.6549C17.6267 18.3055 16.9866 18.7578 16.2466 18.7548L14.7573 18.7488C14.5989 17.7549 13.7614 16.9862 12.7479 16.9821L11.3193 16.9763C10.1843 16.9717 9.25913 17.9098 9.25443 19.0679L9.25091 19.937C9.24621 21.0961 10.1647 22.0407 11.2988 22.0453L12.7273 22.0511C13.7408 22.0552 14.5845 21.2933 14.751 20.3007L16.2403 20.3068C17.8617 20.3134 19.2238 19.1394 19.5446 17.578C20.964 17.2627 22.033 15.9741 22.0393 14.4328L22.0473 12.4463ZM6.91635 10.9881L6.89693 15.7788C6.8963 15.934 6.77422 16.0577 6.62224 16.0571L5.20414 16.0513C4.29229 16.0476 3.55542 15.289 3.5592 14.3578L3.56725 12.3713C3.57103 11.4402 4.31403 10.6876 5.22587 10.6913L6.64398 10.697C6.79595 10.6976 6.91703 10.8223 6.9164 10.9775L6.91635 10.9881ZM13.2631 19.9436C13.2619 20.2433 13.0177 20.4906 12.7242 20.4894L11.2956 20.4836C11.0021 20.4825 10.76 20.2332 10.7612 19.9334L10.7647 19.0643C10.7659 18.7646 11.0101 18.5173 11.3036 18.5185L12.7322 18.5243C13.0257 18.5255 13.2678 18.7748 13.2666 19.0745L13.2631 19.9436ZM20.5195 14.4266C20.5158 15.3578 19.7728 16.1104 18.8609 16.1067L17.4428 16.101C17.2908 16.1003 17.1698 15.9757 17.1704 15.8205L17.1898 11.0298C17.1905 10.8746 17.3125 10.7509 17.4645 10.7516L18.8826 10.7573C19.7945 10.761 20.5313 11.5196 20.5276 12.4508L20.5195 14.4373L20.5195 14.4266Z" |
|||
fill="black" |
|||
/> |
|||
</svg> |
|||
); |
|||
|
|||
case "info": |
|||
return ( |
|||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}> |
|||
<g clipPath={`url(#${clipId})`}> |
|||
<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={clipId}> |
|||
<rect |
|||
width="19" |
|||
height="19" |
|||
fill="white" |
|||
transform="translate(3 2.14258)" |
|||
/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
case "document": |
|||
return ( |
|||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}> |
|||
<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" |
|||
strokeWidth="1.5" |
|||
strokeLinecap="round" |
|||
strokeLinejoin="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" |
|||
strokeWidth="1.5" |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
/> |
|||
<path |
|||
fillRule="evenodd" |
|||
clipRule="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> |
|||
); |
|||
|
|||
case "question": |
|||
return ( |
|||
<svg viewBox="0 0 11 11" fill="none" aria-hidden="true" {...props}> |
|||
<g clipPath={`url(#${clipId})`}> |
|||
<path |
|||
d="M5.81527 3.10492C6.30043 3.10295 6.69214 2.70806 6.69017 2.2229C6.6882 1.73774 6.29331 1.34603 5.80815 1.348C5.32299 1.34997 4.93128 1.74486 4.93325 2.23002C4.93522 2.71518 5.33011 3.10689 5.81527 3.10492Z" |
|||
fill="white" |
|||
/> |
|||
<path |
|||
d="M6.72699 6.66728C6.70265 6.65106 6.67532 6.63985 6.64659 6.63433C6.61787 6.62881 6.58833 6.62908 6.55971 6.63513C6.53109 6.64118 6.50397 6.65288 6.47993 6.66956C6.4559 6.68623 6.43544 6.70754 6.41975 6.73223C6.20869 7.04754 5.94971 7.32797 5.65216 7.56341C5.5799 7.61861 5.31059 7.82837 5.19619 7.7849C5.11703 7.76106 5.16252 7.60493 5.17763 7.53897L5.29264 7.19366C5.34039 7.05289 6.1715 4.55434 6.2626 4.26843C6.39708 3.85056 6.33611 3.44007 5.71492 3.54143C5.54587 3.55969 3.83136 3.78848 3.80062 3.7908C3.77178 3.79279 3.74362 3.80045 3.71774 3.81332C3.69186 3.8262 3.66877 3.84404 3.64979 3.86584C3.63081 3.88764 3.61631 3.91297 3.60711 3.94037C3.59792 3.96777 3.59421 3.99672 3.5962 4.02555C3.59819 4.05439 3.60585 4.08255 3.61872 4.10843C3.6316 4.13431 3.64944 4.1574 3.67124 4.17638C3.69304 4.19536 3.71836 4.20987 3.74577 4.21906C3.77317 4.22826 3.80212 4.23196 3.83095 4.22997C3.83095 4.22997 4.48954 4.14164 4.56199 4.13476C4.5991 4.13098 4.63651 4.13803 4.66971 4.15504C4.70291 4.17205 4.73047 4.19831 4.74908 4.23064C4.78938 4.35351 4.78446 4.48673 4.73523 4.60629C4.67901 4.82617 3.78659 7.37329 3.75864 7.52057C3.72851 7.64384 3.73754 7.77343 3.78447 7.89133C3.83139 8.00924 3.91389 8.10959 4.02049 8.17844C4.22057 8.31063 4.45843 8.3737 4.69772 8.358C4.93046 8.35437 5.16046 8.30725 5.37587 8.21907C5.92189 7.99721 6.49059 7.41065 6.78964 6.95037C6.81536 6.90363 6.82319 6.84912 6.81166 6.79702C6.80013 6.74493 6.77004 6.69881 6.72699 6.66728Z" |
|||
fill="white" |
|||
/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id={clipId}> |
|||
<rect |
|||
width="7.9072" |
|||
height="7.9072" |
|||
fill="white" |
|||
transform="translate(1.25 0.927734) rotate(-0.232334)" |
|||
/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
case "person": |
|||
return ( |
|||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}> |
|||
<path |
|||
d="M6 7C6 5.67392 6.52678 4.40215 7.46447 3.46447C8.40215 2.52678 9.67392 2 11 2C12.3261 2 13.5979 2.52678 14.5355 3.46447C15.4732 4.40215 16 5.67392 16 7C16 8.32608 15.4732 9.59785 14.5355 10.5355C13.5979 11.4732 12.3261 12 11 12C9.67392 12 8.40215 11.4732 7.46447 10.5355C6.52678 9.59785 6 8.32608 6 7ZM4.822 14.6715C4.822 14.6717 4.82224 14.6719 4.82242 14.6717C6.42538 13.6939 8.60521 13 11 13C11.4473 13 11.886 13.0233 12.316 13.07C12.4878 13.0884 12.6518 13.151 12.7922 13.2517C12.9326 13.3524 13.0445 13.4878 13.117 13.6446C13.1895 13.8014 13.2202 13.9743 13.206 14.1465C13.1918 14.3186 13.1332 14.4842 13.036 14.627C12.3587 15.6213 11.9976 16.797 12 18C12 18.92 12.207 19.79 12.575 20.567C12.6467 20.7184 12.6792 20.8853 12.6696 21.0525C12.66 21.2198 12.6085 21.3819 12.5199 21.524C12.4313 21.6662 12.3085 21.7838 12.1626 21.8661C12.0167 21.9484 11.8525 21.9927 11.685 21.995L11 22C8.771 22 6.665 21.86 5.087 21.442C4.302 21.234 3.563 20.936 3.003 20.486C2.41 20.01 2 19.345 2 18.5C2 17.713 2.358 16.977 2.844 16.361C3.33791 15.7361 4.02076 15.1612 4.82157 14.6713C4.82176 14.6711 4.822 14.6713 4.822 14.6715ZM16 18C16 17.7348 16.1054 17.4804 16.2929 17.2929C16.4804 17.1054 16.7348 17 17 17H17.99C18.548 17 19 17.452 19 18.01V19.4795C19 19.8845 19.2689 20.2432 19.4238 20.6173C19.5081 20.8207 19.5229 21.0462 19.4659 21.2588C19.4089 21.4714 19.2834 21.6593 19.1087 21.7933C18.9341 21.9273 18.7201 22 18.5 22H18.01C17.7421 22 17.4852 21.8936 17.2958 21.7042C17.1064 21.5148 17 21.2579 17 20.99V19.7854C17 19.3516 16.5996 19.0138 16.2929 18.7071C16.1054 18.5196 16 18.2652 16 18ZM18 14C17.7451 14.0003 17.5 14.0979 17.3146 14.2728C17.1293 14.4478 17.0178 14.687 17.0028 14.9414C16.9879 15.1958 17.0707 15.4464 17.2343 15.6418C17.3979 15.8373 17.6299 15.9629 17.883 15.993L18.002 16C18.2569 15.9997 18.502 15.9021 18.6874 15.7272C18.8727 15.5522 18.9842 15.313 18.9992 15.0586C19.0141 14.8042 18.9313 14.5536 18.7677 14.3582C18.6041 14.1627 18.3721 14.0371 18.119 14.007L18 14Z" |
|||
fill={`url(#${gradientId})`} |
|||
/> |
|||
<defs> |
|||
<linearGradient |
|||
id={gradientId} |
|||
x1="19.6278" |
|||
y1="22.0503" |
|||
x2="3.93906" |
|||
y2="5.13022" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
{PINK_GRADIENT_STOPS} |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
case "education": |
|||
return ( |
|||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}> |
|||
<path |
|||
d="M21.4528 8.61533C21.4495 8.61202 21.4457 8.60926 21.4415 8.60717L12.4478 4.11033C12.3095 4.04162 12.1572 4.00586 12.0028 4.00586C11.8484 4.00586 11.6961 4.04162 11.5578 4.11033L5.55781 7.11033L2.55781 8.61033L1.55781 9.11033C1.3929 9.19343 1.25422 9.32057 1.15714 9.47766C1.06006 9.63476 1.00837 9.81566 1.00781 10.0003V15.0003C1.00781 15.5526 1.45553 16.0003 2.00781 16.0003C2.5601 16.0003 3.00781 15.5526 3.00781 15.0003V13.946C3.00781 12.4185 4.61573 11.4252 5.9817 12.109L11.5578 14.9003C11.6978 14.9703 11.8478 15.0103 12.0078 15.0103C12.1678 15.0103 12.3178 14.9703 12.4578 14.9003L21.4578 10.4003C21.7978 10.2303 22.0078 9.88033 22.0078 9.51033C22.0078 9.14263 21.8004 8.79468 21.4641 8.62352C21.46 8.6214 21.4561 8.61864 21.4528 8.61533Z" |
|||
fill={`url(#${gradientId})`} |
|||
/> |
|||
<path |
|||
d="M12 16.9996C11.54 16.9996 11.07 16.8896 10.66 16.6796L6.42214 14.5607C5.76876 14.234 5 14.7091 5 15.4396C5 17.4996 8.12 22 12 22C15.88 22 19 17.5096 19 15.4396C19 14.7091 18.2312 14.234 17.5779 14.5607L13.34 16.6796C12.93 16.8896 12.46 16.9996 12 16.9996Z" |
|||
fill={`url(#${gradientId})`} |
|||
/> |
|||
<defs> |
|||
<linearGradient |
|||
id={gradientId} |
|||
x1="22.1612" |
|||
y1="16.0305" |
|||
x2="14.9621" |
|||
y2="0.495013" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
{PINK_GRADIENT_STOPS} |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
case "details": |
|||
return ( |
|||
<svg viewBox="0 0 19 21" fill="none" aria-hidden="true" {...props}> |
|||
<path |
|||
d="M14 4.66045e-09C14.7652 -4.26217e-05 15.5015 0.292325 16.0583 0.817284C16.615 1.34224 16.9501 2.06011 16.995 2.824L17 3V12.25C17 12.6642 17.3358 13 17.75 13C18.397 13 18.93 13.492 18.994 14.122L19 14.25V16C19 16.7652 18.7077 17.5015 18.1827 18.0583C17.6578 18.615 16.9399 18.9501 16.176 18.995L16 19H6C5.23479 19 4.49849 18.7077 3.94174 18.1827C3.38499 17.6578 3.04989 16.9399 3.005 16.176L3 16V7.75C3 6.7835 2.2165 6 1.25 6C0.940542 6.00014 0.642032 5.88549 0.412234 5.67823C0.182437 5.47097 0.0376885 5.18583 0.00600005 4.878L4.66045e-09 4.75V3C-4.26217e-05 2.23479 0.292325 1.49849 0.817284 0.941739C1.34224 0.384993 2.06011 0.0498925 2.824 0.00500012L3 4.66045e-09H14ZM17 16C17 15.4477 16.5523 15 16 15H9C8.44771 15 8 15.4477 8 16C8 16.4978 8.35137 17 8.84919 17H16C16.2652 17 16.5196 16.8946 16.7071 16.7071C16.8946 16.5196 17 16.2652 17 16ZM10 9H8C7.74512 9.00028 7.49997 9.09788 7.31463 9.27285C7.1293 9.44782 7.01776 9.68695 7.00283 9.94139C6.98789 10.1958 7.07067 10.4464 7.23426 10.6418C7.39785 10.8373 7.6299 10.9629 7.883 10.993L8 11H10C10.2549 10.9997 10.5 10.9021 10.6854 10.7272C10.8707 10.5522 10.9822 10.313 10.9972 10.0586C11.0121 9.80416 10.9293 9.55362 10.7657 9.35817C10.6021 9.16271 10.3701 9.0371 10.117 9.007L10 9ZM12 5H8C7.73478 5 7.48043 5.10536 7.29289 5.29289C7.10536 5.48043 7 5.73478 7 6C7 6.26522 7.10536 6.51957 7.29289 6.70711C7.48043 6.89464 7.73478 7 8 7H12C12.2652 7 12.5196 6.89464 12.7071 6.70711C12.8946 6.51957 13 6.26522 13 6C13 5.73478 12.8946 5.48043 12.7071 5.29289C12.5196 5.10536 12.2652 5 12 5ZM3 2.78538C3 2.35163 2.5996 1.98618 2.29289 2.29289C2.10536 2.48043 2 2.73478 2 3V3.5C2 3.77614 2.22386 4 2.5 4C2.77614 4 3 3.77614 3 3.5V2.78538Z" |
|||
fill={`url(#${gradientId})`} |
|||
/> |
|||
<defs> |
|||
<linearGradient |
|||
id={gradientId} |
|||
x1="19.1388" |
|||
y1="19.0477" |
|||
x2="4.51281" |
|||
y2="1.02042" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
{PINK_GRADIENT_STOPS} |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
case "checklist": |
|||
return ( |
|||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}> |
|||
<g clipPath={`url(#${clipId})`}> |
|||
<path |
|||
fillRule="evenodd" |
|||
clipRule="evenodd" |
|||
d="M19.9922 3.75C19.9922 3.021 19.7022 2.321 19.1872 1.805C18.6712 1.29 17.9712 1 17.2422 1C14.3712 1 9.11319 1 6.24219 1C5.51319 1 4.81319 1.29 4.29719 1.805C3.78219 2.321 3.49219 3.021 3.49219 3.75C3.49219 7.582 3.49219 15.918 3.49219 19.75C3.49219 20.479 3.78219 21.179 4.29719 21.695C4.81319 22.21 5.51319 22.5 6.24219 22.5C9.11319 22.5 14.3712 22.5 17.2422 22.5C17.9712 22.5 18.6712 22.21 19.1872 21.695C19.7022 21.179 19.9922 20.479 19.9922 19.75V3.75ZM6.21219 18.28L7.21219 19.28C7.50519 19.573 7.97919 19.573 8.27219 19.28L10.2722 17.28C10.5652 16.988 10.5652 16.512 10.2722 16.22C9.98019 15.927 9.50419 15.927 9.21219 16.22L8.21185 17.2197C7.95235 17.479 7.53187 17.4791 7.27219 17.22C6.98019 16.927 6.50419 16.927 6.21219 17.22C5.91919 17.512 5.91919 17.988 6.21219 18.28ZM12.7422 18.5H16.2422C16.6562 18.5 16.9922 18.164 16.9922 17.75C16.9922 17.336 16.6562 17 16.2422 17H12.7422C12.3282 17 11.9922 17.336 11.9922 17.75C11.9922 18.164 12.3282 18.5 12.7422 18.5ZM9.77219 12.22C9.513 11.9603 9.513 11.5397 9.77219 11.28C10.0652 10.988 10.0652 10.512 9.77219 10.22C9.48019 9.927 9.00419 9.927 8.71219 10.22C8.45245 10.4792 8.03192 10.4792 7.77219 10.22C7.48019 9.927 7.00419 9.927 6.71219 10.22C6.41919 10.512 6.41919 10.988 6.71219 11.28C6.97137 11.5397 6.97137 11.9603 6.71219 12.22C6.41919 12.512 6.41919 12.988 6.71219 13.28C7.00419 13.573 7.48019 13.573 7.77219 13.28C8.03192 13.0208 8.45245 13.0208 8.71219 13.28C9.00419 13.573 9.48019 13.573 9.77219 13.28C10.0652 12.988 10.0652 12.512 9.77219 12.22ZM12.7422 12.5H16.2422C16.6562 12.5 16.9922 12.164 16.9922 11.75C16.9922 11.336 16.6562 11 16.2422 11H12.7422C12.3282 11 11.9922 11.336 11.9922 11.75C11.9922 12.164 12.3282 12.5 12.7422 12.5ZM6.21219 6.28L7.21219 7.28C7.50519 7.573 7.97919 7.573 8.27219 7.28L10.2722 5.28C10.5652 4.988 10.5652 4.512 10.2722 4.22C9.98019 3.927 9.50419 3.927 9.21219 4.22L8.21185 5.21966C7.95235 5.47898 7.53187 5.47913 7.27219 5.22C6.98019 4.927 6.50419 4.927 6.21219 5.22C5.91919 5.512 5.91919 5.988 6.21219 6.28ZM12.7422 6.5H16.2422C16.6562 6.5 16.9922 6.164 16.9922 5.75C16.9922 5.336 16.6562 5 16.2422 5H12.7422C12.3282 5 11.9922 5.336 11.9922 5.75C11.9922 6.164 12.3282 6.5 12.7422 6.5Z" |
|||
fill={`url(#${gradientId})`} |
|||
/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id={clipId}> |
|||
<rect width="24" height="24" fill="white" /> |
|||
</clipPath> |
|||
<linearGradient |
|||
id={gradientId} |
|||
x1="20.1127" |
|||
y1="22.554" |
|||
x2="3.22528" |
|||
y2="6.57994" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
{PINK_GRADIENT_STOPS} |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
case "contact": |
|||
return ( |
|||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" {...props}> |
|||
<path |
|||
fillRule="evenodd" |
|||
clipRule="evenodd" |
|||
d="M10 4H14C17.771 4 19.657 4 20.828 5.172C21.999 6.344 22 8.229 22 12C22 15.771 22 17.657 20.828 18.828C19.656 19.999 17.771 20 14 20H10C6.229 20 4.343 20 3.172 18.828C2.001 17.656 2 15.771 2 12C2 8.229 2 6.343 3.172 5.172C4.344 4.001 6.229 4 10 4ZM13.25 9C13.25 8.80109 13.329 8.61032 13.4697 8.46967C13.6103 8.32902 13.8011 8.25 14 8.25H19C19.1989 8.25 19.3897 8.32902 19.5303 8.46967C19.671 8.61032 19.75 8.80109 19.75 9C19.75 9.19891 19.671 9.38968 19.5303 9.53033C19.3897 9.67098 19.1989 9.75 19 9.75H14C13.8011 9.75 13.6103 9.67098 13.4697 9.53033C13.329 9.38968 13.25 9.19891 13.25 9ZM14.25 12C14.25 11.8011 14.329 11.6103 14.4697 11.4697C14.6103 11.329 14.8011 11.25 15 11.25H19C19.1989 11.25 19.3897 11.329 19.5303 11.4697C19.671 11.6103 19.75 11.8011 19.75 12C19.75 12.1989 19.671 12.3897 19.5303 12.5303C19.3897 12.671 19.1989 12.75 19 12.75H15C14.8011 12.75 14.6103 12.671 14.4697 12.5303C14.329 12.3897 14.25 12.1989 14.25 12ZM15.25 15C15.25 14.8011 15.329 14.6103 15.4697 14.4697C15.6103 14.329 15.8011 14.25 16 14.25H19C19.1989 14.25 19.3897 14.329 19.5303 14.4697C19.671 14.6103 19.75 14.8011 19.75 15C19.75 15.1989 19.671 15.3897 19.5303 15.5303C19.3897 15.671 19.1989 15.75 19 15.75H16C15.8011 15.75 15.6103 15.671 15.4697 15.5303C15.329 15.3897 15.25 15.1989 15.25 15ZM11 9C11 9.53043 10.7893 10.0391 10.4142 10.4142C10.0391 10.7893 9.53043 11 9 11C8.46957 11 7.96086 10.7893 7.58579 10.4142C7.21071 10.0391 7 9.53043 7 9C7 8.46957 7.21071 7.96086 7.58579 7.58579C7.96086 7.21071 8.46957 7 9 7C9.53043 7 10.0391 7.21071 10.4142 7.58579C10.7893 7.96086 11 8.46957 11 9ZM9 17C13 17 13 16.105 13 15C13 13.895 11.21 13 9 13C6.79 13 5 13.895 5 15C5 16.105 5 17 9 17Z" |
|||
fill={`url(#${gradientId})`} |
|||
/> |
|||
<defs> |
|||
<linearGradient |
|||
id={gradientId} |
|||
x1="22.1461" |
|||
y1="20.0402" |
|||
x2="10.65" |
|||
y2="2.32821" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
{PINK_GRADIENT_STOPS} |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
case "success": |
|||
return ( |
|||
<svg viewBox="0 0 13 13" fill="none" aria-hidden="true" {...props}> |
|||
<rect |
|||
x="0.0390625" |
|||
y="0.0546676" |
|||
width="12" |
|||
height="12" |
|||
rx="6" |
|||
fill="#0EB13C" |
|||
/> |
|||
<rect |
|||
x="8.625" |
|||
y="4.12661" |
|||
width="0.715424" |
|||
height="4.78806" |
|||
transform="rotate(45 8.625 4.12661)" |
|||
fill="white" |
|||
stroke="white" |
|||
strokeWidth="0.18" |
|||
/> |
|||
<rect |
|||
x="3.51335" |
|||
y="6.68553" |
|||
width="0.715424" |
|||
height="2.52178" |
|||
transform="rotate(-45 3.51335 6.68553)" |
|||
fill="white" |
|||
stroke="white" |
|||
strokeWidth="0.18" |
|||
/> |
|||
</svg> |
|||
); |
|||
|
|||
case "diamond": |
|||
return ( |
|||
<svg viewBox="0 0 25 25" fill="none" aria-hidden="true" {...props}> |
|||
<g clipPath={`url(#${clipId})`}> |
|||
<path |
|||
d="M5.40748 2.52134L18.7714 2.57553L22.5624 9.24096L12.0124 21.5483L1.56254 9.1558L5.40748 2.52134Z" |
|||
stroke="#111111" |
|||
strokeWidth="1.5" |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
/> |
|||
<path |
|||
d="M5.40748 2.52134L12.0124 21.5483L18.7714 2.57553M1.56254 9.1558L22.5624 9.24096" |
|||
stroke="#111111" |
|||
strokeWidth="1.5" |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
/> |
|||
<path |
|||
d="M7.76567 9.18119L12.0876 2.54867L16.3561 9.21603" |
|||
stroke="#111111" |
|||
strokeWidth="1.5" |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
/> |
|||
</g> |
|||
<defs> |
|||
<clipPath id={clipId}> |
|||
<rect |
|||
width="24" |
|||
height="24" |
|||
fill="white" |
|||
transform="translate(0.0973196) rotate(0.232334)" |
|||
/> |
|||
</clipPath> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
|
|||
default: |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
export default UiIcon; |
|||
@ -1,55 +1,30 @@ |
|||
"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"; |
|||
import type { FormSchemaResponse } from "./use-form-schema"; |
|||
import { useFormSchemaQuery } from "./use-form-schema"; |
|||
|
|||
export async function getMarriageSections() { |
|||
const getClientCookie = (name: string) => { |
|||
if (typeof document === "undefined") return "en"; |
|||
const value = `; ${document.cookie}`; |
|||
const parts = value.split(`; ${name}=`); |
|||
if (parts.length === 2) return parts.pop()?.split(";").shift() ?? "en"; |
|||
return "en"; |
|||
}; |
|||
const lang = getClientCookie("HABIB_LANGUAGE") || getClientCookie("habib_language") || "en"; |
|||
|
|||
const { data } = await http.get<FormSchemaResponse>( |
|||
`/api/marriage/forms/profile/?lang=${lang}` |
|||
); |
|||
|
|||
return data.sections.map((sec, idx) => { |
|||
const prog = data.progress.sections_progress[sec.id] || { |
|||
current_step: 0, |
|||
total_steps: 0, |
|||
completion_percent: 0.0, |
|||
}; |
|||
return { |
|||
id: idx + 1, |
|||
slug: sec.id, |
|||
title: sec.title, |
|||
is_required: sec.is_required, |
|||
importance_weight: 1, |
|||
estimated_minutes: sec.estimated_minutes, |
|||
total_steps: prog.total_steps, |
|||
order: idx, |
|||
current_step: prog.current_step, |
|||
completion_percent: prog.completion_percent, |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
export function useMarriageSectionsQuery<TData = MarriageSection[]>( |
|||
options?: QueryOptions<MarriageSection[], TData>, |
|||
export function useMarriageSectionsQuery( |
|||
locale: string, |
|||
) { |
|||
return useQuery({ |
|||
staleTime: 10 * 1000, |
|||
refetchOnWindowFocus: false, |
|||
...options, |
|||
queryFn: getMarriageSections, |
|||
queryKey: marriageQueryKeys.sections(), |
|||
return useFormSchemaQuery<MarriageSection[]>("profile", locale, { |
|||
select: (schema) => schema.sections.map((section, index) => { |
|||
const progress = schema.progress.sections_progress[section.id] || { |
|||
current_step: 0, |
|||
total_steps: 0, |
|||
completion_percent: 0, |
|||
}; |
|||
return { |
|||
id: index + 1, |
|||
slug: section.id, |
|||
title: section.title, |
|||
is_required: section.is_required, |
|||
importance_weight: 1, |
|||
estimated_minutes: section.estimated_minutes, |
|||
total_steps: progress.total_steps, |
|||
order: index, |
|||
current_step: progress.current_step, |
|||
completion_percent: progress.completion_percent, |
|||
}; |
|||
}), |
|||
}); |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
import { describe, expect, it } from "vitest"; |
|||
import { getAssessmentLocalProgress } from "./assessment-progress"; |
|||
|
|||
describe("getAssessmentLocalProgress", () => { |
|||
it("uses server completion as the only path to 100", () => { |
|||
expect(getAssessmentLocalProgress({ answers: { 1: "A" }, totalQuestions: 1 }, true)).toBe(100); |
|||
expect(getAssessmentLocalProgress({ answers: { 1: "A" }, totalQuestions: 1 }, false)).toBe(99); |
|||
}); |
|||
|
|||
it("calculates local progress from answered questions", () => { |
|||
expect(getAssessmentLocalProgress({ answers: { 1: "A", 2: "B" }, totalQuestions: 8 }, false)).toBe(25); |
|||
expect(getAssessmentLocalProgress(null, false)).toBe(0); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,17 @@ |
|||
export function getAssessmentLocalProgress( |
|||
draft: unknown, |
|||
serverCompleted: boolean, |
|||
): number { |
|||
if (serverCompleted) return 100; |
|||
if (!draft || typeof draft !== "object") return 0; |
|||
|
|||
const value = draft as { answers?: unknown; totalQuestions?: unknown }; |
|||
const answers = value.answers; |
|||
const total = Number(value.totalQuestions); |
|||
if (!answers || typeof answers !== "object" || !Number.isFinite(total) || total <= 0) { |
|||
return 0; |
|||
} |
|||
|
|||
const answered = Object.keys(answers).length; |
|||
return Math.min(99, Math.round((answered / total) * 100)); |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue