"use client"; /** * /u/[handle] shared layout — public profile shell. * * Mirrors /u/me's layout so visitors see the same ProfileHeader + tab * strip whether they're on the grid (`/u/`) or the scroll feed * (`/u//posts`). Without this layout, /u//posts rendered a bare * `
` with extra padding — visually disconnected from /u/ and * narrower than expected on mobile. * * Profile data is fetched once here and exposed via PublicProfileContext * so child pages (which need `public_collections`, etc.) don't refetch. * * If the logged-in user lands on their own /u/, redirect to * /u/me — preserving the `/posts` sub-path when present. */ import { use, useEffect, useState } from "react"; import { notFound, usePathname, useRouter } from "next/navigation"; import { AtSign, FolderClosed, LayoutGrid } from "lucide-react"; import { profile, SocialError, type PublicProfile } from "@/lib/social"; import { useAuth } from "@/components/auth/auth-provider"; import { ProfileHeader } from "@/components/profile/profile-header"; import { ProfileTabs, type ProfileTab, } from "@/components/profile/profile-tabs"; import { PublicProfileContext } from "./profile-context"; export default function PublicProfileLayout(props: { params: Promise<{ handle: string }>; children: React.ReactNode; }) { const { handle } = use(props.params); const router = useRouter(); const pathname = usePathname(); const { user } = useAuth(); // Store the request handle alongside the result. The view derives // `loading` from "do we have a result whose handle matches the current // route?" — that lets us skip a synchronous setLoading(true) inside the // fetch effect (which would trigger a cascading render on every mount). type Result = | { handle: string; data: PublicProfile; missing: false } | { handle: string; data: null; missing: true }; const [result, setResult] = useState(null); const isOwn = !!user && user.handle.toLowerCase() === handle.toLowerCase(); useEffect(() => { if (!isOwn) return; // Preserve whatever sub-path the visitor is on so /u//collections // jumps to /u/me/collections (and so on for /posts, /mentions). const m = pathname.match(/^\/u\/[^/]+(\/.*)?$/); const suffix = m?.[1] ?? ""; router.replace(`/u/me${suffix}`); }, [isOwn, router, pathname]); useEffect(() => { if (isOwn) return; let cancelled = false; profile .get(handle) .then((p) => { if (!cancelled) setResult({ handle, data: p, missing: false }); }) .catch((err) => { if ( !cancelled && err instanceof SocialError && err.status === 404 ) { setResult({ handle, data: null, missing: true }); } }); return () => { cancelled = true; }; }, [handle, isOwn]); // A result for a previous handle is treated as "still loading the new // one" — so when the route changes we automatically show the spinner // again without any setState during render or in an effect body. const fresh = result?.handle === handle ? result : null; const missing = fresh?.missing === true; const data = fresh && fresh.missing === false ? fresh.data : null; const loading = fresh === null; if (missing) notFound(); if (isOwn) return null; if (loading || !data) { return (

Loading profile…

); } const tabs: ProfileTab[] = [ { href: `/u/${data.handle}`, label: "Posts", icon: , activeFor: [`/u/${data.handle}/posts`], }, { href: `/u/${data.handle}/collections`, label: "Collections", icon: , }, { href: `/u/${data.handle}/mentions`, label: "Tagged", icon: , }, ]; return (
{props.children}
); }