113 lines
3.6 KiB
TypeScript
113 lines
3.6 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).
|
|
* • ReadingSettings gear — for every signed-in viewer, regardless of
|
|
* whose profile they're on. SiteHeader drops its own mobile gear
|
|
* for auth users (see comment there) so this is the single source
|
|
* of truth for that affordance once you're signed in.
|
|
*
|
|
* Anonymous visitors don't see this row at all — they read SiteHeader's
|
|
* gear instead.
|
|
*/
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { ReadingSettings } from "@/components/reading-settings";
|
|
import { useAuth } from "@/components/auth/auth-provider";
|
|
|
|
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. */
|
|
editHref?: string;
|
|
}) {
|
|
const { user, loading } = useAuth();
|
|
// ReadingSettings appears for every authenticated viewer (own profile
|
|
// OR someone else's), so an auth user reading a friend's profile can
|
|
// still reach theme/palette/font controls. Anonymous viewers fall back
|
|
// to SiteHeader's mobile gear.
|
|
const showReadingSettings = !loading && !!user;
|
|
const showActions = !!editHref || showReadingSettings;
|
|
|
|
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 ? <ReadingSettings /> : 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>
|
|
);
|
|
}
|