"use client"; /** * /u/me shared layout — auth gate + Instagram-style profile shell. * * • Anonymous visitors trigger the global AuthModal and are bounced * home if they dismiss it (unchanged from before). * • Renders the ProfileHeader + ProfileTabs above every /u/me/* page. * Tabs: Posts / Collections / Bookmarks / Mentions. * * The old left ProfileSidebar is gone. The "Edit profile" affordance * moved onto the header as a button that toggles edit mode on /u/me * via a query param (kept simple — no new route for the form). */ import { Suspense, useEffect, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { AtSign, Bookmark, FolderClosed, LayoutGrid, } from "lucide-react"; import { useAuth } from "@/components/auth/auth-provider"; import { useAuthGate } from "@/components/auth/auth-gate"; import { profile } from "@/lib/social"; import { ProfileHeader } from "@/components/profile/profile-header"; import { ProfileTabs, type ProfileTab } from "@/components/profile/profile-tabs"; export default function MeLayout({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(); const { requireAuth } = useAuthGate(); const router = useRouter(); useEffect(() => { if (loading || user) return; let cancelled = false; (async () => { const ok = await requireAuth(); if (!ok && !cancelled) router.replace("/"); })(); return () => { cancelled = true; }; }, [loading, user, requireAuth, router]); // Own profile counts (posts / followers / following) for the header // stat line. /auth/me doesn't carry these, so we read them from the // public profile endpoint (which returns no relationship for self). const [counts, setCounts] = useState<{ posts: number; followers: number; following: number; } | null>(null); const handle = user?.handle; useEffect(() => { if (!handle) return; let cancelled = false; profile .get(handle) .then((p) => { if (!cancelled) { setCounts({ posts: p.post_count, followers: p.follower_count, following: p.following_count, }); } }) .catch(() => { /* counts are non-critical; leave the stat line minimal on error */ }); return () => { cancelled = true; }; }, [handle]); if (loading || !user) { return (

{loading ? "Loading…" : "Sign in to view your library."}

); } const tabs: ProfileTab[] = [ { href: "/u/me", label: "Posts", icon: , activeFor: ["/u/me/posts"], }, { href: "/u/me/collections", label: "Collections", icon: , }, { href: "/u/me/bookmarks", label: "Bookmarks", icon: , }, { href: "/u/me/mentions", label: "Mentions", icon: , }, ]; return (
{/* 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(); // Hide tabs when the user is editing their profile inline — the strip // is visual noise that distracts from the task they navigated in to do. const editMode = searchParams.get("edit") === "1"; return editMode ? null : ; }