"use client"; /** * ProfileHeader — Instagram-style centered header used at the top of * /u/me and /u/[handle]. Avatar, display name, @handle, a single stat * line, and an action row. * * The action row hosts: * • "Edit profile" link — only on the owner's own profile (via the * `editHref` prop set by /u/me layout). * • Reading-settings gear — also owner-only. We use `editHref` as the * "is this my profile?" signal (the /u/me layout passes it; the * /u/[handle] layout does not), so the two buttons stay in lockstep * and visitors on someone else's profile see a clean row with no * personal-settings affordances. The gear links to /settings (the * dedicated settings page) rather than opening a popover — * centred-row popovers tend to spill off the left edge on narrow * viewports. * * Clicking the avatar (when there's an uploaded image) opens a portal * modal showing the full-size image inside a circular crop. Tap the * backdrop or press Escape to close. * * Auth users on someone else's profile reach reading settings via the * sidebar rail on desktop. Anonymous visitors use SiteHeader's mobile * gear. */ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import Image from "next/image"; import Link from "next/link"; import { Settings, X } from "lucide-react"; import { cn } from "@/lib/utils"; import type { RelationshipResult, RelationshipState } from "@/lib/social"; import { ProfileActions } from "@/components/profile/profile-actions"; import { FollowListDrawer, type FollowMode, } from "@/components/profile/follow-list-drawer"; export function ProfileHeader({ avatarURL, name, handle, postCount, joinedAt, editHref, followerCount, followingCount, relationship, enableActions = false, country, }: { avatarURL: string | null; name: string; handle: string; postCount?: number; joinedAt?: string; /** ISO-2 account country, surfaced in the "More info" modal. */ country?: string | null; /** When provided, shows an "Edit profile" CTA below the handle AND * the reading-settings gear. Acts as the canonical "this is my own * profile" signal across the action row. */ editHref?: string; /** Accepted-follower / accepted-following totals for the stat line. * Omit to hide those two stats. */ followerCount?: number; followingCount?: number; /** The viewer's relationship to this profile (null for anonymous * viewers). Only consumed when `enableActions` is set. */ relationship?: RelationshipState | null; /** Render the follow / block / report action row — set on OTHER users' * profiles (not the owner's /u/me, which shows Edit/Settings). Mount * this header with key={handle} so the live count + relationship state * below re-seeds cleanly when navigating between profiles. */ enableActions?: boolean; }) { // Owner affordances ride on `editHref`; the follow-action row rides on // `enableActions`. Mutually exclusive by construction. const showOwnerControls = !!editHref; const [zoomOpen, setZoomOpen] = useState(false); // Live counts + relationship — seeded from props, updated in place after // a follow/accept/block action so the stat line and buttons stay in sync // without a refetch. Re-seeds per profile via the call-site key. const [followers, setFollowers] = useState(followerCount ?? 0); const [following, setFollowing] = useState(followingCount ?? 0); const [rel, setRel] = useState(relationship ?? null); // Which follow list (if any) is open in the drawer. const [followList, setFollowList] = useState(null); const handleResult = (r: RelationshipResult) => { setRel(r.relationship); setFollowers(r.follower_count); setFollowing(r.following_count); }; const showCounts = typeof followerCount === "number"; return ( <>
setZoomOpen(true) : undefined} />

{name}

@{handle}

{/* Stat line: posts · followers · following */}
{typeof postCount === "number" ? ( ) : null} {showCounts ? ( <> setFollowList("followers")} /> setFollowList("following")} /> ) : null}
{showOwnerControls ? (
Edit profile
) : enableActions ? ( ) : null}
{avatarURL ? ( setZoomOpen(false)} /> ) : null} {showCounts ? ( setFollowList(null)} /> ) : null} ); } /** One inline profile stat — bold count over a muted label. Renders as a * button (opening the follow list) when `onClick` is provided, else a plain * div (e.g. the posts count). */ function Stat({ n, label, onClick, }: { n: number; label: string; onClick?: () => void; }) { const inner = ( <> {n} {label} ); if (!onClick) { return (
{inner}
); } return ( ); } function Avatar({ src, name, onZoom, }: { src: string | null; name: string; /** Provided when the avatar should be interactive — the parent shows * the zoom modal on click. Omit (or pass undefined) to render the * initials placeholder as a plain non-clickable element. */ onZoom?: () => void; }) { if (src) { return ( ); } const initials = name .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase() || "?"; return (
{initials}
); } /** * AvatarZoomModal — portaled full-screen overlay with the avatar shown * inside a large circular crop. Click the backdrop or press Escape to * close. Body scroll is locked while open so the page underneath * doesn't jitter when the modal mounts. */ function AvatarZoomModal({ src, name, open, onClose, }: { src: string; name: string; open: boolean; onClose: () => void; }) { useEffect(() => { if (!open) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, onClose]); useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open]); if (!open || typeof document === "undefined") return null; return createPortal(
{/* Stop-propagation so clicks on the image itself don't dismiss the modal — only the surrounding backdrop closes. */}
e.stopPropagation()} className="relative rounded-full overflow-hidden ring-1 ring-white/20 shadow-2xl" style={{ // Square box capped at 80% of the smaller viewport dimension — // keeps the circle large but always fully visible with a // comfortable margin on tiny screens. width: "min(80vw, 80vh, 28rem)", height: "min(80vw, 80vh, 28rem)", }} > {name}
, document.body, ); }