48 changed files with 3328 additions and 425 deletions
-
1331.playwright-mcp/console-2026-08-15T21-43-01-507Z.log
-
192.playwright-mcp/console-2026-08-15T21-58-02-063Z.log
-
30.playwright-mcp/console-2026-08-15T22-06-02-559Z.log
-
8.playwright-mcp/page-2026-08-15T21-43-05-290Z.yml
-
8.playwright-mcp/page-2026-08-15T21-58-15-641Z.yml
-
8.playwright-mcp/page-2026-08-15T21-58-30-755Z.yml
-
43WEBVIEW_ACTION_DOWNLOAD.md
-
33src/app/globals.css
-
30src/app/new-match/page.tsx
-
105src/app/questions-list/[slug]/question-detail-client.tsx
-
57src/app/questions-list/page.tsx
-
27src/app/request-accepted/page.tsx
-
74src/components/Componentes/advisor-actions-card.tsx
-
271src/components/Componentes/data-error-state.tsx
-
210src/components/Componentes/navigation-button.tsx
-
101src/components/Componentes/question-sheet.test.tsx
-
29src/components/Componentes/question-sheet.tsx
-
186src/components/Componentes/question-snap-list.test.tsx
-
73src/components/Componentes/question-snap-list.tsx
-
257src/components/Componentes/question-viewport-coordinator.ts
-
143src/components/Componentes/subscription-required-sheet.tsx
-
136src/components/Componentes/use-sheet-scroll-lock.ts
-
191src/lib/webview-actions.test.ts
-
18src/lib/webview-actions.ts
-
8src/translations/locales/ar.json
-
8src/translations/locales/az.json
-
8src/translations/locales/bn.json
-
8src/translations/locales/da.json
-
8src/translations/locales/de.json
-
8src/translations/locales/en.json
-
8src/translations/locales/es.json
-
8src/translations/locales/fa.json
-
8src/translations/locales/fr.json
-
8src/translations/locales/gu.json
-
8src/translations/locales/ha.json
-
8src/translations/locales/he.json
-
8src/translations/locales/hi.json
-
8src/translations/locales/id.json
-
8src/translations/locales/ks.json
-
8src/translations/locales/pt.json
-
8src/translations/locales/ru.json
-
8src/translations/locales/sw.json
-
8src/translations/locales/tg.json
-
8src/translations/locales/tr.json
-
8src/translations/locales/ul.json
-
8src/translations/locales/ur.json
-
8src/translations/locales/uz.json
-
8src/translations/locales/zh.json
1331
.playwright-mcp/console-2026-08-15T21-43-01-507Z.log
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,192 @@ |
|||
[ 2312ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786830299174 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 2313ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786830306438 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 2313ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786830345862 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 3843ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 5505ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 8367ms] Invalid or unexpected token |
|||
[ 12931ms] [LOG] ⏱️ Load time: -72 ms @ :133 |
|||
[ 13275ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 13533ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 14733ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/userspace/app/errors/intercept-console-error.js:57 |
|||
[ 14825ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 17526ms] [LOG] [Fast Refresh] done in 2677ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 17599ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 19569ms] [LOG] [Fast Refresh] done in 1966ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 20291ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 21005ms] [LOG] [Fast Refresh] done in 714ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 22079ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 22518ms] [LOG] [Fast Refresh] done in 438ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 23531ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 23993ms] [LOG] [Fast Refresh] done in 461ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 25019ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 25545ms] [LOG] [Fast Refresh] done in 526ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 26409ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 26752ms] [LOG] [Fast Refresh] done in 343ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 27962ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 29256ms] [LOG] [Fast Refresh] done in 1295ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 30598ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 31276ms] [LOG] [Fast Refresh] done in 677ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 32373ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 32898ms] [LOG] [Fast Refresh] done in 525ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 33845ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 34324ms] [LOG] [Fast Refresh] done in 479ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 35084ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 35402ms] [LOG] [Fast Refresh] done in 317ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 36586ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 37194ms] [LOG] [Fast Refresh] done in 608ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 38517ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 39774ms] [LOG] [Fast Refresh] done in 1258ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 43870ms] Invalid or unexpected token |
|||
[ 54089ms] [LOG] ⏱️ Load time: -282 ms @ :133 |
|||
[ 54442ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 55063ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 57980ms] [ERROR] Can't perform a React state update on a component that hasn't mounted yet. This indicates that you have a side-effect in your render function that asynchronously tries to update the component. Move this work to useEffect instead. @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/userspace/app/errors/intercept-console-error.js:57 |
|||
[ 58065ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 60207ms] [LOG] [Fast Refresh] done in 2141ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 60239ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 62575ms] [LOG] [Fast Refresh] done in 2335ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 62668ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 65667ms] [LOG] [Fast Refresh] done in 2998ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 68471ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 69333ms] [LOG] [Fast Refresh] done in 861ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 70653ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 71289ms] [LOG] [Fast Refresh] done in 636ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 72826ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 73894ms] [LOG] [Fast Refresh] done in 1068ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 75146ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 75523ms] [LOG] [Fast Refresh] done in 377ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 76615ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 77399ms] [LOG] [Fast Refresh] done in 783ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 79266ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 80538ms] [LOG] [Fast Refresh] done in 1272ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 81933ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 82728ms] [LOG] [Fast Refresh] done in 795ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 84305ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 84835ms] [LOG] [Fast Refresh] done in 531ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 85995ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 86367ms] [LOG] [Fast Refresh] done in 371ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 87461ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 88244ms] [LOG] [Fast Refresh] done in 782ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 89525ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 89942ms] [LOG] [Fast Refresh] done in 416ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 101433ms] [LOG] ⏱️ Load time: -277 ms @ :133 |
|||
[ 101767ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 102024ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 102286ms] [ERROR] In HTML, text nodes cannot be a child of <%s>. |
|||
This will cause a hydration error.%s html |
|||
|
|||
... |
|||
<HotReload globalError={[...]} webSocket={WebSocket} staticIndicatorState={{pathname:null, ...}}> |
|||
<AppDevOverlayErrorBoundary globalError={[...]}> |
|||
<ReplaySsrOnlyErrors> |
|||
<DevRootHTTPAccessFallbackBoundary> |
|||
<HTTPAccessFallbackBoundary notFound={<NotAllowedRootHTTPFallbackError>}> |
|||
<HTTPAccessFallbackErrorBoundary pathname="/en/questi..." notFound={<NotAllowedRootHTTPFallbackError>} ...> |
|||
<RedirectBoundary> |
|||
<RedirectErrorBoundary router={{...}}> |
|||
<Head> |
|||
<__next_root_layout_boundary__> |
|||
<SegmentViewNode type="layout" pagePath="layout.tsx"> |
|||
<SegmentTrieNode> |
|||
<link> |
|||
<RootLayout> |
|||
> <html |
|||
> lang="en" |
|||
> dir="ltr" |
|||
> data-locator="/home/alireza/Desktop/main/habbib/marriage/src/app/layout.tsx:61:4" |
|||
> > |
|||
<head> |
|||
> {"گ "} |
|||
... |
|||
... |
|||
@ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/userspace/app/errors/intercept-console-error.js:57 |
|||
[ 105477ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 107967ms] [LOG] [Fast Refresh] done in 2498ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 108059ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 108091ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 108133ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 112893ms] [LOG] [Fast Refresh] done in 4760ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 112899ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 112949ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 113552ms] [LOG] [Fast Refresh] done in 603ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 115756ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3001/api/remote-network:0 |
|||
[ 117651ms] [LOG] [Fast Refresh] done in 4703ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 118373ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 118495ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 118521ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 119352ms] [LOG] [Fast Refresh] done in 857ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 119978ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 120162ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 120850ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 121792ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 121882ms] [LOG] [Fast Refresh] done in 1032ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 122286ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 122998ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 123013ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 123046ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 123567ms] [LOG] [Fast Refresh] done in 568ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 124530ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 124660ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 124838ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 126241ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 126307ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 126370ms] [LOG] [Fast Refresh] done in 1530ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 128199ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 128506ms] [LOG] [Fast Refresh] done in 307ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 131511ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831205299 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 131516ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831198662 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 131516ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831203335 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 131516ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831200775 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 139682ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831205299 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 139682ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831198662 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 139682ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831203335 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 139682ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831200775 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 140129ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fprofile%2Fmain%2F:0 |
|||
[ 140191ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ http://localhost:3001/api/proxy?__proxyPath=%2Fapi%2Fmarriage%2Fforms%2Fprofile%2F%3Flang%3Den:0 |
|||
[ 143590ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831205299 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 143590ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831198662 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 143590ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831203335 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 143590ms] [WARNING] The resource http://localhost:3001/_next/static/css/app/layout.css?v=1786831200775 was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally. @ http://localhost:3001/en/questions-list:0 |
|||
[ 159789ms] [LOG] ⏱️ Load time: -162 ms @ :133 |
|||
[ 161142ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 171022ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 297120ms] Error: Internal Next.js error: Router action dispatched before initialization. |
|||
at dispatchAppRouterAction (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/use-action-queue.js:50:37) |
|||
at eval (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router-instance.js:329:61) |
|||
at startTransition (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react/cjs/react.development.js:556:27) |
|||
at Object.hmrRefresh (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router-instance.js:328:40) |
|||
at eval (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js:292:64) |
|||
at startTransition (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/compiled/react/cjs/react.development.js:556:27) |
|||
at processMessage (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/dev/hot-reloader/app/hot-reloader-app.js:291:44) |
|||
at WebSocket.handleMessage (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/dev/hot-reloader/app/web-socket.js:95:52) |
|||
[ 298019ms] [ERROR] In HTML, text nodes cannot be a child of <%s>. |
|||
This will cause a hydration error.%s html |
|||
|
|||
... |
|||
<HotReload globalError={[...]} webSocket={WebSocket} staticIndicatorState={{pathname:null, ...}}> |
|||
<AppDevOverlayErrorBoundary globalError={[...]}> |
|||
<ReplaySsrOnlyErrors> |
|||
<DevRootHTTPAccessFallbackBoundary> |
|||
<HTTPAccessFallbackBoundary notFound={<NotAllowedRootHTTPFallbackError>}> |
|||
<HTTPAccessFallbackErrorBoundary pathname="/en/questi..." notFound={<NotAllowedRootHTTPFallbackError>} ...> |
|||
<RedirectBoundary> |
|||
<RedirectErrorBoundary router={{...}}> |
|||
<Head> |
|||
<__next_root_layout_boundary__> |
|||
<SegmentViewNode type="layout" pagePath="layout.tsx"> |
|||
<SegmentTrieNode> |
|||
<link> |
|||
<RootLayout> |
|||
> <html |
|||
> lang="en" |
|||
> dir="ltr" |
|||
> data-locator="/home/alireza/Desktop/main/habbib/marriage/src/app/layout.tsx:61:4" |
|||
> > |
|||
<head> |
|||
> {"گ "} |
|||
... |
|||
... |
|||
@ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/userspace/app/errors/intercept-console-error.js:57 |
|||
[ 425863ms] [LOG] ⏱️ Load time: -264 ms @ :133 |
|||
[ 426503ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 434299ms] [LOG] [HMR] connected @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
@ -0,0 +1,30 @@ |
|||
[ 58786ms] [LOG] [Fast Refresh] rebuilding @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
[ 97017ms] [ERROR] In HTML, text nodes cannot be a child of <%s>. |
|||
This will cause a hydration error.%s html |
|||
|
|||
... |
|||
<HotReload globalError={[...]} webSocket={WebSocket} staticIndicatorState={{pathname:null, ...}}> |
|||
<AppDevOverlayErrorBoundary globalError={[...]}> |
|||
<ReplaySsrOnlyErrors> |
|||
<DevRootHTTPAccessFallbackBoundary> |
|||
<HTTPAccessFallbackBoundary notFound={<NotAllowedRootHTTPFallbackError>}> |
|||
<HTTPAccessFallbackErrorBoundary pathname="/en/questi..." notFound={<NotAllowedRootHTTPFallbackError>} ...> |
|||
<RedirectBoundary> |
|||
<RedirectErrorBoundary router={{...}}> |
|||
<Head> |
|||
<__next_root_layout_boundary__> |
|||
<SegmentViewNode type="layout" pagePath="layout.tsx"> |
|||
<SegmentTrieNode> |
|||
<link> |
|||
<RootLayout> |
|||
> <html |
|||
> lang="en" |
|||
> dir="ltr" |
|||
> data-locator="/home/alireza/Desktop/main/habbib/marriage/src/app/layout.tsx:61:4" |
|||
> > |
|||
<head> |
|||
> {"گ "} |
|||
... |
|||
... |
|||
@ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/userspace/app/errors/intercept-console-error.js:57 |
|||
[ 138485ms] [LOG] [Fast Refresh] done in 79848ms @ webpack-internal:///(app-pages-browser)/./node_modules/next/dist/next-devtools/shared/forward-logs-shared.js:27 |
|||
@ -0,0 +1,8 @@ |
|||
- generic [active] [ref=e1]: |
|||
- text: گ |
|||
- main [ref=e4]: |
|||
- generic [ref=e5]: |
|||
- button "Close questions list" [ref=e6] [cursor=pointer] |
|||
- heading "Profile registration" [level=1] [ref=e9] |
|||
- button "Support" [ref=e10] [cursor=pointer] |
|||
- button "Send LocalStorage to Terminal API" [ref=e78] [cursor=pointer] |
|||
@ -0,0 +1,8 @@ |
|||
- generic [active] [ref=f1e1]: |
|||
- text: گ |
|||
- main [ref=f1e4]: |
|||
- generic [ref=f1e5]: |
|||
- button "Close questions list" [ref=f1e6] [cursor=pointer] |
|||
- heading "Profile registration" [level=1] [ref=f1e9] |
|||
- button "Support" [ref=f1e10] [cursor=pointer] |
|||
- button "Send LocalStorage to Terminal API" [ref=f1e78] [cursor=pointer] |
|||
@ -0,0 +1,8 @@ |
|||
- generic [active] [ref=f1e1]: |
|||
- text: گ |
|||
- main [ref=f1e4]: |
|||
- generic [ref=f1e5]: |
|||
- button "Close questions list" [ref=f1e6] [cursor=pointer] |
|||
- heading "Profile registration" [level=1] [ref=f1e9] |
|||
- button "Support" [ref=f1e10] [cursor=pointer] |
|||
- button "Send LocalStorage to Terminal API" [ref=f1e78] [cursor=pointer] |
|||
@ -0,0 +1,271 @@ |
|||
"use client"; |
|||
|
|||
import { useI18n } from "@/translations/provider"; |
|||
|
|||
type DataErrorStateProps = { |
|||
/** Override the default title */ |
|||
title?: string; |
|||
/** Override the default subtitle / message */ |
|||
message?: string; |
|||
/** Called when the user taps "Try Again". Hide the button when omitted. */ |
|||
onRetry?: () => void; |
|||
/** Optional className for the outermost wrapper */ |
|||
className?: string; |
|||
}; |
|||
|
|||
/** |
|||
* Full-screen data-loading error state. |
|||
* |
|||
* Shown when a critical API call fails (profile, section list, section |
|||
* detail, etc.) — **not** for form-submit failures which keep their |
|||
* existing toast / inline feedback. |
|||
* |
|||
* Design references: |
|||
* – Najm `NoInternetErrorWidget` (disconnected-plug illustration + title |
|||
* + subtitle + "Try Again" button) |
|||
* – Hosseinieh `ImportErrorState` (retry button with refresh icon) |
|||
*/ |
|||
export default function DataErrorState({ |
|||
title, |
|||
message, |
|||
onRetry, |
|||
className = "", |
|||
}: DataErrorStateProps) { |
|||
const { dictionary: t } = useI18n(); |
|||
|
|||
const displayTitle = |
|||
title ?? t["Connection error"] ?? "Connection error"; |
|||
const displayMessage = |
|||
message ?? |
|||
t[ |
|||
"Something went wrong. Please check your internet connection and try again." |
|||
] ?? |
|||
"Something went wrong. Please check your internet connection and try again."; |
|||
const retryLabel = t["Try again"] ?? "Try again"; |
|||
|
|||
return ( |
|||
<div |
|||
className={`flex min-h-[60svh] flex-col items-center justify-center gap-0 px-6 text-center ${className}`} |
|||
> |
|||
{/* ── Disconnected-plug illustration (inline SVG — no network request) ── */} |
|||
<DisconnectedIllustration /> |
|||
|
|||
{/* ── Title ── */} |
|||
<h2 className="mt-8 text-[18px] font-bold leading-snug text-[#1B1B1B]"> |
|||
{displayTitle} |
|||
</h2> |
|||
|
|||
{/* ── Subtitle / message ── */} |
|||
<p className="mt-2 max-w-[280px] text-[13px] font-semibold leading-[1.55] text-[#8B8B8B]"> |
|||
{displayMessage} |
|||
</p> |
|||
|
|||
{/* ── Retry button ── */} |
|||
{onRetry && ( |
|||
<button |
|||
type="button" |
|||
onClick={onRetry} |
|||
className="mt-6 flex items-center gap-2 rounded-[15px] bg-white px-5 py-2.5 text-[13px] font-semibold text-[#202227] shadow-[0_4px_16px_rgba(0,0,0,0.08)] transition-all duration-200 active:scale-95 hover:shadow-[0_6px_20px_rgba(0,0,0,0.12)]" |
|||
> |
|||
<svg |
|||
aria-hidden="true" |
|||
className="size-4" |
|||
viewBox="0 0 24 24" |
|||
fill="none" |
|||
stroke="currentColor" |
|||
strokeWidth={2} |
|||
strokeLinecap="round" |
|||
strokeLinejoin="round" |
|||
> |
|||
<path d="M21 12a9 9 0 1 1-2.64-6.36" /> |
|||
<path d="M21 4v5h-5" /> |
|||
</svg> |
|||
{retryLabel} |
|||
</button> |
|||
)} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
/* ───────────────────────────────────────────────────────────────────────────── |
|||
* Disconnected-plug SVG illustration |
|||
* |
|||
* Adapted from Najm's `disconnected.svg` — a split electrical plug with a |
|||
* soft blob background. Rendered inline so it's paint-ready on the first |
|||
* frame without a network round-trip. |
|||
* ───────────────────────────────────────────────────────────────────────── */ |
|||
function DisconnectedIllustration() { |
|||
return ( |
|||
<svg |
|||
aria-hidden="true" |
|||
className="w-[180px] h-[180px]" |
|||
viewBox="0 0 206 206" |
|||
fill="none" |
|||
> |
|||
{/* Background blob */} |
|||
<path |
|||
d="M53.7056 43.7398C53.7056 43.7398 80.7952 64.2725 121.979 25.8801C158.553 -8.21513 188.417 45.5796 188.626 71.3414C188.896 104.712 152.051 131.414 169.932 153.332C187.813 175.251 134.471 211.446 105.723 180.122C69.9607 141.155 60.2726 172.816 39.8883 172.816C25.2583 172.816 -4.77848 136.507 15.5051 109.495C32.5734 86.7652 23.2646 79.2214 18.7568 71.3414C12.254 59.9758 27.6967 29.1275 53.7056 43.7398Z" |
|||
fill="url(#de-blob)" |
|||
/> |
|||
{/* Right cable */} |
|||
<path |
|||
d="M150.891 91.4016C152.078 91.843 153.375 91.8724 154.61 91.5884C159.782 90.4017 177.566 87.258 174.476 101.039C173.803 103.773 163.388 119.087 178.22 124.676C179.308 125.091 180.457 125.317 181.62 125.345C186.187 125.439 195.237 124.361 199.025 120.506C203.601 115.848 198.602 118.056 198.602 118.056C198.602 118.056 181.207 125.648 175.062 117.594C174.378 116.698 173.943 115.396 173.819 114.276C173.602 112.333 174.464 109.766 175.597 107.531C177.131 104.507 184.494 90.9013 168.508 86.938C164.826 86.061 159.477 85.2738 152.581 88.1769C148.558 89.8705 149.449 90.866 150.891 91.4016Z" |
|||
fill="url(#de-cable)" |
|||
/> |
|||
<path |
|||
d="M154.45 86.3221L149.82 88.0554C148.222 88.6536 147.411 90.434 148.009 92.032L148.239 92.6456C148.837 94.2435 150.617 95.0543 152.215 94.4562L156.846 92.7236C158.444 92.1254 159.255 90.345 158.657 88.7463L158.427 88.1334C157.829 86.5347 156.049 85.724 154.45 86.3221Z" |
|||
fill="#DADADA" |
|||
/> |
|||
{/* Right plug lines */} |
|||
<path |
|||
d="M126.027 91.3367L110.758 97.0517C109.958 97.3504 109.553 98.2406 109.853 99.0396L109.913 99.2007C110.212 99.9997 111.102 100.405 111.901 100.106L127.171 94.3913C127.97 94.0919 128.376 93.2017 128.076 92.4027L128.016 92.2416C127.717 91.4426 126.827 91.0372 126.027 91.3367Z" |
|||
fill="white" |
|||
/> |
|||
<path |
|||
d="M130.289 102.723L115.019 108.438C114.22 108.737 113.815 109.627 114.114 110.426L114.174 110.587C114.473 111.386 115.363 111.792 116.162 111.492L131.432 105.778C132.231 105.479 132.637 104.588 132.338 103.789L132.278 103.628C131.979 102.829 131.088 102.424 130.289 102.723Z" |
|||
fill="white" |
|||
/> |
|||
{/* Right plug body */} |
|||
<path |
|||
d="M144.388 83.566L132.012 88.1973C130.413 88.7955 129.603 90.5766 130.201 92.1746L134.427 103.464C135.025 105.062 136.805 105.873 138.403 105.275L150.779 100.643C152.377 100.045 153.188 98.2648 152.59 96.6661L148.365 85.3766C147.767 83.7786 145.986 82.9679 144.388 83.566Z" |
|||
fill="url(#de-plug-r1)" |
|||
/> |
|||
<path |
|||
d="M133.29 86.5274L120.914 91.1595C119.316 91.7576 118.505 93.538 119.103 95.1367L124.144 108.605C124.742 110.203 126.523 111.014 128.121 110.416L140.497 105.785C142.095 105.187 142.906 103.406 142.308 101.807L137.266 88.3387C136.668 86.74 134.888 85.9293 133.29 86.5274Z" |
|||
fill="url(#de-plug-r2)" |
|||
/> |
|||
<path |
|||
d="M135.229 83.7943L135.131 83.8303C134.066 84.2291 133.525 85.4165 133.924 86.4819L140.986 105.351C141.385 106.416 142.572 106.957 143.637 106.558L143.734 106.521C144.8 106.122 145.34 104.936 144.942 103.87L137.88 85.0016C137.481 83.9363 136.294 83.3955 135.229 83.7943Z" |
|||
fill="#DADADA" |
|||
/> |
|||
{/* Left cable */} |
|||
<path |
|||
d="M9.03455 95.8576C9.03455 95.8576 33.8893 88.8727 30.2808 104.966C29.6541 107.512 20.6335 120.622 31.3784 126.917C33.6856 128.269 36.3482 128.868 39.022 128.854C43.8159 128.829 51.4157 127.906 54.8299 124.431C59.4055 119.773 54.4068 121.982 54.4068 121.982C54.4068 121.982 40.1323 127.854 32.7886 123.173C30.2811 121.575 29.1445 118.481 29.8254 115.586C30.1735 114.148 30.7034 112.761 31.4025 111.457C32.9996 108.465 40.2989 94.8268 24.3133 90.8635C20.6308 89.9866 15.2823 89.1993 8.38595 92.1025C1.48959 95.0063 9.03455 95.8576 9.03455 95.8576Z" |
|||
fill="url(#de-cable-l)" |
|||
/> |
|||
<path |
|||
d="M58.1729 118.381L53.5429 120.114C51.9446 120.712 51.1338 122.493 51.7321 124.092L51.9616 124.704C52.5597 126.303 54.3403 127.114 55.9386 126.515L60.5686 124.782C62.167 124.184 62.9777 122.404 62.3795 120.805L62.15 120.192C61.5518 118.594 59.7712 117.783 58.1729 118.381Z" |
|||
fill="#DADADA" |
|||
/> |
|||
{/* Left plug body */} |
|||
<path |
|||
d="M65.9947 110.763L59.1328 113.332C57.5344 113.93 56.7237 115.71 57.3219 117.309L60.6307 126.149C61.2289 127.748 63.0095 128.559 64.6077 127.961L71.4697 125.392C73.068 124.794 73.8786 123.014 73.2806 121.415L69.9718 112.575C69.3736 110.976 67.593 110.165 65.9947 110.763Z" |
|||
fill="url(#de-plug-l1)" |
|||
/> |
|||
<path |
|||
d="M77.4203 103.956L67.1874 107.786C65.5891 108.385 64.7784 110.165 65.3766 111.764L70.4175 125.232C71.0157 126.831 72.7963 127.642 74.3943 127.044L84.6274 123.214C86.2261 122.616 87.0368 120.835 86.4387 119.236L81.3976 105.768C80.7994 104.169 79.019 103.358 77.4203 103.956Z" |
|||
fill="url(#de-plug-l2)" |
|||
/> |
|||
<path |
|||
d="M79.5881 101.167L79.4909 101.203C78.4249 101.602 77.8849 102.79 78.2836 103.855L85.345 122.724C85.7438 123.789 86.9312 124.33 87.9965 123.931L88.0937 123.894C89.159 123.496 89.6997 122.309 89.301 121.243L82.2388 102.375C81.8401 101.309 80.6534 100.769 79.5881 101.167Z" |
|||
fill="white" |
|||
/> |
|||
{/* Spark lines (top) */} |
|||
<path |
|||
d="M105.719 89.1464C105.35 89.1052 105.012 88.9235 104.775 88.638C104.539 88.3525 104.422 87.9869 104.449 87.6168L105.213 73.6706C105.258 72.8376 105.962 72.2046 106.76 72.2783L107.872 72.3813C108.671 72.4549 109.242 73.2053 109.131 74.0326L107.254 87.8758C107.212 88.2444 107.028 88.5821 106.742 88.819C106.455 89.0559 106.089 89.1729 105.719 89.1464Z" |
|||
fill="#BEBEBE" |
|||
/> |
|||
<path |
|||
d="M99.1255 90.3488C99.0108 90.3826 98.8908 90.3936 98.7717 90.3811C98.6532 90.3679 98.5377 90.3318 98.4332 90.2745C98.328 90.2178 98.2361 90.1406 98.161 90.0471C98.086 89.9544 98.0301 89.847 97.9962 89.733L95.2697 81.0574C95.1902 80.811 95.2123 80.5424 95.33 80.3122C95.4484 80.0811 95.653 79.9068 95.9002 79.8273L96.6057 79.614C96.7271 79.5786 96.8544 79.5691 96.9794 79.5853C97.1045 79.6015 97.2245 79.6434 97.3326 79.7089C97.4408 79.7736 97.5342 79.8604 97.607 79.9634C97.6799 80.0664 97.7306 80.1834 97.7556 80.307L99.7766 89.1959C99.8333 89.4343 99.7972 89.6859 99.6766 89.9C99.5559 90.1133 99.3595 90.2737 99.1255 90.3488Z" |
|||
fill="#BEBEBE" |
|||
/> |
|||
<path |
|||
d="M95.4472 95.3515C94.9587 95.957 93.9757 95.9497 93.2194 95.3353L79.7919 84.4254C78.9893 83.7736 78.7531 82.695 79.271 82.0535L79.9935 81.1589C80.5122 80.5166 81.576 80.5695 82.3353 81.2744L95.0411 93.0796C95.7569 93.744 95.9357 94.7468 95.4472 95.3515Z" |
|||
fill="#BEBEBE" |
|||
/> |
|||
{/* Spark lines (bottom) */} |
|||
<path |
|||
d="M101.655 123.531C102 123.665 102.281 123.927 102.437 124.264C102.593 124.6 102.612 124.984 102.492 125.334L98.1915 138.623C97.9348 139.416 97.0931 139.848 96.3397 139.573L95.2899 139.19C94.5372 138.915 94.176 138.043 94.4953 137.273L99.8447 124.367C99.9808 124.022 100.244 123.742 100.582 123.586C100.919 123.43 101.303 123.41 101.655 123.531Z" |
|||
fill="#BEBEBE" |
|||
/> |
|||
<path |
|||
d="M108.335 124.052C108.455 124.049 108.574 124.069 108.686 124.112C108.797 124.155 108.899 124.219 108.986 124.301C109.073 124.383 109.143 124.481 109.191 124.59C109.24 124.699 109.267 124.817 109.27 124.937L109.691 134.021C109.704 134.28 109.615 134.533 109.441 134.725C109.268 134.919 109.026 135.035 108.767 135.049L108.03 135.075C107.904 135.078 107.779 135.055 107.662 135.007C107.545 134.959 107.439 134.888 107.351 134.798C107.264 134.707 107.195 134.6 107.151 134.481C107.108 134.363 107.088 134.237 107.095 134.111L107.411 125.001C107.418 124.756 107.517 124.522 107.688 124.346C107.859 124.17 108.09 124.066 108.335 124.052Z" |
|||
fill="#BEBEBE" |
|||
/> |
|||
<path |
|||
d="M113.168 120.155C113.795 119.694 114.743 119.952 115.317 120.74L125.514 134.717C126.122 135.552 126.076 136.655 125.411 137.143L124.484 137.824C123.819 138.312 122.803 137.989 122.249 137.114L112.979 122.455C112.458 121.631 112.541 120.615 113.168 120.155Z" |
|||
fill="#BEBEBE" |
|||
/> |
|||
<defs> |
|||
<linearGradient |
|||
id="de-blob" |
|||
x1="64.16" |
|||
y1="22" |
|||
x2="150.74" |
|||
y2="209.05" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
<stop stopColor="#F1F1F1" /> |
|||
<stop offset="0.389" stopColor="#EAEAEA" /> |
|||
<stop offset="1" stopColor="#F3F3F3" /> |
|||
</linearGradient> |
|||
<linearGradient |
|||
id="de-cable" |
|||
x1="175.3" |
|||
y1="86.1" |
|||
x2="175.3" |
|||
y2="125.4" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
<stop stopColor="#BCBCBC" /> |
|||
<stop offset="1" stopColor="#DADADA" /> |
|||
</linearGradient> |
|||
<linearGradient |
|||
id="de-plug-r1" |
|||
x1="141.4" |
|||
y1="83.4" |
|||
x2="141.4" |
|||
y2="105.5" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
<stop stopColor="#BCBCBC" /> |
|||
<stop offset="1" stopColor="#DADADA" /> |
|||
</linearGradient> |
|||
<linearGradient |
|||
id="de-plug-r2" |
|||
x1="130.7" |
|||
y1="86.3" |
|||
x2="130.7" |
|||
y2="110.6" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
<stop stopColor="#BCBCBC" /> |
|||
<stop offset="1" stopColor="#DADADA" /> |
|||
</linearGradient> |
|||
<linearGradient |
|||
id="de-cable-l" |
|||
x1="31.1" |
|||
y1="90.1" |
|||
x2="31.1" |
|||
y2="128.9" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
<stop stopColor="#BCBCBC" /> |
|||
<stop offset="1" stopColor="#DADADA" /> |
|||
</linearGradient> |
|||
<linearGradient |
|||
id="de-plug-l1" |
|||
x1="65.3" |
|||
y1="110.6" |
|||
x2="65.3" |
|||
y2="128.2" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
<stop stopColor="#BCBCBC" /> |
|||
<stop offset="1" stopColor="#DADADA" /> |
|||
</linearGradient> |
|||
<linearGradient |
|||
id="de-plug-l2" |
|||
x1="75.9" |
|||
y1="103.8" |
|||
x2="75.9" |
|||
y2="127.2" |
|||
gradientUnits="userSpaceOnUse" |
|||
> |
|||
<stop stopColor="#BCBCBC" /> |
|||
<stop offset="1" stopColor="#DADADA" /> |
|||
</linearGradient> |
|||
</defs> |
|||
</svg> |
|||
); |
|||
} |
|||
@ -0,0 +1,186 @@ |
|||
import { |
|||
cleanup, |
|||
fireEvent, |
|||
render, |
|||
screen, |
|||
waitFor, |
|||
} from "@testing-library/react"; |
|||
import { afterEach, describe, expect, it, vi } from "vitest"; |
|||
import { QuestionSnapList } from "./question-snap-list"; |
|||
import { |
|||
computeQuestionLift, |
|||
updateQuestionKeyboardHeight, |
|||
} from "./question-viewport-coordinator"; |
|||
|
|||
vi.mock("@/translations/provider", () => ({ |
|||
useI18n: () => ({ dictionary: {} }), |
|||
})); |
|||
|
|||
describe("QuestionSnapList keyboard interaction", () => { |
|||
afterEach(() => { |
|||
cleanup(); |
|||
document.body.classList.remove("question-keyboard-open"); |
|||
document.documentElement.style.removeProperty("--question-lift-y"); |
|||
}); |
|||
|
|||
it("computes a stable occlusion lift without overshoot", () => { |
|||
expect( |
|||
computeQuestionLift({ |
|||
baseTop: 300, |
|||
baseBottom: 600, |
|||
visibleTop: 100, |
|||
visibleBottom: 500, |
|||
}), |
|||
).toBe(150); |
|||
expect( |
|||
computeQuestionLift({ |
|||
baseTop: 300, |
|||
baseBottom: 600, |
|||
visibleTop: 100, |
|||
visibleBottom: 800, |
|||
}), |
|||
).toBe(0); |
|||
}); |
|||
|
|||
it("does not treat pointer contact as input focus", () => { |
|||
render( |
|||
<QuestionSnapList> |
|||
<input aria-label="Question one" type="text" /> |
|||
</QuestionSnapList>, |
|||
); |
|||
|
|||
fireEvent.pointerDown( |
|||
screen.getByRole("textbox", { name: "Question one" }), |
|||
); |
|||
|
|||
expect(document.body).not.toHaveClass("question-keyboard-open"); |
|||
}); |
|||
|
|||
it("activates only on real focus and resets before changing questions", async () => { |
|||
render( |
|||
<QuestionSnapList> |
|||
<input aria-label="Question one" type="text" /> |
|||
<input aria-label="Question two" type="text" /> |
|||
</QuestionSnapList>, |
|||
); |
|||
|
|||
const firstInput = screen.getByRole("textbox", { name: "Question one" }); |
|||
const secondInput = document.querySelector<HTMLInputElement>( |
|||
'input[aria-label="Question two"]', |
|||
); |
|||
const content = firstInput.closest<HTMLElement>(".question-snap-content"); |
|||
vi.spyOn(content as HTMLElement, "getBoundingClientRect").mockReturnValue({ |
|||
top: 300, |
|||
bottom: 600, |
|||
left: 0, |
|||
right: 300, |
|||
width: 300, |
|||
height: 300, |
|||
x: 0, |
|||
y: 300, |
|||
toJSON: () => ({}), |
|||
}); |
|||
|
|||
firstInput.focus(); |
|||
updateQuestionKeyboardHeight(300); |
|||
await waitFor(() => { |
|||
expect(document.body).toHaveClass("question-keyboard-open"); |
|||
}); |
|||
|
|||
fireEvent.wheel(screen.getByRole("region", { name: "Questions" }), { |
|||
deltaY: 100, |
|||
}); |
|||
|
|||
await waitFor(() => { |
|||
expect(document.body).not.toHaveClass("question-keyboard-open"); |
|||
expect(firstInput).not.toHaveFocus(); |
|||
expect(secondInput).not.toHaveFocus(); |
|||
expect( |
|||
document.documentElement.style.getPropertyValue("--question-lift-y"), |
|||
).toBe("0px"); |
|||
}); |
|||
}); |
|||
|
|||
it("does not keep question keyboard state for a dialog input", async () => { |
|||
render( |
|||
<> |
|||
<QuestionSnapList> |
|||
<input aria-label="Question input" type="text" /> |
|||
</QuestionSnapList> |
|||
<div role="dialog"> |
|||
<input aria-label="Dialog search" type="text" /> |
|||
</div> |
|||
</>, |
|||
); |
|||
|
|||
const questionInput = screen.getByRole("textbox", { |
|||
name: "Question input", |
|||
}); |
|||
const content = questionInput.closest<HTMLElement>( |
|||
".question-snap-content", |
|||
); |
|||
vi.spyOn(content as HTMLElement, "getBoundingClientRect").mockReturnValue({ |
|||
top: 300, |
|||
bottom: 600, |
|||
left: 0, |
|||
right: 300, |
|||
width: 300, |
|||
height: 300, |
|||
x: 0, |
|||
y: 300, |
|||
toJSON: () => ({}), |
|||
}); |
|||
|
|||
questionInput.focus(); |
|||
updateQuestionKeyboardHeight(300); |
|||
await waitFor(() => { |
|||
expect(document.body).toHaveClass("question-keyboard-open"); |
|||
}); |
|||
|
|||
screen.getByRole("textbox", { name: "Dialog search" }).focus(); |
|||
|
|||
await waitFor(() => { |
|||
expect(document.body).not.toHaveClass("question-keyboard-open"); |
|||
}); |
|||
}); |
|||
|
|||
it("raises again when Flutter reopens the keyboard without another focusin", async () => { |
|||
render( |
|||
<QuestionSnapList> |
|||
<input aria-label="Persistent input" type="text" /> |
|||
</QuestionSnapList>, |
|||
); |
|||
|
|||
const input = screen.getByRole("textbox", { name: "Persistent input" }); |
|||
const content = input.closest<HTMLElement>(".question-snap-content"); |
|||
vi.spyOn(content as HTMLElement, "getBoundingClientRect").mockReturnValue({ |
|||
top: 300, |
|||
bottom: 600, |
|||
left: 0, |
|||
right: 300, |
|||
width: 300, |
|||
height: 300, |
|||
x: 0, |
|||
y: 300, |
|||
toJSON: () => ({}), |
|||
}); |
|||
|
|||
input.focus(); |
|||
updateQuestionKeyboardHeight(300); |
|||
await waitFor(() => |
|||
expect(document.body).toHaveClass("question-keyboard-open"), |
|||
); |
|||
|
|||
updateQuestionKeyboardHeight(0); |
|||
await waitFor(() => { |
|||
expect(document.body).not.toHaveClass("question-keyboard-open"); |
|||
expect(input).toHaveFocus(); |
|||
}); |
|||
|
|||
updateQuestionKeyboardHeight(300); |
|||
await waitFor(() => { |
|||
expect(document.body).toHaveClass("question-keyboard-open"); |
|||
expect(input).toHaveFocus(); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,257 @@ |
|||
"use client"; |
|||
|
|||
import { useEffect } from "react"; |
|||
import { viewPaddingsBridge } from "@/lib/view-paddings"; |
|||
|
|||
const KEYBOARD_THRESHOLD = 80; |
|||
const QUESTION_GAP = 16; |
|||
const LIFT_VARIABLE = "--question-lift-y"; |
|||
|
|||
let activeQuestionInput: HTMLElement | null = null; |
|||
let keyboardVisible = false; |
|||
let keyboardHeight = 0; |
|||
let lastKeyboardHeight = 0; |
|||
let compactSheetTop: number | null = null; |
|||
let currentLift = 0; |
|||
let geometryFrame: number | null = null; |
|||
let closedViewportHeight = 0; |
|||
|
|||
const NON_KEYBOARD_INPUT_TYPES = new Set([ |
|||
"button", |
|||
"checkbox", |
|||
"color", |
|||
"file", |
|||
"hidden", |
|||
"image", |
|||
"radio", |
|||
"range", |
|||
"reset", |
|||
"submit", |
|||
]); |
|||
|
|||
export type QuestionLiftGeometry = { |
|||
baseTop: number; |
|||
baseBottom: number; |
|||
visibleTop: number; |
|||
visibleBottom: number; |
|||
gap?: number; |
|||
}; |
|||
|
|||
export function computeQuestionLift({ |
|||
baseTop, |
|||
baseBottom, |
|||
visibleTop, |
|||
visibleBottom, |
|||
gap = QUESTION_GAP, |
|||
}: QuestionLiftGeometry) { |
|||
const contentHeight = baseBottom - baseTop; |
|||
const availableTop = visibleTop + gap; |
|||
const availableBottom = visibleBottom - gap; |
|||
const availableCenter = (availableTop + availableBottom) / 2; |
|||
const contentCenter = baseTop + contentHeight / 2; |
|||
const centerShift = Math.max(0, contentCenter - availableCenter); |
|||
const overlapShift = Math.max(0, baseBottom - availableBottom); |
|||
const maxShift = Math.max(0, baseTop - availableTop); |
|||
|
|||
return Math.min(Math.max(centerShift, overlapShift), maxShift); |
|||
} |
|||
|
|||
function isKeyboardInputTarget( |
|||
target: EventTarget | null, |
|||
): target is HTMLElement { |
|||
if (target instanceof HTMLTextAreaElement) { |
|||
return !target.disabled && !target.readOnly; |
|||
} |
|||
if (target instanceof HTMLInputElement) { |
|||
const type = (target.getAttribute("type") ?? "text").toLowerCase(); |
|||
return ( |
|||
!NON_KEYBOARD_INPUT_TYPES.has(type) && |
|||
!target.disabled && |
|||
!target.readOnly |
|||
); |
|||
} |
|||
return target instanceof HTMLElement && target.isContentEditable; |
|||
} |
|||
|
|||
export function isActiveQuestionKeyboardInput( |
|||
target: EventTarget | null, |
|||
): target is HTMLElement { |
|||
return ( |
|||
isKeyboardInputTarget(target) && |
|||
target.closest('.question-snap-item[aria-current="step"]') !== null |
|||
); |
|||
} |
|||
|
|||
function setLift(nextLift: number) { |
|||
currentLift = Math.max(0, nextLift); |
|||
document.documentElement.style.setProperty(LIFT_VARIABLE, `${currentLift}px`); |
|||
document.body.classList.toggle( |
|||
"question-keyboard-open", |
|||
currentLift > 0 && keyboardVisible && activeQuestionInput !== null, |
|||
); |
|||
} |
|||
|
|||
function getKeyboardTop() { |
|||
if (!keyboardVisible || !activeQuestionInput) return null; |
|||
|
|||
const flutterTop = window.innerHeight - keyboardHeight; |
|||
const viewport = window.visualViewport; |
|||
const viewportBottom = viewport |
|||
? viewport.offsetTop + viewport.height |
|||
: window.innerHeight; |
|||
|
|||
return Math.min(flutterTop, viewportBottom); |
|||
} |
|||
|
|||
function updateLift() { |
|||
const content = document.querySelector<HTMLElement>( |
|||
'.question-snap-item[aria-current="step"] .question-snap-content', |
|||
); |
|||
if (!content) { |
|||
setLift(0); |
|||
return; |
|||
} |
|||
|
|||
const keyboardTop = getKeyboardTop(); |
|||
const visibleBottomCandidates = [keyboardTop, compactSheetTop].filter( |
|||
(value): value is number => value !== null, |
|||
); |
|||
if (visibleBottomCandidates.length === 0) { |
|||
setLift(0); |
|||
return; |
|||
} |
|||
|
|||
const rect = content.getBoundingClientRect(); |
|||
const snapList = content.closest<HTMLElement>(".question-snap-list"); |
|||
const baseTop = rect.top + currentLift; |
|||
const baseBottom = rect.bottom + currentLift; |
|||
const safeTop = Number.parseFloat( |
|||
getComputedStyle(document.documentElement).getPropertyValue("--safe-top"), |
|||
); |
|||
|
|||
setLift( |
|||
computeQuestionLift({ |
|||
baseTop, |
|||
baseBottom, |
|||
visibleTop: Math.max( |
|||
Number.isFinite(safeTop) ? safeTop : 0, |
|||
snapList?.getBoundingClientRect().top ?? 0, |
|||
), |
|||
visibleBottom: Math.min(...visibleBottomCandidates), |
|||
}), |
|||
); |
|||
} |
|||
|
|||
function scheduleLiftUpdate() { |
|||
if (geometryFrame !== null) window.cancelAnimationFrame(geometryFrame); |
|||
geometryFrame = window.requestAnimationFrame(() => { |
|||
geometryFrame = null; |
|||
updateLift(); |
|||
}); |
|||
} |
|||
|
|||
export function updateQuestionKeyboardHeight(height: number) { |
|||
keyboardHeight = Math.max(0, height); |
|||
keyboardVisible = keyboardHeight > KEYBOARD_THRESHOLD; |
|||
if (keyboardVisible) { |
|||
lastKeyboardHeight = keyboardHeight; |
|||
if (isActiveQuestionKeyboardInput(document.activeElement)) { |
|||
activeQuestionInput = document.activeElement; |
|||
} |
|||
} |
|||
scheduleLiftUpdate(); |
|||
} |
|||
|
|||
export function registerCompactQuestionSheet(element: HTMLElement) { |
|||
compactSheetTop = element.getBoundingClientRect().top; |
|||
scheduleLiftUpdate(); |
|||
return () => { |
|||
compactSheetTop = null; |
|||
scheduleLiftUpdate(); |
|||
}; |
|||
} |
|||
|
|||
export function resetQuestionKeyboardState() { |
|||
if (geometryFrame !== null) { |
|||
window.cancelAnimationFrame(geometryFrame); |
|||
geometryFrame = null; |
|||
} |
|||
if (isActiveQuestionKeyboardInput(document.activeElement)) { |
|||
document.activeElement.blur(); |
|||
} |
|||
activeQuestionInput = null; |
|||
keyboardVisible = false; |
|||
keyboardHeight = 0; |
|||
compactSheetTop = null; |
|||
setLift(0); |
|||
} |
|||
|
|||
export function useQuestionViewportCoordinator() { |
|||
useEffect(() => { |
|||
closedViewportHeight = window.visualViewport?.height ?? window.innerHeight; |
|||
|
|||
const handleFocusIn = (event: FocusEvent) => { |
|||
if (!isActiveQuestionKeyboardInput(event.target)) return; |
|||
activeQuestionInput = event.target; |
|||
|
|||
// Start with the last known keyboard geometry so lifting begins in the
|
|||
// same frame as focus; Flutter then replaces it with the exact height.
|
|||
if (!keyboardVisible && lastKeyboardHeight > KEYBOARD_THRESHOLD) { |
|||
keyboardHeight = lastKeyboardHeight; |
|||
keyboardVisible = true; |
|||
} |
|||
scheduleLiftUpdate(); |
|||
}; |
|||
|
|||
const handleFocusOut = () => { |
|||
window.setTimeout(() => { |
|||
activeQuestionInput = isActiveQuestionKeyboardInput( |
|||
document.activeElement, |
|||
) |
|||
? document.activeElement |
|||
: null; |
|||
if (!activeQuestionInput) scheduleLiftUpdate(); |
|||
}, 0); |
|||
}; |
|||
|
|||
const unsubscribeConfig = viewPaddingsBridge.subscribeConfig((config) => { |
|||
updateQuestionKeyboardHeight(config.keyboardHeight); |
|||
}); |
|||
|
|||
const handleViewportResize = () => { |
|||
const viewportHeight = |
|||
window.visualViewport?.height ?? window.innerHeight; |
|||
const reduction = closedViewportHeight - viewportHeight; |
|||
|
|||
if (reduction > KEYBOARD_THRESHOLD) { |
|||
if (isActiveQuestionKeyboardInput(document.activeElement)) { |
|||
activeQuestionInput = document.activeElement; |
|||
keyboardVisible = true; |
|||
keyboardHeight = Math.max(keyboardHeight, reduction); |
|||
lastKeyboardHeight = keyboardHeight; |
|||
scheduleLiftUpdate(); |
|||
} |
|||
} else if (keyboardHeight <= KEYBOARD_THRESHOLD) { |
|||
keyboardVisible = false; |
|||
keyboardHeight = 0; |
|||
closedViewportHeight = viewportHeight; |
|||
scheduleLiftUpdate(); |
|||
} |
|||
}; |
|||
|
|||
document.addEventListener("focusin", handleFocusIn); |
|||
document.addEventListener("focusout", handleFocusOut); |
|||
window.visualViewport?.addEventListener("resize", handleViewportResize); |
|||
|
|||
return () => { |
|||
document.removeEventListener("focusin", handleFocusIn); |
|||
document.removeEventListener("focusout", handleFocusOut); |
|||
window.visualViewport?.removeEventListener( |
|||
"resize", |
|||
handleViewportResize, |
|||
); |
|||
unsubscribeConfig(); |
|||
resetQuestionKeyboardState(); |
|||
}; |
|||
}, []); |
|||
} |
|||
@ -0,0 +1,191 @@ |
|||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
|||
import { |
|||
buyHabibCoinPackages, |
|||
copyToClipboard, |
|||
downloadFile, |
|||
isInFlutterWebView, |
|||
openExternalUrl, |
|||
postActionToFlutter, |
|||
uploadFile, |
|||
} from "./webview-actions"; |
|||
|
|||
describe("webview-actions", () => { |
|||
const originalHabibApp = window.HabibApp; |
|||
|
|||
beforeEach(() => { |
|||
vi.restoreAllMocks(); |
|||
}); |
|||
|
|||
afterEach(() => { |
|||
window.HabibApp = originalHabibApp; |
|||
}); |
|||
|
|||
describe("isInFlutterWebView", () => { |
|||
it("returns true when window.HabibApp.postMessage is defined", () => { |
|||
window.HabibApp = { postMessage: vi.fn() }; |
|||
expect(isInFlutterWebView()).toBe(true); |
|||
}); |
|||
|
|||
it("returns false when window.HabibApp is undefined", () => { |
|||
delete (window as any).HabibApp; |
|||
expect(isInFlutterWebView()).toBe(false); |
|||
}); |
|||
}); |
|||
|
|||
describe("postActionToFlutter", () => { |
|||
it("sends message without data when data is omitted", () => { |
|||
const mockPostMessage = vi.fn(); |
|||
window.HabibApp = { postMessage: mockPostMessage }; |
|||
|
|||
const result = postActionToFlutter("custom_action"); |
|||
expect(result).toBe(true); |
|||
expect(mockPostMessage).toHaveBeenCalledTimes(1); |
|||
expect(mockPostMessage).toHaveBeenCalledWith( |
|||
JSON.stringify({ action: "custom_action" }), |
|||
); |
|||
}); |
|||
|
|||
it("sends message with data when data is provided", () => { |
|||
const mockPostMessage = vi.fn(); |
|||
window.HabibApp = { postMessage: mockPostMessage }; |
|||
|
|||
const result = postActionToFlutter("custom_action", { foo: "bar" }); |
|||
expect(result).toBe(true); |
|||
expect(mockPostMessage).toHaveBeenCalledWith( |
|||
JSON.stringify({ action: "custom_action", data: { foo: "bar" } }), |
|||
); |
|||
}); |
|||
|
|||
it("returns false and does not throw when not in flutter webview", () => { |
|||
delete (window as any).HabibApp; |
|||
const result = postActionToFlutter("custom_action"); |
|||
expect(result).toBe(false); |
|||
}); |
|||
}); |
|||
|
|||
describe("buyHabibCoinPackages", () => { |
|||
it("dispatches buy_coin action to Flutter and returns true", () => { |
|||
const mockPostMessage = vi.fn(); |
|||
window.HabibApp = { postMessage: mockPostMessage }; |
|||
|
|||
const result = buyHabibCoinPackages(); |
|||
expect(result).toBe(true); |
|||
expect(mockPostMessage).toHaveBeenCalledTimes(1); |
|||
expect(mockPostMessage).toHaveBeenCalledWith( |
|||
JSON.stringify({ action: "buy_coin" }), |
|||
); |
|||
}); |
|||
|
|||
it("returns false when not running in Flutter WebView", () => { |
|||
delete (window as any).HabibApp; |
|||
const result = buyHabibCoinPackages(); |
|||
expect(result).toBe(false); |
|||
}); |
|||
}); |
|||
|
|||
describe("downloadFile", () => { |
|||
it("dispatches download_file action with options", () => { |
|||
const mockPostMessage = vi.fn(); |
|||
window.HabibApp = { postMessage: mockPostMessage }; |
|||
|
|||
const result = downloadFile({ |
|||
url: "https://example.com/file.pdf", |
|||
fileName: "file.pdf", |
|||
mimeType: "application/pdf", |
|||
fileType: "document", |
|||
}); |
|||
|
|||
expect(result).toBe(true); |
|||
expect(mockPostMessage).toHaveBeenCalledWith( |
|||
JSON.stringify({ |
|||
action: "download_file", |
|||
data: { |
|||
url: "https://example.com/file.pdf", |
|||
fileName: "file.pdf", |
|||
mimeType: "application/pdf", |
|||
fileType: "document", |
|||
}, |
|||
}), |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe("copyToClipboard", () => { |
|||
it("dispatches copy_to_clipboard in Flutter WebView", async () => { |
|||
const mockPostMessage = vi.fn(); |
|||
window.HabibApp = { postMessage: mockPostMessage }; |
|||
|
|||
const result = await copyToClipboard("sample text", "invite_code"); |
|||
expect(result).toBe(true); |
|||
expect(mockPostMessage).toHaveBeenCalledWith( |
|||
JSON.stringify({ |
|||
action: "copy_to_clipboard", |
|||
data: { |
|||
text: "sample text", |
|||
label: "invite_code", |
|||
showToast: true, |
|||
}, |
|||
}), |
|||
); |
|||
}); |
|||
|
|||
it("falls back to navigator.clipboard.writeText when outside WebView", async () => { |
|||
delete (window as any).HabibApp; |
|||
const writeTextMock = vi.fn().mockResolvedValue(undefined); |
|||
Object.assign(navigator, { |
|||
clipboard: { writeText: writeTextMock }, |
|||
}); |
|||
|
|||
const result = await copyToClipboard("sample text"); |
|||
expect(result).toBe(true); |
|||
expect(writeTextMock).toHaveBeenCalledWith("sample text"); |
|||
}); |
|||
}); |
|||
|
|||
describe("openExternalUrl", () => { |
|||
it("dispatches open_external_url action to Flutter", () => { |
|||
const mockPostMessage = vi.fn(); |
|||
window.HabibApp = { postMessage: mockPostMessage }; |
|||
|
|||
const result = openExternalUrl({ url: "https://habibapp.com" }); |
|||
expect(result).toBe(true); |
|||
expect(mockPostMessage).toHaveBeenCalledWith( |
|||
JSON.stringify({ |
|||
action: "open_external_url", |
|||
data: { |
|||
mode: "externalApplication", |
|||
showErrorToast: true, |
|||
url: "https://habibapp.com", |
|||
}, |
|||
}), |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe("uploadFile", () => { |
|||
it("dispatches upload_file action with options to Flutter", () => { |
|||
const mockPostMessage = vi.fn(); |
|||
window.HabibApp = { postMessage: mockPostMessage }; |
|||
|
|||
const result = uploadFile({ |
|||
mediaType: "image", |
|||
source: "gallery", |
|||
returnAs: "upload", |
|||
uploadUrl: "/api/upload", |
|||
}); |
|||
|
|||
expect(result).toBe(true); |
|||
expect(mockPostMessage).toHaveBeenCalledWith( |
|||
JSON.stringify({ |
|||
action: "upload_file", |
|||
data: { |
|||
mediaType: "image", |
|||
source: "gallery", |
|||
returnAs: "upload", |
|||
uploadUrl: "/api/upload", |
|||
}, |
|||
}), |
|||
); |
|||
}); |
|||
}); |
|||
}); |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue