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:
Xavier Portilla Edo
2025-06-04 10:32:19 +00:00
parent 87f51161ee
commit 3e9fbd9b35
32 changed files with 1840 additions and 77 deletions
+31
View File
@@ -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>
);
}
+100
View File
@@ -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>
);
}
+111
View File
@@ -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>
);
}
+124
View File
@@ -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>
);
}
+82
View File
@@ -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 &quot;{card.name}&quot; 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>
);
}
+204
View File
@@ -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>
);
}
+111
View File
@@ -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>
);
}
+40
View File
@@ -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}</>;
}