diff --git a/app/u/me/layout.tsx b/app/u/me/layout.tsx index 7701c505..bb67afbc 100644 --- a/app/u/me/layout.tsx +++ b/app/u/me/layout.tsx @@ -13,7 +13,7 @@ * via a query param (kept simple — no new route for the form). */ -import { useEffect } from "react"; +import { Suspense, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { AtSign, @@ -30,11 +30,6 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(); const { requireAuth } = useAuthGate(); const router = useRouter(); - const searchParams = useSearchParams(); - // /u/me?edit=1 swaps the Posts grid for the inline edit form (see - // app/u/me/page.tsx). The tab strip would just be in the way while the - // user is editing — hide it for that one URL state. - const editMode = searchParams.get("edit") === "1"; useEffect(() => { if (loading || user) return; @@ -91,8 +86,25 @@ export default function MeLayout({ children }: { children: React.ReactNode }) { joinedAt={user.created_at} editHref="/u/me?edit=1" /> - {editMode ? null : } + {/* Tab strip hides on /u/me?edit=1 (see ConditionalTabs). The + Suspense boundary is required by Next.js because useSearchParams + forces a CSR bailout during prerender. Without it, every + /u/me/* route fails to build. Fallback renders the tabs as-is + so the visual default (tabs visible) is what users see while + the search params resolve. */} + }> + +
{children}
); } + +function ConditionalTabs({ tabs }: { tabs: ProfileTab[] }) { + const searchParams = useSearchParams(); + // /u/me?edit=1 swaps the Posts grid for the inline edit form (see + // app/u/me/page.tsx). The tab strip would just be in the way while the + // user is editing — hide it for that one URL state. + const editMode = searchParams.get("edit") === "1"; + return editMode ? null : ; +} diff --git a/app/u/me/page.tsx b/app/u/me/page.tsx index d5cc23c0..7a8f0334 100644 --- a/app/u/me/page.tsx +++ b/app/u/me/page.tsx @@ -12,7 +12,7 @@ * shareable while avoiding a separate route + layout. */ -import { useEffect, useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { LayoutGrid } from "lucide-react"; import { useAuth } from "@/components/auth/auth-provider"; @@ -22,6 +22,21 @@ import { ConfirmModal } from "@/components/ui/confirm-modal"; import { posts, SocialError, type Post } from "@/lib/social"; export default function MyProfilePage() { + // useSearchParams() forces a CSR bailout during prerender; the + // Suspense boundary is required by Next.js so the build doesn't fail + // (the same wrapping is in app/u/me/layout.tsx for its own check). + // The fallback matches the page's idle loading state so the brief + // moment before search params resolve doesn't flash an empty section. + return ( + Loading…

} + > + +
+ ); +} + +function MyProfilePageInner() { const { user } = useAuth(); const router = useRouter(); const searchParams = useSearchParams();