feat(profile): enhance avatar handling and improve layout for profile header
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
type ProfileHeaderProps = {
|
type ProfileHeaderProps = {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -7,34 +10,67 @@ type ProfileHeaderProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ProfileHeader({ displayName, username, bio, avatarUrl }: ProfileHeaderProps) {
|
export function ProfileHeader({ displayName, username, bio, avatarUrl }: ProfileHeaderProps) {
|
||||||
const words = displayName.trim().split(/\s+/).filter(Boolean);
|
const [avatarFailed, setAvatarFailed] = useState(false);
|
||||||
const initials =
|
|
||||||
words.length >= 2
|
const initials = useMemo(() => {
|
||||||
? `${words[0]?.[0] ?? ""}${words[1]?.[0] ?? ""}`.toUpperCase()
|
const words = displayName.trim().split(/\s+/).filter(Boolean);
|
||||||
: (words[0]?.slice(0, 2) ?? username.slice(0, 2)).toUpperCase();
|
if (words.length >= 2) {
|
||||||
|
return `${words[0]?.[0] ?? ""}${words[1]?.[0] ?? ""}`.toUpperCase();
|
||||||
|
}
|
||||||
|
if (words.length === 1) {
|
||||||
|
return (words[0]?.[0] ?? username[0] ?? "?").toUpperCase();
|
||||||
|
}
|
||||||
|
return (username[0] ?? "?").toUpperCase();
|
||||||
|
}, [displayName, username]);
|
||||||
|
|
||||||
|
const normalizedAvatarUrl = avatarUrl?.trim() ?? "";
|
||||||
|
const hasValidAvatarUrl = useMemo(() => {
|
||||||
|
if (!normalizedAvatarUrl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const url = new URL(normalizedAvatarUrl);
|
||||||
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Reject placeholder-like values such as "https://..."
|
||||||
|
if (!url.hostname || url.hostname === "..." || !url.hostname.includes(".")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [normalizedAvatarUrl]);
|
||||||
|
|
||||||
|
const shouldShowAvatar = hasValidAvatarUrl && !avatarFailed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="terminal-card space-y-4">
|
<header className="terminal-card space-y-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-start">
|
||||||
{avatarUrl ? (
|
{shouldShowAvatar ? (
|
||||||
<img
|
<img
|
||||||
src={avatarUrl}
|
src={normalizedAvatarUrl}
|
||||||
alt={`${displayName} avatar`}
|
alt={`${displayName} avatar`}
|
||||||
className="h-16 w-16 rounded-full border border-border object-cover"
|
className="shrink-0 rounded-sm border border-border object-cover"
|
||||||
|
style={{ width: 160, height: 160 }}
|
||||||
|
onError={() => setAvatarFailed(true)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-16 w-16 items-center justify-center rounded-full border border-border bg-panel text-lg font-semibold text-muted">
|
<div
|
||||||
|
className="flex shrink-0 items-center justify-center rounded-sm border border-border bg-panel text-[5.5rem] font-semibold leading-none text-muted md:text-[6.5rem]"
|
||||||
|
style={{ width: 160, height: 160 }}
|
||||||
|
>
|
||||||
{initials}
|
{initials}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="min-h-[160px] space-y-2 pt-1" style={{ marginLeft: 40 }}>
|
||||||
<h1 className="text-3xl font-bold leading-tight">{displayName}</h1>
|
<h1 className="text-5xl font-bold leading-tight tracking-[0.01em]">{displayName}</h1>
|
||||||
<p className="text-sm text-muted">@{username}</p>
|
<p className="text-xl text-muted">@{username}</p>
|
||||||
|
{bio ? <p className="max-w-2xl pt-2 text-sm leading-relaxed text-muted">{bio}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bio ? <p className="max-w-2xl text-sm leading-relaxed text-muted">{bio}</p> : null}
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user