132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
"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.
|
|
*
|
|
* Auth users on someone else's profile reach reading settings via the
|
|
* sidebar rail on desktop. Anonymous visitors use SiteHeader's mobile
|
|
* gear.
|
|
*/
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { Settings } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export function ProfileHeader({
|
|
avatarURL,
|
|
name,
|
|
handle,
|
|
postCount,
|
|
joinedAt,
|
|
editHref,
|
|
}: {
|
|
avatarURL: string | null;
|
|
name: string;
|
|
handle: string;
|
|
postCount?: number;
|
|
joinedAt?: string;
|
|
/** 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;
|
|
}) {
|
|
// Both action-row affordances are owner-only and ride on the same
|
|
// `editHref` signal — when it's absent we render no action row at
|
|
// all, which keeps visitor profile views clean.
|
|
const showReadingSettings = !!editHref;
|
|
const showActions = !!editHref;
|
|
|
|
return (
|
|
<header className="flex flex-col items-center text-center gap-4 pt-4 pb-6 sm:pt-6 sm:pb-8">
|
|
<Avatar src={avatarURL} name={name} />
|
|
<div className="space-y-1">
|
|
<h1 className="font-[family-name:var(--font-title)] font-semibold text-xl sm:text-2xl text-foreground leading-tight">
|
|
{name}
|
|
</h1>
|
|
<p className="text-sm text-muted-foreground">@{handle}</p>
|
|
{typeof postCount === "number" || joinedAt ? (
|
|
<p className="text-xs text-muted-foreground">
|
|
{typeof postCount === "number" ? (
|
|
<>
|
|
<span className="tabular-nums">{postCount}</span> post
|
|
{postCount === 1 ? "" : "s"}
|
|
</>
|
|
) : null}
|
|
{typeof postCount === "number" && joinedAt ? " · " : null}
|
|
{joinedAt ? <>Joined {new Date(joinedAt).toLocaleDateString()}</> : null}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
{showActions ? (
|
|
<div className="flex items-center gap-2">
|
|
{editHref ? (
|
|
<Link
|
|
href={editHref}
|
|
className="inline-flex items-center justify-center h-9 px-5 rounded-lg border border-border text-sm font-medium text-foreground hover:bg-muted transition-colors"
|
|
>
|
|
Edit profile
|
|
</Link>
|
|
) : null}
|
|
{showReadingSettings ? (
|
|
<Link
|
|
href="/settings"
|
|
aria-label="Settings"
|
|
title="Settings"
|
|
className={cn(
|
|
"size-9 inline-flex items-center justify-center rounded-lg",
|
|
"border border-border text-foreground hover:bg-muted",
|
|
"transition-colors outline-none",
|
|
"focus-visible:ring-2 focus-visible:ring-ring",
|
|
)}
|
|
>
|
|
<Settings className="size-4" />
|
|
</Link>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
</header>
|
|
);
|
|
}
|
|
|
|
function Avatar({ src, name }: { src: string | null; name: string }) {
|
|
if (src) {
|
|
return (
|
|
<Image
|
|
src={src}
|
|
alt={name}
|
|
width={112}
|
|
height={112}
|
|
className="size-24 sm:size-28 rounded-full object-cover ring-1 ring-foreground/10"
|
|
unoptimized
|
|
/>
|
|
);
|
|
}
|
|
const initials =
|
|
name
|
|
.split(/\s+/)
|
|
.map((p) => p[0])
|
|
.slice(0, 2)
|
|
.join("")
|
|
.toUpperCase() || "?";
|
|
return (
|
|
<div className="size-24 sm:size-28 rounded-full bg-primary/15 text-primary flex items-center justify-center text-3xl font-semibold ring-1 ring-foreground/10">
|
|
{initials}
|
|
</div>
|
|
);
|
|
}
|