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
@@ -0,0 +1,113 @@
'use client';
import { useEffect, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { getCardById, updateCardInCollection } from '@/lib/firestore';
import type { PokemonCard } from '@/types';
import CardForm, { type CardFormInputs } from '@/components/cards/CardForm';
import { useToast } from '@/hooks/use-toast';
import { Loader2, AlertTriangle } from 'lucide-react';
export default function EditCardPage() {
const { user } = useAuth();
const router = useRouter();
const params = useParams();
const cardId = params.cardId as string;
const [card, setCard] = useState<PokemonCard | null>(null);
const [loading, setLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
useEffect(() => {
if (user && cardId) {
setLoading(true);
setError(null);
getCardById(user.uid, cardId)
.then(fetchedCard => {
if (fetchedCard) {
setCard(fetchedCard);
} else {
setError('Card not found or you do not have permission to edit it.');
toast({ title: "Error", description: "Card not found.", variant: "destructive" });
}
})
.catch(err => {
console.error("Error fetching card for edit:", err);
setError((err as Error).message || 'Failed to load card details.');
toast({ title: "Error", description: "Failed to load card details.", variant: "destructive" });
})
.finally(() => setLoading(false));
} else if (!user && !loading) { // if auth is loaded and no user
router.replace('/login');
}
}, [user, cardId, router, toast, loading]);
const handleUpdateCard = async (data: CardFormInputs) => {
if (!user || !card) return;
setIsSubmitting(true);
try {
const updatedData: Partial<PokemonCard> = {
name: data.name,
set: data.set,
rarity: data.rarity,
// imageDataUrl is part of CardFormInputs but might not change if not re-uploaded
// For simplicity, we assume imageDataUrl is handled if it's part of the form
// and if image upload was part of the edit form (which it isn't currently for simplicity)
// For this schema, imageDataUrl is part of CardFormInputs, so it will be included.
// If image editing is not part of this form, ensure data.imageDataUrl is the original one.
imageDataUrl: data.imageDataUrl,
};
await updateCardInCollection(user.uid, card.id, updatedData);
toast({ title: 'Card Updated', description: `${data.name} has been updated.` });
router.push('/dashboard/collection');
} catch (e) {
console.error('Error updating card:', e);
const errorMessage = (e instanceof Error) ? e.message : 'An unknown error occurred while updating.';
toast({ title: 'Update Error', description: errorMessage, variant: 'destructive' });
} finally {
setIsSubmitting(false);
}
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-200px)]">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
if (error) {
return (
<div className="text-center py-10 text-destructive">
<AlertTriangle className="mx-auto h-12 w-12 mb-4" />
<h2 className="text-xl font-semibold mb-2">Error Loading Card</h2>
<p>{error}</p>
</div>
);
}
if (!card) {
// This case should ideally be covered by error state, but as a fallback:
return <div className="text-center py-10">Card not found.</div>;
}
return (
<div>
<h1 className="text-3xl font-bold mb-8 text-center font-headline">Edit Card: {card.name}</h1>
<CardForm
initialData={card}
onSubmit={handleUpdateCard}
isSubmitting={isSubmitting}
submitButtonText="Save Changes"
formTitle="Update Card Information"
formDescription="Modify the details of your Pokémon card."
onCancel={() => router.push('/dashboard/collection')}
/>
</div>
);
}
+115
View File
@@ -0,0 +1,115 @@
'use client';
import { useEffect, useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
import { getUserCards, deleteCardFromCollection } from '@/lib/firestore';
import type { PokemonCard } from '@/types';
import CardItem from '@/components/cards/CardItem';
import { Button } from '@/components/ui/button';
import { Loader2, PlusCircle, AlertTriangle } from 'lucide-react';
import Link from 'next/link';
import { useToast } from '@/hooks/use-toast';
export default function CollectionPage() {
const { user } = useAuth();
const [cards, setCards] = useState<PokemonCard[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [deletingCardId, setDeletingCardId] = useState<string | null>(null);
const { toast } = useToast();
useEffect(() => {
if (user) {
fetchCards();
} else {
setLoading(false);
}
}, [user]);
const fetchCards = async () => {
if (!user) return;
setLoading(true);
setError(null);
try {
const userCards = await getUserCards(user.uid);
setCards(userCards);
} catch (e) {
console.error("Error fetching cards:", e);
setError((e as Error).message || 'Failed to load your collection. Please try again.');
toast({ title: "Error", description: "Failed to load collection.", variant: "destructive" });
} finally {
setLoading(false);
}
};
const handleDeleteCard = async (cardId: string) => {
if (!user) return;
setDeletingCardId(cardId);
try {
await deleteCardFromCollection(user.uid, cardId);
setCards(prevCards => prevCards.filter(card => card.id !== cardId));
toast({ title: "Card Deleted", description: "The card has been removed from your collection." });
} catch (e) {
console.error("Error deleting card:", e);
toast({ title: "Error", description: "Failed to delete card.", variant: "destructive" });
} finally {
setDeletingCardId(null);
}
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-[calc(100vh-200px)]">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
if (error) {
return (
<div className="text-center py-10 text-destructive">
<AlertTriangle className="mx-auto h-12 w-12 mb-4" />
<h2 className="text-xl font-semibold mb-2">Error Loading Collection</h2>
<p className="mb-4">{error}</p>
<Button onClick={fetchCards}>Try Again</Button>
</div>
);
}
return (
<div className="space-y-8">
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
<h1 className="text-3xl font-bold font-headline">My Card Collection</h1>
<Button asChild>
<Link href="/dashboard/scan">
<PlusCircle className="mr-2 h-5 w-5" /> Add New Card
</Link>
</Button>
</div>
{cards.length === 0 ? (
<div className="text-center py-10 border-2 border-dashed border-border rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-archive-restore mx-auto mb-4 text-muted-foreground"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-V8"/><path d="M9.5 12.5 12 15l2.5-2.5"/><path d="M12 15V9"/></svg>
<h2 className="text-xl font-semibold text-muted-foreground">Your collection is empty.</h2>
<p className="text-muted-foreground mt-2">Start by scanning your first Pokémon card!</p>
<Button asChild className="mt-6">
<Link href="/dashboard/scan">
<PlusCircle className="mr-2 h-5 w-5" /> Scan a Card
</Link>
</Button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{cards.map(card => (
<CardItem
key={card.id}
card={card}
onDelete={handleDeleteCard}
isDeleting={deletingCardId === card.id}
/>
))}
</div>
)}
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
import AuthGuard from '@/components/auth/AuthGuard';
import type { ReactNode } from 'react';
export default function DashboardLayout({ children }: { children: ReactNode }) {
return <AuthGuard>{children}</AuthGuard>;
}
+21
View File
@@ -0,0 +1,21 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default function DashboardPage() {
const router = useRouter();
useEffect(() => {
// For now, redirect to scan page as the primary action
router.replace('/dashboard/scan');
}, [router]);
return (
<div className="text-center">
<h1 className="text-3xl font-bold font-headline">Welcome to your Cardex Dashboard</h1>
<p className="mt-4 text-lg text-muted-foreground">
Manage your Pokémon card collection with ease.
</p>
<p className="mt-2 text-md text-muted-foreground">Redirecting to card scanner...</p>
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
import CardScanner from '@/components/cards/CardScanner';
export default function ScanCardPage() {
return (
<div>
<CardScanner />
</div>
);
}
+109
View File
@@ -0,0 +1,109 @@
'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 { sendPasswordResetEmail } 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 { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import Link from 'next/link';
import { Loader2, Mail } from 'lucide-react';
const forgotPasswordSchema = z.object({
email: z.string().email({ message: 'Invalid email address' }),
});
type ForgotPasswordInputs = z.infer<typeof forgotPasswordSchema>;
export default function ForgotPasswordPage() {
const [loading, setLoading] = useState(false);
const { toast } = useToast();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ForgotPasswordInputs>({
resolver: zodResolver(forgotPasswordSchema),
});
const onSubmit: SubmitHandler<ForgotPasswordInputs> = async (data) => {
setLoading(true);
try {
await sendPasswordResetEmail(auth, data.email);
toast({
title: 'Password reset email sent',
description: 'Please check your inbox for further instructions.',
});
} catch (error) {
console.error('Password reset error:', error);
toast({
title: 'Error sending reset email',
description: (error as Error).message,
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground font-headline">
Forgot Your Password?
</h1>
<p className="mt-2 text-center text-sm text-muted-foreground">
No worries! Enter your email and we&apos;ll send you a reset link.
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<Card className="shadow-xl">
<CardHeader>
<CardTitle className="text-xl">Reset Password</CardTitle>
<CardDescription>Enter your email address below.</CardDescription>
</CardHeader>
<CardContent>
<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>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Send Reset Link
</Button>
</form>
</CardContent>
<CardFooter className="justify-center">
<p className="text-sm text-muted-foreground">
Remembered your password?{' '}
<Link href="/login" legacyBehavior>
<a className="font-medium text-primary hover:text-primary/80">
Sign in
</a>
</Link>
</p>
</CardFooter>
</Card>
</div>
</div>
);
}
+60 -64
View File
@@ -2,79 +2,75 @@
@tailwind components;
@tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--background: 200 17% 94%; /* #ecf0f1 */
--foreground: 210 20% 20%; /* Custom dark gray for text */
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--card-foreground: 210 20% 20%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--popover-foreground: 210 20% 20%;
--primary: 207 70% 53%; /* #3498db */
--primary-foreground: 0 0% 100%; /* White for contrast on primary */
--secondary: 200 17% 85%; /* Slightly darker than background for secondary elements */
--secondary-foreground: 210 20% 20%;
--muted: 200 17% 90%;
--muted-foreground: 210 20% 40%;
--accent: 207 70% 63%; /* Lighter shade of primary for accents */
--accent-foreground: 0 0% 100%;
--destructive: 6 78% 57%; /* #e74c3c */
--destructive-foreground: 0 0% 100%;
--border: 200 17% 80%;
--input: 200 17% 88%;
--ring: 207 70% 53%; /* Primary color for rings */
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
/* Sidebar specific colors, can be adjusted if sidebar is used extensively */
--sidebar-background: 200 17% 92%;
--sidebar-foreground: 210 20% 20%;
--sidebar-primary: 207 70% 53%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
--sidebar-accent: 207 70% 63%;
--sidebar-accent-foreground: 0 0% 100%;
--sidebar-border: 200 17% 80%;
--sidebar-ring: 207 70% 53%;
}
.dark {
/* Define dark theme colors if needed, for now, focusing on light theme */
--background: 210 20% 10%;
--foreground: 200 17% 94%;
--card: 210 20% 12%;
--card-foreground: 200 17% 94%;
--popover: 210 20% 12%;
--popover-foreground: 200 17% 94%;
--primary: 207 70% 53%;
--primary-foreground: 0 0% 100%;
--secondary: 210 20% 20%;
--secondary-foreground: 200 17% 94%;
--muted: 210 20% 20%;
--muted-foreground: 200 17% 70%;
--accent: 207 70% 63%;
--accent-foreground: 0 0% 100%;
--destructive: 6 78% 57%;
--destructive-foreground: 0 0% 100%;
--border: 210 20% 25%;
--input: 210 20% 25%;
--ring: 207 70% 53%;
--sidebar-background: 210 20% 12%;
--sidebar-foreground: 200 17% 94%;
--sidebar-primary: 207 70% 53%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 207 70% 63%;
--sidebar-accent-foreground: 0 0% 100%;
--sidebar-border: 210 20% 25%;
--sidebar-ring: 207 70% 53%;
}
}
@@ -83,6 +79,6 @@ body {
@apply border-border;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground font-body;
}
}
}
+27 -8
View File
@@ -1,9 +1,17 @@
import type {Metadata} from 'next';
import type { Metadata } from 'next';
import './globals.css';
import { Inter } from 'next/font/google';
import { FirebaseProvider } from '@/components/providers/FirebaseProvider';
import { AuthProvider } from '@/components/providers/AuthProvider';
import { Toaster } from '@/components/ui/toaster';
import Navbar from '@/components/layout/Navbar';
// If you have specific font weights and styles, configure them here
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
export const metadata: Metadata = {
title: 'Firebase Studio App',
description: 'Generated by Firebase Studio',
title: 'Cardex - Pokémon Card Scanner',
description: 'Scan, identify, and manage your Pokémon card collection with Cardex.',
};
export default function RootLayout({
@@ -12,13 +20,24 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="en" className={inter.variable}>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></link>
{/* Google Fonts link is kept as per instructions, next/font is also used for Inter for better optimization */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body className="font-body antialiased">{children}</body>
<body className="font-body antialiased min-h-screen flex flex-col">
<FirebaseProvider>
<AuthProvider>
<Navbar />
<main className="flex-grow container mx-auto px-4 py-8">
{children}
</main>
<Toaster />
</AuthProvider>
</FirebaseProvider>
</body>
</html>
);
}
+80
View File
@@ -0,0 +1,80 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import LoginForm from '@/components/auth/LoginForm';
import GoogleSignInButton from '@/components/auth/GoogleSignInButton';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import Link from 'next/link';
import { Loader2 } from 'lucide-react';
export default function LoginPage() {
const { user, loading: authLoading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!authLoading && user) {
router.replace('/dashboard');
}
}, [user, authLoading, router]);
if (authLoading || (!authLoading && user)) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground font-headline">
Sign in to Cardex
</h1>
<p className="mt-2 text-center text-sm text-muted-foreground">
Access your Pokémon card collection.
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<Card className="shadow-xl">
<CardHeader>
<CardTitle className="text-xl">Welcome back</CardTitle>
<CardDescription>Enter your credentials to continue.</CardDescription>
</CardHeader>
<CardContent>
<LoginForm />
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-card px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<div className="mt-6">
<GoogleSignInButton />
</div>
</div>
</CardContent>
<CardFooter className="justify-center">
<p className="text-sm text-muted-foreground">
Don&apos;t have an account?{' '}
<Link href="/signup" legacyBehavior>
<a className="font-medium text-primary hover:text-primary/80">
Sign up
</a>
</Link>
</p>
</CardFooter>
</Card>
</div>
</div>
);
}
+26 -2
View File
@@ -1,3 +1,27 @@
export default function Home() {
return <></>;
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import { Loader2 } from 'lucide-react';
export default function HomePage() {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading) {
if (user) {
router.replace('/dashboard');
} else {
router.replace('/login');
}
}
}, [user, loading, router]);
return (
<div className="flex items-center justify-center min-h-[calc(100vh-4rem-var(--header-height,0px))]">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
+80
View File
@@ -0,0 +1,80 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
import SignupForm from '@/components/auth/SignupForm';
import GoogleSignInButton from '@/components/auth/GoogleSignInButton';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import Link from 'next/link';
import { Loader2 } from 'lucide-react';
export default function SignupPage() {
const { user, loading: authLoading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!authLoading && user) {
router.replace('/dashboard');
}
}, [user, authLoading, router]);
if (authLoading || (!authLoading && user)) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
</div>
);
}
return (
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground font-headline">
Create your Cardex account
</h1>
<p className="mt-2 text-center text-sm text-muted-foreground">
Start managing your Pokémon card collection today.
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<Card className="shadow-xl">
<CardHeader>
<CardTitle className="text-xl">Get Started</CardTitle>
<CardDescription>It&apos;s quick and easy.</CardDescription>
</CardHeader>
<CardContent>
<SignupForm />
<div className="mt-6">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<Separator />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-card px-2 text-muted-foreground">
Or sign up with
</span>
</div>
</div>
<div className="mt-6">
<GoogleSignInButton />
</div>
</div>
</CardContent>
<CardFooter className="justify-center">
<p className="text-sm text-muted-foreground">
Already have an account?{' '}
<Link href="/login" legacyBehavior>
<a className="font-medium text-primary hover:text-primary/80">
Sign in
</a>
</Link>
</p>
</CardFooter>
</Card>
</div>
</div>
);
}