mirror of
https://github.com/xavidop/cardex.git
synced 2026-09-04 16:23:19 +00:00
I see this error with the app, reported by NextJS, please fix it. The error is reported as HTML but presented visually to the user).
A > before the line number in the error source usually indicates the line of interest:
> <!DOCTYPE html><html id="__next_error__"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/%5Bturbopack%5D_browser_dev_hmr-client_hmr-client_ts_49a6ea35._.js"/><script src="/_next/static/chunks/node_modules_next_dist_compiled_2ce9398a._.js" async=""></script><script src="/_next/static/chunks/node_modules_next_dist_client_43e3ffb8._.js" async=""></script><script src="/_next/static/chunks/node_modules_next_dist_3bfaed20._.js" async=""></script><script src="/_next/static/chunks/node_modules_%40swc_helpers_cjs_00636ac3._.js" async=""></script><script src="/_next/static/chunks/_e69f0d32._.js" async=""></script><script src="/_next/static/chunks/_be317ff2._.js" async=""></script><script src="/_next/static/chunks/node_modules_next_dist_1a6ee436._.js" async=""></script><script src="/_next/static/chunks/src_app_favicon_ico_mjs_79b6a596._.js" async=""></script><meta name="robots" content="noindex"/><meta name="next-size-adjust" content=""/><meta name="next-error" content="not-found"/><!--$--><!--/$--><script src="/_next/static/chunks/node_modules_next_dist_build_polyfills_polyfill-nomodule.js" noModule=""></script></head><body><template data-next-error-message="Firebase: Error (auth/invalid-api-key)." data-next-error-digest="2253673657" data-next-error-stack="FirebaseError: Firebase: Error (auth/invalid-api-key).
> at createErrorInternal (file:///home/user/studio/.next/server/chunks/ssr/d9ef2_@firebase_auth_dist_node-esm_dec10075._.js:538:40)
> at _assert (file:///home/user/studio/.next/server/chunks/ssr/d9ef2_@firebase_auth_dist_node-esm_dec10075._.js:542:15)
> at Component.eval [as instanceFactory] (file:///home/user/studio/.next/server/chunks/ssr/d9ef2_@firebase_auth_dist_node-esm_dec10075._.js:6842:9)
> at Provider.getOrInitializeService (file:///home/user/studio/.next/server/chunks/ssr/node_modules_594ee151._.js:2456:39)
> at Provider.initialize (file:///home/user/studio/.next/server/chunks/ssr/node_modules_594ee151._.js:2403:31)
> at initializeAuth (file:///home/user/studio/.next/server/chunks/ssr/d9ef2_@firebase_auth_dist_node-esm_dec10075._.js:3404:27)
> at getAuth (file:///home/user/studio/.next/server/chunks/ssr/d9ef2_@firebase_auth_dist_node-esm_dec10075._.js:6899:18)
> at [project]/src/lib/firebase.ts [app-ssr] (ecmascript) (file:///home/user/studio/.next/server/chunks/ssr/%5Broot%20of%20the%20server%5D__4c51450b._.js:176:263)
> at instantiateModule (/home/user/studio/.next/server/chunks/ssr/[turbopack]_runtime.js:593:23)
> at getOrInstantiateModuleFromParent (/home/user/studio/.next/server/chunks/ssr/[turbopack]_runtime.js:652:12)
> at esmImport (/home/user/studio/.next/server/chunks/ssr/[turbopack]_runtime.js:132:20)
> at [project]/src/components/providers/FirebaseProvider.tsx [app-ssr] (ecmascript) (file:///home/user/studio/.next/server/chunks/ssr/%5Broot%20of%20the%20server%5D__4c51450b._.js:190:148)
> at instantiateModule (/home/user/studio/.next/server/chunks/ssr/[turbopack]_runtime.js:593:23)
> at getOrInstantiateModuleFromParent (/home/user/studio/.next/server/chunks/ssr/[turbopack]_runtime.js:652:12)
> at commonJsRequire (/home/user/studio/.next/server/chunks/ssr/[turbopack]_runtime.js:147:20)
> at require (/home/user/studio/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:443:15880)
> at requireModule (/home/user/studio/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:206:1036)
> at initializeModuleChunk (/home/user/studio/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:206:25809)
> at resolveModuleChunk (/home/user/studio/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:206:24944)
> at /home/user/studio/node_modules/next/dist/compiled/next-server/app-page.runtime.dev.js:206:54326"></template><script src="/_next/static/chunks/%5Bturbopack%5D_browser_dev_hmr-client_hmr-client_ts_49a6ea35._.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"3:\"$Sreact.fragment\"\n5:I[\"[project]/node_modules/next/dist/client/components/layout-router.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"default\"]\n6:I[\"[project]/node_modules/next/dist/client/components/render-from-template-context.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"default\"]\n8:I[\"[project]/src/components/providers/FirebaseProvider.tsx [app-client] (ecmascript)\",[\"static/chunks/[turbopack]_browser_dev_hmr-client_hmr-client_ts_39ae8672._.js\",\"static/chunks/d9ef2_@firebase_auth_dist_esm2017_bdfab715._.js\",\"static/chunks/node_modules_@firebase_firestore_dist_index_esm2017_c2fcaa2e.js\",\"static/chunks/node_modules_46e819fb._.js\",\"static/chunks/src_d3f13007._.js\",\"static/chunks/src_app_layout_tsx_f0e4c1a2._.js\"],\"FirebaseProvider\"]\n9:I[\"[project]/src/components/providers/AuthProvider.tsx [app-client] (ecmascript)\",[\"static/chunks/[turbopack]_browser_dev_hmr-client_hmr-client_ts_39ae8672._.js\",\"static/chunks/d9ef2_@firebase_auth_dist_esm2017_bdfab715._.js\",\"static/chunks/node_modules_@firebase_firestore_dist_index_esm2017_c2fcaa2e.js\",\"static/chunks/node_modules_46e819fb._.js\",\"static/chunks/src_d3f13007._.js\",\"static/chunks/src_app_layout_tsx_f0e4c1a2._.js\"],\"AuthProvider\"]\na:I[\"[project]/src/components/layout/Navbar.tsx [app-client] (ecmascript)\",[\"static/chunks/[turbopack]_browser_dev_hmr-client_hmr-client_ts_39ae8672._.js\",\"static/chunks/d9ef2_@firebase_auth_dist_esm2017_bdfab715._.js\",\"static/chunks/node_modules_@firebase_firestore_dist_index_esm2017_c2fcaa2e.js\",\"static/chunks/node_modules_46e819fb._.js\",\"static/chunks/src_d3f13007._.js\",\"static/chunks/src_app_layout_tsx_f0e4c1a2._.js\"],\"default\"]\ne:I[\"[project]/src/components/ui/toaster.tsx [app-client] (ecmascript)\",[\"static/chunks/[turbopack]_browser_dev_hmr-client_hmr-client_ts_39ae8672._.js\",\"static/chunks/d9ef2_@firebase_auth_dist_esm2017_bdfab715._.js\",\"static/c"])</script><script>self.__next_f.push([1,"hunks/node_modules_@firebase_firestore_dist_index_esm2017_c2fcaa2e.js\",\"static/chunks/node_modules_46e819fb._.js\",\"static/chunks/src_d3f13007._.js\",\"static/chunks/src_app_layout_tsx_f0e4c1a2._.js\"],\"Toaster\"]\nf:I[\"[project]/node_modules/next/dist/client/components/client-page.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"ClientPageRoot\"]\n10:I[\"[project]/src/app/page.tsx [app-client] (ecmascript)\",[\"static/chunks/[turbopack]_browser_dev_hmr-client_hmr-client_ts_39ae8672._.js\",\"static/chunks/d9ef2_@firebase_auth_dist_esm2017_bdfab715._.js\",\"static/chunks/node_modules_@firebase_firestore_dist_index_esm2017_c2fcaa2e.js\",\"static/chunks/node_modules_46e819fb._.js\",\"static/chunks/src_d3f13007._.js\",\"static/chunks/src_app_layout_tsx_f0e4c1a2._.js\",\"static/chunks/src_app_page_tsx_b025fed5._.js\",\"static/chunks/src_app_page_tsx_54ce92c9._.js\"],\"default\"]\n14:I[\"[project]/node_modules/next/dist/client/components/metadata/metadata-boundary.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"MetadataBoundary\"]\n17:I[\"[project]/node_modules/next/dist/client/components/metadata/metadata-boundary.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"OutletBoundary\"]\n1e:I[\"[project]/node_modules/next/dist/client/components/metadata/async-metadata.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"AsyncMetadataOutlet\"]\n21:I[\"[project]/node_modules/next/dist/client/components/client-segment.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"ClientSegmentRoot\"]\n22:I[\"[project]/node_modules/next/dist/client/components/error-boundary.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dis"])</script><script>self.__next_f.push([1,"t_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"default\"]\n23:I[\"[project]/node_modules/next/dist/client/components/http-access-fallback/error-boundary.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"HTTPAccessFallbackBoundary\"]\n24:I[\"[project]/node_modules/next/dist/client/components/metadata/metadata-boundary.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"ViewportBoundary\"]\n25:\"$SkResourceStore\"\n2e:\"$Sreact.suspense\"\n2f:I[\"[project]/node_modules/next/dist/client/components/metadata/async-metadata.js [app-client] (ecmascript)\",[\"static/chunks/node_modules_next_dist_1a6ee436._.js\",\"static/chunks/src_app_favicon_ico_mjs_79b6a596._.js\"],\"AsyncMetadata\"]\n:HL[\"/_next/static/chunks/%5Broot%20of%20the%20server%5D__f9b1a2bf._.css\",\"style\"]\n:HL[\"/_next/static/media/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7W0Q5nw-s.p.0faac26c.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:{\"name\":\"Preloads\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{\"preloadCallbacks\":[\"$E(()=\u003e{ctx.componentMod.preloadStyle(fullHref,ctx.renderOpts.crossOrigin,ctx.nonce)})\",\"$E(()=\u003e{ctx.componentMod.preloadFont(href,type,ctx.renderOpts.crossOrigin,ctx.nonce)})\"]}}\n1:D\"$2\"\n1:null\n7:{\"name\":\"RootLayout\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{\"children\":[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$3\",null,{\"children\":[\"$\",\"$L6\",null,{},null,[],1]},null,[],0],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$Y\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"},null,[],1],\"params\":\"$Y\"}}\n4:D\"$7\"\nc:{\"name\":\"NotFound\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{}}\nb:D\"$c\"\nd:{\"name\":\"HTTPAccessErrorFallback\",\"env\":\"Server\",\"key\":null,\"owner\":\"$c\",\"stack\":[],\"props\":{\"status\":404,\"me"])</script><script>self.__next_f.push([1,"ssage\":\"This page could not be found.\"}}\nb:D\"$d\"\nb:[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"},\"$d\",[],1],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}},\"$d\",[],1],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404},\"$d\",[],1],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"},\"$d\",[],1]},\"$d\",[],1]]},\"$d\",[],1]},\"$d\",[],1]]\n"])</script><script>self.__next_f.push([1,"4:[\"$\",\"html\",null,{\"lang\":\"en\",\"className\":\"inter_9e72d27f-module__JKMi0a__variable\",\"children\":[[\"$\",\"head\",null,{\"children\":[[\"$\",\"link\",null,{\"rel\":\"preconnect\",\"href\":\"https://fonts.googleapis.com\"},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",227,272]],1],[\"$\",\"link\",null,{\"rel\":\"preconnect\",\"href\":\"https://fonts.gstatic.com\",\"crossOrigin\":\"anonymous\"},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",235,272]],1],[\"$\",\"link\",null,{\"href\":\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700\u0026display=swap\",\"rel\":\"stylesheet\"},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",244,272]],1]]},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",225,264]],1],[\"$\",\"body\",null,{\"className\":\"font-body antialiased min-h-screen flex flex-col\",\"children\":[\"$\",\"$L8\",null,{\"children\":[\"$\",\"$L9\",null,{\"children\":[[\"$\",\"$La\",null,{},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",263,280]],1],[\"$\",\"main\",null,{\"className\":\"flex-grow container mx-auto px-4 py-8\",\"children\":[\"$\",\"$L5\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L6\",null,{},null,[],1],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[\"$b\",[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"},null,[],1]},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",268,280]],1],[\"$\",\"$Le\",null,{},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",276,280]],1]]},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",261,282]],1]},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",260,278]],1]},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",258,264]],1]]},\"$7\",[[\"RootLayout\",\"/home/user/studio/.next/server/chunks/ssr/[root of the server]__57a4c55f._.js\",221,263]],1]\n"])</script><script>self.__next_f.push([1,"12:{\"name\":\"\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{}}\n11:D\"$12\"\n13:{\"name\":\"MetadataTree\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{}}\n11:D\"$13\"\n16:{\"name\":\"__next_metadata_boundary__\",\"env\":\"Server\",\"key\":null,\"owner\":\"$13\",\"stack\":[],\"props\":{}}\n15:D\"$16\"\n11:[\"$\",\"$L14\",null,{\"children\":\"$L15\"},\"$13\",[],1]\n19:{\"name\":\"__next_outlet_boundary__\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{\"ready\":\"$E(async function getViewportReady() {\\n await viewport();\\n return undefined;\\n })\"}}\n18:D\"$19\"\n1b:{\"name\":\"__next_outlet_boundary__\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{\"ready\":\"$E(async function getMetadataReady() {\\n // Only warm up metadata() call when it's blocking metadata,\\n // otherwise it will be fully managed by AsyncMetadata component.\\n if (!serveStreamingMetadata) {\\n await metadata();\\n }\\n return undefined;\\n })\"}}\n1a:D\"$1b\"\n1d:{\"name\":\"StreamingMetadataOutlet\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{}}\n1c:D\"$1d\"\n1c:[\"$\",\"$L1e\",null,{\"promise\":\"$@1f\"},\"$1d\",[],1]\n"])</script><script>self.__next_f.push([1,"26:{\"name\":\"NonIndex\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{\"ctx\":{\"componentMod\":{\"ClientPageRoot\":\"$f\",\"ClientSegmentRoot\":\"$21\",\"GlobalError\":\"$22\",\"HTTPAccessFallbackBoundary\":\"$23\",\"LayoutRouter\":\"$5\",\"MetadataBoundary\":\"$14\",\"OutletBoundary\":\"$17\",\"Postpone\":\"$E(function Postpone({ reason, route }) {\\n const prerenderStore = __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"].getStore();\\n const dynamicTracking = prerenderStore \u0026\u0026 prerenderStore.type === 'prerender-ppr' ? prerenderStore.dynamicTracking : null;\\n postponeWithTracking(route, reason, dynamicTracking);\\n})\",\"RenderFromTemplateContext\":\"$6\",\"ViewportBoundary\":\"$24\",\"__next_app__\":{\"require\":\"$E(function () { [native code] })\",\"loadChunk\":\"$E(function () { [native code] })\"},\"actionAsyncStorage\":{\"kResourceStore\":\"$25\",\"enabled\":false},\"collectSegmentData\":\"$E(async function collectSegmentData(shouldAssumePartialData, fullPageDataBuffer, staleTime, clientModules, serverConsumerManifest, fallbackRouteParams) {\\n // Traverse the router tree and generate a prefetch response for each segment.\\n // A mutable map to collect the results as we traverse the route tree.\\n const resultMap = new Map();\\n // Before we start, warm up the module cache by decoding the page data once.\\n // Then we can assume that any remaining async tasks that occur the next time\\n // are due to hanging promises caused by dynamic data access. Note we only\\n // have to do this once per page, not per individual segment.\\n //\\n try {\\n await (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2d$server$2d$dom$2d$turbopack$2f$client$2e$edge$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"createFromReadableStream\\\"])((0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$stream$2d$utils$2f$node$2d$web$2d$streams$2d$helper$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"streamFromBuffer\\\"])(fullPageDataBuffer), {\\n serverConsumerManifest\\n });\\n await (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$lib$2f$scheduler$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"waitAtLeastOneReactRenderTask\\\"])();\\n } catch {}\\n // Create an abort controller that we'll use to stop the stream.\\n const abortController = new AbortController();\\n const onCompletedProcessingRouteTree = async ()=\u003e{\\n // Since all we're doing is decoding and re-encoding a cached prerender, if\\n // serializing the stream takes longer than a microtask, it must because of\\n // hanging promises caused by dynamic data.\\n await (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$lib$2f$scheduler$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"waitAtLeastOneReactRenderTask\\\"])();\\n abortController.abort();\\n };\\n // Generate a stream for the route tree prefetch. While we're walking the\\n // tree, we'll also spawn additional tasks to generate the segment prefetches.\\n // The promises for these tasks are pushed to a mutable array that we will\\n // await once the route tree is fully rendered.\\n const segmentTasks = [];\\n const { prelude: treeStream } = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$server$2d$dom$2d$turbopack$2d$static$2d$edge$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"unstable_prerender\\\"])(// we need to use a component so that when we decode the original stream\\n // inside of it, the side effects are transferred to the new stream.\\n // @ts-expect-error\\n /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(PrefetchTreeData, {\\n shouldAssumePartialData: shouldAssumePartialData,\\n fullPageDataBuffer: fullPageDataBuffer,\\n fallbackRouteParams: fallbackRouteParams,\\n serverConsumerManifest: serverConsumerManifest,\\n clientModules: clientModules,\\n staleTime: staleTime,\\n segmentTasks: segmentTasks,\\n onCompletedProcessingRouteTree: onCompletedProcessingRouteTree\\n }), clientModules, {\\n signal: abortController.signal,\\n onError: onSegmentPrerenderError\\n });\\n // Write the route tree to a special `/_tree` segment.\\n const treeBuffer = await (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$stream$2d$utils$2f$node$2d$web$2d$streams$2d$helper$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"streamToBuffer\\\"])(treeStream);\\n resultMap.set('/_tree', treeBuffer);\\n // Now that we've finished rendering the route tree, all the segment tasks\\n // should have been spawned. Await them in parallel and write the segment\\n // prefetches to the result map.\\n for (const [segmentPath, buffer] of (await Promise.all(segmentTasks))){\\n resultMap.set(segmentPath, buffer);\\n }\\n return resultMap;\\n})\",\"createMetadataComponents\":\"$E(function createMetadataComponents({ tree, searchParams, metadataContext, getDynamicParamFromSegment, appUsingSizeAdjustment, errorType, createServerParamsForMetadata, workStore, MetadataBoundary, ViewportBoundary, serveStreamingMetadata }) {\\n function ViewportTree() {\\n return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsxs\\\"])(__TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"Fragment\\\"], {\\n children: [\\n /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(ViewportBoundary, {\\n children: /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(Viewport, {})\\n }),\\n appUsingSizeAdjustment ? /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(\\\"meta\\\", {\\n name: \\\"next-size-adjust\\\",\\n content: \\\"\\\"\\n }) : null\\n ]\\n });\\n }\\n function MetadataTree() {\\n return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(MetadataBoundary, {\\n children: /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(Metadata, {})\\n });\\n }\\n function viewport() {\\n return getResolvedViewport(tree, searchParams, getDynamicParamFromSegment, createServerParamsForMetadata, workStore, errorType);\\n }\\n async function Viewport() {\\n try {\\n return await viewport();\\n } catch (error) {\\n if (!errorType \u0026\u0026 (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$client$2f$components$2f$http$2d$access$2d$fallback$2f$http$2d$access$2d$fallback$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"isHTTPAccessFallbackError\\\"])(error)) {\\n try {\\n return await getNotFoundViewport(tree, searchParams, getDynamicParamFromSegment, createServerParamsForMetadata, workStore);\\n } catch {}\\n }\\n // We don't actually want to error in this component. We will\\n // also error in the MetadataOutlet which causes the error to\\n // bubble from the right position in the page to be caught by the\\n // appropriate boundaries\\n return null;\\n }\\n }\\n Viewport.displayName = __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$lib$2f$metadata$2f$metadata$2d$constants$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"VIEWPORT_BOUNDARY_NAME\\\"];\\n function metadata() {\\n return getResolvedMetadata(tree, searchParams, getDynamicParamFromSegment, metadataContext, createServerParamsForMetadata, workStore, errorType);\\n }\\n async function resolveFinalMetadata() {\\n let result;\\n let error = null;\\n try {\\n result = await metadata();\\n return {\\n metadata: result,\\n error: null,\\n digest: undefined\\n };\\n } catch (metadataErr) {\\n error = metadataErr;\\n if (!errorType \u0026\u0026 (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$client$2f$components$2f$http$2d$access$2d$fallback$2f$http$2d$access$2d$fallback$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"isHTTPAccessFallbackError\\\"])(metadataErr)) {\\n try {\\n result = await getNotFoundMetadata(tree, searchParams, getDynamicParamFromSegment, metadataContext, createServerParamsForMetadata, workStore);\\n return {\\n metadata: result,\\n error,\\n digest: error == null ? void 0 : error.digest\\n };\\n } catch (notFoundMetadataErr) {\\n error = notFoundMetadataErr;\\n // In PPR rendering we still need to throw the postpone error.\\n // If metadata is postponed, React needs to be aware of the location of error.\\n if (serveStreamingMetadata \u0026\u0026 (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$lib$2f$router$2d$utils$2f$is$2d$postpone$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"isPostpone\\\"])(notFoundMetadataErr)) {\\n throw notFoundMetadataErr;\\n }\\n }\\n }\\n // In PPR rendering we still need to throw the postpone error.\\n // If metadata is postponed, React needs to be aware of the location of error.\\n if (serveStreamingMetadata \u0026\u0026 (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$lib$2f$router$2d$utils$2f$is$2d$postpone$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"isPostpone\\\"])(metadataErr)) {\\n throw metadataErr;\\n }\\n // We don't actually want to error in this component. We will\\n // also error in the MetadataOutlet which causes the error to\\n // bubble from the right position in the page to be caught by the\\n // appropriate boundaries\\n return {\\n metadata: result,\\n error,\\n digest: error == null ? void 0 : error.digest\\n };\\n }\\n }\\n async function Metadata() {\\n const promise = resolveFinalMetadata();\\n if (serveStreamingMetadata) {\\n return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(__TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"Suspense\\\"], {\\n fallback: null,\\n children: /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(__TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$client$2f$components$2f$metadata$2f$async$2d$metadata$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"AsyncMetadata\\\"], {\\n promise: promise\\n })\\n });\\n }\\n const metadataState = await promise;\\n return metadataState.metadata;\\n }\\n Metadata.displayName = __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$lib$2f$metadata$2f$metadata$2d$constants$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"METADATA_BOUNDARY_NAME\\\"];\\n async function getMetadataReady() {\\n // Only warm up metadata() call when it's blocking metadata,\\n // otherwise it will be fully managed by AsyncMetadata component.\\n if (!serveStreamingMetadata) {\\n await metadata();\\n }\\n return undefined;\\n }\\n async function getViewportReady() {\\n await viewport();\\n return undefined;\\n }\\n function StreamingMetadataOutlet() {\\n if (serveStreamingMetadata) {\\n return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$jsx$2d$runtime$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"jsx\\\"])(__TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$client$2f$components$2f$metadata$2f$async$2d$metadata$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"AsyncMetadataOutlet\\\"], {\\n promise: resolveFinalMetadata()\\n });\\n }\\n return null;\\n }\\n return {\\n ViewportTree,\\n MetadataTree,\\n getViewportReady,\\n getMetadataReady,\\n StreamingMetadataOutlet\\n };\\n})\",\"createPrerenderParamsForClientSegment\":\"$E(function createPrerenderParamsForClientSegment(underlyingParams, workStore) {\\n const prerenderStore = __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"].getStore();\\n if (prerenderStore \u0026\u0026 prerenderStore.type === 'prerender') {\\n const fallbackParams = workStore.fallbackRouteParams;\\n if (fallbackParams) {\\n for(let key in underlyingParams){\\n if (fallbackParams.has(key)) {\\n // This params object has one of more fallback params so we need to consider\\n // the awaiting of this params object \\\"dynamic\\\". Since we are in dynamicIO mode\\n // we encode this as a promise that never resolves\\n return (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$dynamic$2d$rendering$2d$utils$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"makeHangingPromise\\\"])(prerenderStore.renderSignal, '`params`');\\n }\\n }\\n }\\n }\\n // We're prerendering in a mode that does not abort. We resolve the promise without\\n // any tracking because we're just transporting a value from server to client where the tracking\\n // will be applied.\\n return Promise.resolve(underlyingParams);\\n})\",\"createPrerenderSearchParamsForClientPage\":\"$E(function createPrerenderSearchParamsForClientPage(workStore) {\\n if (workStore.forceStatic) {\\n // When using forceStatic we override all other logic and always just return an empty\\n // dictionary object.\\n return Promise.resolve({});\\n }\\n const prerenderStore = __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"].getStore();\\n if (prerenderStore \u0026\u0026 prerenderStore.type === 'prerender') {\\n // dynamicIO Prerender\\n // We're prerendering in a mode that aborts (dynamicIO) and should stall\\n // the promise to ensure the RSC side is considered dynamic\\n return (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$dynamic$2d$rendering$2d$utils$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"makeHangingPromise\\\"])(prerenderStore.renderSignal, '`searchParams`');\\n }\\n // We're prerendering in a mode that does not aborts. We resolve the promise without\\n // any tracking because we're just transporting a value from server to client where the tracking\\n // will be applied.\\n return Promise.resolve({});\\n})\",\"createServerParamsForMetadata\":\"$E(function createServerParamsForServerSegment(underlyingParams, workStore) {\\n const workUnitStore = __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"].getStore();\\n if (workUnitStore) {\\n switch(workUnitStore.type){\\n case 'prerender':\\n case 'prerender-ppr':\\n case 'prerender-legacy':\\n return createPrerenderParams(underlyingParams, workStore, workUnitStore);\\n default:\\n }\\n }\\n return createRenderParams(underlyingParams, workStore);\\n})\",\"createServerParamsForServerSegment\":\"$E(function createServerParamsForServerSegment(underlyingParams, workStore) {\\n const workUnitStore = __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"].getStore();\\n if (workUnitStore) {\\n switch(workUnitStore.type){\\n case 'prerender':\\n case 'prerender-ppr':\\n case 'prerender-legacy':\\n return createPrerenderParams(underlyingParams, workStore, workUnitStore);\\n default:\\n }\\n }\\n return createRenderParams(underlyingParams, workStore);\\n})\",\"createServerSearchParamsForMetadata\":\"$E(function createServerSearchParamsForServerPage(underlyingSearchParams, workStore) {\\n const workUnitStore = __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"].getStore();\\n if (workUnitStore) {\\n switch(workUnitStore.type){\\n case 'prerender':\\n case 'prerender-ppr':\\n case 'prerender-legacy':\\n return createPrerenderSearchParams(workStore, workUnitStore);\\n default:\\n }\\n }\\n return createRenderSearchParams(underlyingSearchParams, workStore);\\n})\",\"createServerSearchParamsForServerPage\":\"$E(function createServerSearchParamsForServerPage(underlyingSearchParams, workStore) {\\n const workUnitStore = __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"].getStore();\\n if (workUnitStore) {\\n switch(workUnitStore.type){\\n case 'prerender':\\n case 'prerender-ppr':\\n case 'prerender-legacy':\\n return createPrerenderSearchParams(workStore, workUnitStore);\\n default:\\n }\\n }\\n return createRenderSearchParams(underlyingSearchParams, workStore);\\n})\",\"createTemporaryReferenceSet\":\"$E(function(){return new WeakMap})\",\"decodeAction\":\"$E(function(body,serverManifest){var formData=new FormData,action=null;return body.forEach(function(value1,key){key.startsWith(\\\"$ACTION_\\\")?key.startsWith(\\\"$ACTION_REF_\\\")?(value1=decodeBoundActionMetaData(body,serverManifest,value1=\\\"$ACTION_\\\"+key.slice(12)+\\\":\\\"),action=loadServerReference(serverManifest,value1.id,value1.bound)):key.startsWith(\\\"$ACTION_ID_\\\")\u0026\u0026(action=loadServerReference(serverManifest,value1=key.slice(11),null)):formData.append(key,value1)}),null===action?null:action.then(function(fn){return fn.bind(null,formData)})})\",\"decodeFormState\":\"$E(function(actionResult,body,serverManifest){var keyPath=body.get(\\\"$ACTION_KEY\\\");if(\\\"string\\\"!=typeof keyPath)return Promise.resolve(null);var metaData=null;if(body.forEach(function(value1,key){key.startsWith(\\\"$ACTION_REF_\\\")\u0026\u0026(metaData=decodeBoundActionMetaData(body,serverManifest,\\\"$ACTION_\\\"+key.slice(12)+\\\":\\\"))}),null===metaData)return Promise.resolve(null);var referenceId=metaData.id;return Promise.resolve(metaData.bound).then(function(bound){return null===bound?null:[actionResult,keyPath,referenceId,bound.length-1]})})\",\"decodeReply\":\"$E(function(body,turbopackMap,options){if(\\\"string\\\"==typeof body){var form=new FormData;form.append(\\\"0\\\",body),body=form}return turbopackMap=getChunk(body=createResponse(turbopackMap,\\\"\\\",options?options.temporaryReferences:void 0,body),0),close(body),turbopackMap})\",\"pages\":[\"[project]/src/app/page.tsx\"],\"patchFetch\":\"$E(function patchFetch() {\\n return (0, __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$lib$2f$patch$2d$fetch$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"patchFetch\\\"])({\\n workAsyncStorage: __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workAsyncStorage\\\"],\\n workUnitAsyncStorage: __TURBOPACK__imported__module__$5b$externals$5d2f$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js__$5b$external$5d$__$28$next$2f$dist$2f$server$2f$app$2d$render$2f$work$2d$unit$2d$async$2d$storage$2e$external$2e$js$2c$__cjs$29$__[\\\"workUnitAsyncStorage\\\"]\\n });\\n})\",\"preconnect\":\"$E(function preconnect(href, crossOrigin, nonce) {\\n const opts = {};\\n if (typeof crossOrigin === 'string') {\\n opts.crossOrigin = crossOrigin;\\n }\\n if (typeof nonce === 'string') {\\n opts.nonce = nonce;\\n }\\n ;\\n __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$dom$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"default\\\"].preconnect(href, opts);\\n})\",\"preloadFont\":\"$E(function preloadFont(href, type, crossOrigin, nonce) {\\n const opts = {\\n as: 'font',\\n type\\n };\\n if (typeof crossOrigin === 'string') {\\n opts.crossOrigin = crossOrigin;\\n }\\n if (typeof nonce === 'string') {\\n opts.nonce = nonce;\\n }\\n __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$dom$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"default\\\"].preload(href, opts);\\n})\",\"preloadStyle\":\"$E(function preloadStyle(href, crossOrigin, nonce) {\\n const opts = {\\n as: 'style'\\n };\\n if (typeof crossOrigin === 'string') {\\n opts.crossOrigin = crossOrigin;\\n }\\n if (typeof nonce === 'string') {\\n opts.nonce = nonce;\\n }\\n __TURBOPACK__imported__module__$5b$project$5d2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$rsc$2f$react$2d$dom$2e$js__$5b$app$2d$rsc$5d$__$28$ecmascript$29$__[\\\"default\\\"].preload(href, opts);\\n})\",\"prerender\":\"$E(function(model,turbopackMap,options){return new Promise(function(resolve,reject){var request=new RequestInstance(PRERENDER,model,turbopackMap,options?options.onError:void 0,options?options.identifierPrefix:void 0,options?options.onPostpone:void 0,options?options.temporaryReferences:void 0,options?options.environmentName:void 0,options?options.filterStackFrame:void 0,function(){resolve({prelude:new ReadableStream({type:\\\"bytes\\\",start:function(){startWork(request)},pull:function(controller){startFlowing(request,controller)},cancel:function(reason){request.destination=null,abort(request,reason)}},{highWaterMark:0})})},reject);if(options\u0026\u0026options.signal){var signal=options.signal;if(signal.aborted)abort(request,signal.reason);else{var listener=function(){abort(request,signal.reason),signal.removeEventListener(\\\"abort\\\",listener)};signal.addEventListener(\\\"abort\\\",listener)}}startWork(request)})})\",\"renderToReadableStream\":\"$E(function(model,turbopackMap,options){var request=new RequestInstance(20,model,turbopackMap,options?options.onError:void 0,options?options.identifierPrefix:void 0,options?options.onPostpone:void 0,options?options.temporaryReferences:void 0,options?options.environmentName:void 0,options?options.filterStackFrame:void 0,noop,noop);if(options\u0026\u0026options.signal){var signal=options.signal;if(signal.aborted)abort(request,signal.reason);else{var listener=function(){abort(request,signal.reason),signal.removeEventListener(\\\"abort\\\",listener)};signal.addEventListener(\\\"abort\\\",listener)}}return new ReadableStream({type:\\\"bytes\\\",start:function(){startWork(request)},pull:function(controller){startFlowing(request,controller)},cancel:function(reason){request.destination=null,abort(request,reason)}},{highWaterMark:0})})\",\"routeModule\":{\"userland\":{\"loaderTree\":\"$Y\"},\"definition\":\"$Y\"},\"serverHooks\":\"$Y\",\"taintObjectReference\":\"$E(function notImplemented() {\\n throw Object.defineProperty(new Error('Taint can only be used with the taint flag.'), \\\"__NEXT_ERROR_CODE\\\", {\\n value: \\\"E354\\\",\\n enumerable: false,\\n configurable: true\\n });\\n})\",\"tree\":\"$Y\",\"workAsyncStorage\":\"$Y\",\"workUnitAsyncStorage\":\"$Y\"},\"url\":\"$Y\",\"renderOpts\":\"$Y\",\"workStore\":\"$Y\",\"parsedRequestHeaders\":\"$Y\",\"getDynamicParamFromSegment\":\"$E(function(segment){let segmentParam=getSegmentParam(segment);if(!segmentParam)return null;let key=segmentParam.param,value1=params[key];if(fallbackRouteParams\u0026\u0026fallbackRouteParams.has(segmentParam.param)?value1=fallbackRouteParams.get(segmentParam.param):Array.isArray(value1)?value1=value1.map(i=\u003eencodeURIComponent(i)):\\\"string\\\"==typeof value1\u0026\u0026(value1=encodeURIComponent(value1)),!value1){let isCatchall=\\\"catchall\\\"===segmentParam.type,isOptionalCatchall=\\\"optional-catchall\\\"===segmentParam.type;if(isCatchall||isOptionalCatchall){let dynamicParamType=dynamicParamTypes[segmentParam.type];return isOptionalCatchall?{param:key,value:null,type:dynamicParamType,treeSegment:[key,\\\"\\\",dynamicParamType]}:{param:key,value:value1=pagePath.split(\\\"/\\\").slice(1).flatMap(pathSegment=\u003e{let param=function(param){let match=param.match(PARAMETER_PATTERN);return match?parseMatchedParameter(match[2]):parseMatchedParameter(param)}(pathSegment);return params[param.key]??param.key}),type:dynamicParamType,treeSegment:[key,value1.join(\\\"/\\\"),dynamicParamType]}}}let type=function(type){let short=dynamicParamTypes[type];if(!short)throw Object.defineProperty(Error(\\\"Unknown dynamic param type\\\"),\\\"__NEXT_ERROR_CODE\\\",{value:\\\"E378\\\",enumerable:!1,configurable:!0});return short}(segmentParam.type);return{param:key,value:value1,treeSegment:[key,Array.isArray(value1)?value1.join(\\\"/\\\"):value1,type],type:type}})\",\"query\":\"$0:f:0:1:2:children:1:props:children:0:props:searchParams\",\"isPrefetch\":false,\"isAction\":false,\"requestTimestamp\":1749033108372,\"appUsingSizeAdjustment\":true,\"flightRouterState\":\"$undefined\",\"requestId\":\"NjUgkiy7OGko37blY3Dyf\",\"pagePath\":\"/\",\"clientReferenceManifest\":\"$Y\",\"assetPrefix\":\"\",\"isNotFoundPath\":false,\"nonce\":\"$undefined\",\"res\":\"$Y\",\"sharedContext\":\"$Y\"}}}\n"])</script><script>self.__next_f.push([1,"20:D\"$26\"\n20:null\n28:{\"name\":\"ViewportTree\",\"env\":\"Server\",\"key\":\"NjUgkiy7OGko37blY3Dyf\",\"owner\":null,\"stack\":[],\"props\":{}}\n27:D\"$28\"\n2a:{\"name\":\"__next_viewport_boundary__\",\"env\":\"Server\",\"key\":null,\"owner\":\"$28\",\"stack\":[],\"props\":{}}\n29:D\"$2a\"\n27:[\"$\",\"$3\",\"NjUgkiy7OGko37blY3Dyf\",{\"children\":[[\"$\",\"$L24\",null,{\"children\":\"$L29\"},\"$28\",[],1],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"},\"$28\",[],1]]},null,null,0]\n2c:{\"name\":\"\",\"env\":\"Server\",\"key\":null,\"owner\":null,\"stack\":[],\"props\":{}}\n2b:D\"$2c\"\n2b:null\n2d:[]\n0:{\"P\":\"$1\",\"b\":\"development\",\"p\":\"\",\"c\":[\"\",\"\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$3\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/%5Broot%20of%20the%20server%5D__f9b1a2bf._.css\",\"precedence\":\"next_static/chunks/[root of the server]__f9b1a2bf._.css\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"},null,[],0],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/%5Bturbopack%5D_browser_dev_hmr-client_hmr-client_ts_39ae8672._.js\",\"async\":true,\"nonce\":\"$undefined\"},null,[],0],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/d9ef2_%40firebase_auth_dist_esm2017_bdfab715._.js\",\"async\":true,\"nonce\":\"$undefined\"},null,[],0],[\"$\",\"script\",\"script-2\",{\"src\":\"/_next/static/chunks/node_modules_%40firebase_firestore_dist_index_esm2017_c2fcaa2e.js\",\"async\":true,\"nonce\":\"$undefined\"},null,[],0],[\"$\",\"script\",\"script-3\",{\"src\":\"/_next/static/chunks/node_modules_46e819fb._.js\",\"async\":true,\"nonce\":\"$undefined\"},null,[],0],[\"$\",\"script\",\"script-4\",{\"src\":\"/_next/static/chunks/src_d3f13007._.js\",\"async\":true,\"nonce\":\"$undefined\"},null,[],0],[\"$\",\"script\",\"script-5\",{\"src\":\"/_next/static/chunks/src_app_layout_tsx_f0e4c1a2._.js\",\"async\":true,\"nonce\":\"$undefined\"},null,[],0]],\"$4\"]},null,[],0],{\"children\":[\"__PAGE__\",[\"$\",\"$3\",\"c\",{\"children\":[[\"$\",\"$Lf\",null,{\"Component\":\"$10\",\"searchParams\":{},\"params\":{}},null,[],1],\"$11\",[[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/src_app_page_tsx_b025fed5._.js\",\"as"])</script><script>self.__next_f.push([1,"ync\":true,\"nonce\":\"$undefined\"},null,[],0],[\"$\",\"script\",\"script-1\",{\"src\":\"/_next/static/chunks/src_app_page_tsx_54ce92c9._.js\",\"async\":true,\"nonce\":\"$undefined\"},null,[],0]],[\"$\",\"$L17\",null,{\"children\":[\"$L18\",\"$L1a\",\"$1c\"]},null,[],1]]},null,[],0],{},null,false]},null,false],[\"$\",\"$3\",\"h\",{\"children\":[\"$20\",\"$27\",\"$2b\"]},null,[],0],false]],\"m\":\"$W2d\",\"G\":[\"$22\",\"$undefined\"],\"s\":false,\"S\":false}\n15:[\"$\",\"$2e\",null,{\"fallback\":null,\"children\":[\"$\",\"$L2f\",null,{\"promise\":\"$@30\"},\"$16\",[],1]},\"$16\",[],1]\n1a:null\n"])</script><script>self.__next_f.push([1,"30:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Cardex - Pokémon Card Scanner\"},\"$16\",[],0],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Scan, identify, and manage your Pokémon card collection with Cardex.\"},\"$16\",[],0],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico?favicon.56766c03.ico\",\"sizes\":\"48x48\",\"type\":\"image/x-icon\"},\"$16\",[],0]],\"error\":null,\"digest\":\"$undefined\"}\n1f:{\"metadata\":\"$30:metadata\",\"error\":null,\"digest\":\"$undefined\"}\n29:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"},\"$19\",[],0],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"},\"$19\",[],0]]\n18:null\n"])</script></body></html>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, type ReactNode } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthGuard({ children }: AuthGuardProps) {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || !user) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
||||
import { auth } from '@/lib/firebase';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
// Simple inline SVG for Google icon
|
||||
const GoogleIcon = () => (
|
||||
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||
<path d="M1 1h22v22H1z" fill="none"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
export default function GoogleSignInButton() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleGoogleSignIn = async () => {
|
||||
setLoading(true);
|
||||
const provider = new GoogleAuthProvider();
|
||||
try {
|
||||
await signInWithPopup(auth, provider);
|
||||
toast({ title: 'Google Sign-In successful', description: 'Redirecting to dashboard...' });
|
||||
// Redirect handled by AuthProvider or HomePage
|
||||
} catch (error) {
|
||||
console.error('Google Sign-In error:', error);
|
||||
toast({
|
||||
title: 'Google Sign-In failed',
|
||||
description: (error as Error).message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="outline" type="button" className="w-full" onClick={handleGoogleSignIn} disabled={loading}>
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <GoogleIcon />}
|
||||
Sign in with Google
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useForm, type SubmitHandler } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { signInWithEmailAndPassword } from 'firebase/auth';
|
||||
import { auth } from '@/lib/firebase';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Loader2, Mail, Lock } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
password: z.string().min(6, { message: 'Password must be at least 6 characters' }),
|
||||
});
|
||||
|
||||
type LoginFormInputs = z.infer<typeof loginSchema>;
|
||||
|
||||
export default function LoginForm() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormInputs>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await signInWithEmailAndPassword(auth, data.email, data.password);
|
||||
toast({ title: 'Login successful', description: 'Redirecting to dashboard...' });
|
||||
// Redirect handled by AuthProvider or HomePage
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
toast({
|
||||
title: 'Login failed',
|
||||
description: (error as Error).message, // More specific error from Firebase
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<div className="relative mt-1">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
{...register('email')}
|
||||
className="pl-10"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="mt-1 text-sm text-destructive">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative mt-1">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
{...register('password')}
|
||||
className="pl-10"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
{errors.password && <p className="mt-1 text-sm text-destructive">{errors.password.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<Link href="/forgot-password" legacyBehavior>
|
||||
<a className="font-medium text-primary hover:text-primary/80">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Sign In
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useForm, type SubmitHandler } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { createUserWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||
import { auth } from '@/lib/firebase';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Loader2, Mail, Lock, User } from 'lucide-react';
|
||||
|
||||
const signupSchema = z.object({
|
||||
displayName: z.string().min(2, { message: 'Display name must be at least 2 characters' }).max(50),
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
password: z.string().min(6, { message: 'Password must be at least 6 characters' }),
|
||||
});
|
||||
|
||||
type SignupFormInputs = z.infer<typeof signupSchema>;
|
||||
|
||||
export default function SignupForm() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<SignupFormInputs>({
|
||||
resolver: zodResolver(signupSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<SignupFormInputs> = async (data) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const userCredential = await createUserWithEmailAndPassword(auth, data.email, data.password);
|
||||
if (userCredential.user) {
|
||||
await updateProfile(userCredential.user, { displayName: data.displayName });
|
||||
}
|
||||
toast({ title: 'Signup successful', description: 'Redirecting to dashboard...' });
|
||||
// Redirect handled by AuthProvider or HomePage
|
||||
} catch (error) {
|
||||
console.error('Signup error:', error);
|
||||
toast({
|
||||
title: 'Signup failed',
|
||||
description: (error as Error).message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="displayName">Display Name</Label>
|
||||
<div className="relative mt-1">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="displayName"
|
||||
type="text"
|
||||
autoComplete="name"
|
||||
{...register('displayName')}
|
||||
className="pl-10"
|
||||
placeholder="Ash Ketchum"
|
||||
/>
|
||||
</div>
|
||||
{errors.displayName && <p className="mt-1 text-sm text-destructive">{errors.displayName.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<div className="relative mt-1">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
{...register('email')}
|
||||
className="pl-10"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="mt-1 text-sm text-destructive">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative mt-1">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
{...register('password')}
|
||||
className="pl-10"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
{errors.password && <p className="mt-1 text-sm text-destructive">{errors.password.message}</p>}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Create Account
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import { useForm, type SubmitHandler } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import type { ScannedCardData, PokemonCard } from '@/types';
|
||||
|
||||
const cardSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
set: z.string().min(1, 'Set is required'),
|
||||
rarity: z.string().min(1, 'Rarity is required'),
|
||||
imageDataUrl: z.string().refine(val => val.startsWith('data:image/'), {
|
||||
message: 'Image data URL is required and must be valid',
|
||||
}),
|
||||
});
|
||||
|
||||
export type CardFormInputs = z.infer<typeof cardSchema>;
|
||||
|
||||
interface CardFormProps {
|
||||
initialData?: Partial<PokemonCard> | (ScannedCardData & { imageDataUrl?: string });
|
||||
imageDataUrlFromScan?: string; // Specifically for new cards from scan
|
||||
onSubmit: (data: CardFormInputs) => Promise<void>;
|
||||
isSubmitting: boolean;
|
||||
submitButtonText?: string;
|
||||
formTitle?: string;
|
||||
formDescription?: string;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export default function CardForm({
|
||||
initialData,
|
||||
imageDataUrlFromScan,
|
||||
onSubmit: handleFormSubmit,
|
||||
isSubmitting,
|
||||
submitButtonText = 'Save Card',
|
||||
formTitle = 'Card Details',
|
||||
formDescription = 'Fill in the details of the Pokémon card.',
|
||||
onCancel,
|
||||
}: CardFormProps) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<CardFormInputs>({
|
||||
resolver: zodResolver(cardSchema),
|
||||
defaultValues: {
|
||||
name: initialData?.name || '',
|
||||
set: initialData?.set || '',
|
||||
rarity: initialData?.rarity || '',
|
||||
imageDataUrl: imageDataUrlFromScan || (initialData as PokemonCard)?.imageDataUrl || '',
|
||||
},
|
||||
});
|
||||
|
||||
const currentImageDataUrl = watch('imageDataUrl');
|
||||
|
||||
const onSubmit: SubmitHandler<CardFormInputs> = async (data) => {
|
||||
await handleFormSubmit(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-lg mx-auto shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="font-headline">{formTitle}</CardTitle>
|
||||
<CardDescription>{formDescription}</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<CardContent className="space-y-4">
|
||||
{currentImageDataUrl && (
|
||||
<div className="mb-4 rounded-md overflow-hidden border border-border aspect-[63/88] max-w-xs mx-auto bg-muted">
|
||||
<Image
|
||||
src={currentImageDataUrl}
|
||||
alt={watch('name') || 'Pokémon Card'}
|
||||
width={300}
|
||||
height={420}
|
||||
className="object-contain w-full h-full"
|
||||
data-ai-hint="pokemon card"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Hidden input for imageDataUrl, as it's typically set by scanner or pre-loaded */}
|
||||
<input type="hidden" {...register('imageDataUrl')} />
|
||||
{errors.imageDataUrl && <p className="text-sm text-destructive">{errors.imageDataUrl.message}</p>}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" {...register('name')} placeholder="e.g., Charizard" />
|
||||
{errors.name && <p className="text-sm text-destructive">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="set">Set</Label>
|
||||
<Input id="set" {...register('set')} placeholder="e.g., Base Set" />
|
||||
{errors.set && <p className="text-sm text-destructive">{errors.set.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="rarity">Rarity</Label>
|
||||
<Input id="rarity" {...register('rarity')} placeholder="e.g., Holo Rare" />
|
||||
{errors.rarity && <p className="text-sm text-destructive">{errors.rarity.message}</p>}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end space-x-2">
|
||||
{onCancel && (
|
||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import type { PokemonCard } from '@/types';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface CardItemProps {
|
||||
card: PokemonCard;
|
||||
onDelete: (cardId: string) => void;
|
||||
isDeleting: boolean;
|
||||
}
|
||||
|
||||
export default function CardItem({ card, onDelete, isDeleting }: CardItemProps) {
|
||||
return (
|
||||
<Card className="flex flex-col overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||
<CardHeader className="p-4">
|
||||
<CardTitle className="text-lg font-semibold truncate font-headline" title={card.name}>{card.name}</CardTitle>
|
||||
<CardDescription className="text-xs text-muted-foreground">
|
||||
Set: {card.set} | Rarity: {card.rarity}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 flex-grow flex justify-center items-center bg-muted/30">
|
||||
{card.imageDataUrl ? (
|
||||
<Image
|
||||
src={card.imageDataUrl}
|
||||
alt={card.name}
|
||||
width={200}
|
||||
height={280}
|
||||
className="object-contain rounded-md aspect-[63/88] max-w-[150px]"
|
||||
data-ai-hint="pokemon card"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-[200px] h-[280px] bg-gray-200 flex items-center justify-center rounded-md text-muted-foreground aspect-[63/88]">
|
||||
No Image
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="p-4 bg-card border-t flex justify-end space-x-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/dashboard/collection/${card.id}/edit`}>
|
||||
<Pencil className="h-3 w-3 mr-1" /> Edit
|
||||
</Link>
|
||||
</Button>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" disabled={isDeleting}>
|
||||
<Trash2 className="h-3 w-3 mr-1" /> Delete
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the card "{card.name}" from your collection.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => onDelete(card.id)} className="bg-destructive hover:bg-destructive/90">
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
'use client';
|
||||
|
||||
import { useState, type ChangeEvent, useRef } from 'react';
|
||||
import { scanPokemonCard, type ScanPokemonCardOutput } from '@/ai/flows/scan-pokemon-card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Loader2, Upload, AlertCircle, CheckCircle } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import CardForm, { type CardFormInputs } from './CardForm';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { addCardToCollection } from '@/lib/firestore';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../ui/card';
|
||||
|
||||
export default function CardScanner() {
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [scanResult, setScanResult] = useState<ScanPokemonCardOutput | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
const [imageDataUrl, setImageDataUrl] = useState<string | null>(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
const { user } = useAuth();
|
||||
const router = useRouter();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
// Reset previous scan state
|
||||
setScanResult(null);
|
||||
setError(null);
|
||||
setShowForm(false);
|
||||
setImageDataUrl(null);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const dataUrl = reader.result as string;
|
||||
setImagePreview(dataUrl); // For preview
|
||||
setImageDataUrl(dataUrl); // For sending to AI
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScan = async () => {
|
||||
if (!imageDataUrl) {
|
||||
toast({ title: 'No image selected', description: 'Please select an image to scan.', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsScanning(true);
|
||||
setError(null);
|
||||
setScanResult(null);
|
||||
setShowForm(false);
|
||||
|
||||
try {
|
||||
const result = await scanPokemonCard({ photoDataUri: imageDataUrl });
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
toast({ title: 'Scan Failed', description: result.error, variant: 'destructive' });
|
||||
} else if (result.cardDetails) {
|
||||
setScanResult(result);
|
||||
setShowForm(true);
|
||||
toast({ title: 'Scan Successful', description: 'Card details identified. Please review and save.', variant: 'default' });
|
||||
} else {
|
||||
setError('Unexpected response from scanner.');
|
||||
toast({ title: 'Scan Error', description: 'Unexpected response from scanner.', variant: 'destructive' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Scan error:', e);
|
||||
const errorMessage = (e instanceof Error) ? e.message : 'An unknown error occurred during scanning.';
|
||||
setError(errorMessage);
|
||||
toast({ title: 'Scan Error', description: errorMessage, variant: 'destructive' });
|
||||
} finally {
|
||||
setIsScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveCard = async (data: CardFormInputs) => {
|
||||
if (!user) {
|
||||
toast({ title: 'Authentication Error', description: 'You must be logged in to save cards.', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
if (!imageDataUrl) { // Ensure image data is still available
|
||||
toast({ title: 'Image Error', description: 'Image data is missing. Please re-upload.', variant: 'destructive'});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const cardToSave = {
|
||||
name: data.name,
|
||||
set: data.set,
|
||||
rarity: data.rarity,
|
||||
imageDataUrl: data.imageDataUrl, // Use image from form state, which should be original scan
|
||||
};
|
||||
await addCardToCollection(user.uid, cardToSave);
|
||||
toast({ title: 'Card Saved!', description: `${data.name} has been added to your collection.` });
|
||||
// Reset state after successful save
|
||||
setImagePreview(null);
|
||||
setImageDataUrl(null);
|
||||
setScanResult(null);
|
||||
setShowForm(false);
|
||||
setError(null);
|
||||
if(fileInputRef.current) fileInputRef.current.value = ""; // Clear file input
|
||||
router.push('/dashboard/collection');
|
||||
} catch (e) {
|
||||
console.error('Error saving card:', e);
|
||||
const errorMessage = (e instanceof Error) ? e.message : 'An unknown error occurred while saving.';
|
||||
toast({ title: 'Save Error', description: errorMessage, variant: 'destructive' });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelForm = () => {
|
||||
setShowForm(false);
|
||||
// Optionally clear scanResult or keep it if user might want to re-open form
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-2xl mx-auto">
|
||||
<Card className="shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-headline">Scan Pokémon Card</CardTitle>
|
||||
<CardDescription>Upload an image of your Pokémon card to identify its details.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="card-image" className="text-base">Upload Card Image</Label>
|
||||
<Input
|
||||
id="card-image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
ref={fileInputRef}
|
||||
className="mt-2 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-primary/10 file:text-primary hover:file:bg-primary/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePreview && (
|
||||
<div className="mt-4 p-4 border border-dashed border-border rounded-lg bg-muted/50">
|
||||
<h3 className="text-lg font-medium mb-2 text-center">Image Preview</h3>
|
||||
<Image
|
||||
src={imagePreview}
|
||||
alt="Card preview"
|
||||
width={300}
|
||||
height={420}
|
||||
className="rounded-md object-contain mx-auto shadow-md aspect-[63/88] max-w-xs"
|
||||
data-ai-hint="pokemon card"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{imageDataUrl && (
|
||||
<Button onClick={handleScan} disabled={isScanning || !imageDataUrl} className="w-full mt-4 text-lg py-6">
|
||||
{isScanning ? (
|
||||
<Loader2 className="mr-2 h-6 w-6 animate-spin" />
|
||||
) : (
|
||||
<Upload className="mr-2 h-6 w-6" />
|
||||
)}
|
||||
{isScanning ? 'Scanning...' : 'Scan Card'}
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{error && !showForm && (
|
||||
<Card className="border-destructive bg-destructive/10 shadow-lg">
|
||||
<CardHeader className="flex flex-row items-center space-x-3">
|
||||
<AlertCircle className="h-6 w-6 text-destructive" />
|
||||
<CardTitle className="text-destructive">Scan Failed</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-destructive-foreground">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showForm && scanResult?.cardDetails && imageDataUrl && (
|
||||
<CardForm
|
||||
initialData={{
|
||||
name: scanResult.cardDetails.name || '',
|
||||
set: scanResult.cardDetails.set || '',
|
||||
rarity: scanResult.cardDetails.rarity || '',
|
||||
}}
|
||||
imageDataUrlFromScan={imageDataUrl}
|
||||
onSubmit={handleSaveCard}
|
||||
isSubmitting={isSubmitting}
|
||||
submitButtonText="Add to Collection"
|
||||
formTitle="Confirm Card Details"
|
||||
formDescription="Review the scanned details and save the card to your collection."
|
||||
onCancel={handleCancelForm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { LogIn, LogOut, UserCircle, ScanLine, BookOpen } from 'lucide-react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { signOut } from 'firebase/auth';
|
||||
import { auth } from '@/lib/firebase';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
|
||||
export default function Navbar() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await signOut(auth);
|
||||
toast({ title: 'Signed out successfully' });
|
||||
router.push('/login');
|
||||
} catch (error) {
|
||||
console.error('Sign out error:', error);
|
||||
toast({ title: 'Error signing out', description: (error as Error).message, variant: 'destructive' });
|
||||
}
|
||||
};
|
||||
|
||||
const getInitials = (name?: string | null) => {
|
||||
if (!name) return 'U';
|
||||
const names = name.split(' ');
|
||||
if (names.length > 1) {
|
||||
return names[0][0] + names[names.length - 1][0];
|
||||
}
|
||||
return names[0].substring(0, 2);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-card border-b border-border shadow-sm">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<Link href={user ? "/dashboard" : "/"} className="text-2xl font-headline font-bold text-primary hover:opacity-80 transition-opacity">
|
||||
Cardex
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{!loading && user && (
|
||||
<>
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href="/dashboard/scan">
|
||||
<ScanLine className="mr-2 h-4 w-4" /> Scan Card
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href="/dashboard/collection">
|
||||
<BookOpen className="mr-2 h-4 w-4" /> My Collection
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!loading && (
|
||||
user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={user.photoURL || undefined} alt={user.displayName || user.email || 'User'} />
|
||||
<AvatarFallback>{getInitials(user.displayName || user.email)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{user.displayName || 'User'}
|
||||
</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleSignOut} className="cursor-pointer">
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<span>Log out</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button asChild>
|
||||
<Link href="/login">
|
||||
<LogIn className="mr-2 h-4 w-4" /> Login
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useState, useEffect, type ReactNode } from 'react';
|
||||
import { onAuthStateChanged, type User } from 'firebase/auth';
|
||||
import { auth } from '@/lib/firebase';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
|
||||
setUser(currentUser);
|
||||
setLoading(false);
|
||||
});
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import React, { type ReactNode, useEffect, useState } from 'react';
|
||||
import { app } from '@/lib/firebase'; // This initializes Firebase
|
||||
|
||||
export function FirebaseProvider({ children }: { children: ReactNode }) {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// The 'app' import initializes Firebase.
|
||||
// This effect ensures that we acknowledge initialization.
|
||||
if (app) {
|
||||
setInitialized(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!initialized) {
|
||||
// You can return a loader here if needed
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user