fix: standardize public avatar thumbnails

This commit is contained in:
Simo committed 2026-08-10 18:30:25 +02:00
1 parent bf24d9575a
commit adc201bfb6
15 files changed
+111 -203

No files matched your search

+3 -11
View File
@@ -5,9 +5,9 @@ import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { removeFriendship } from "@/actions/messenger"; import { removeFriendship } from "@/actions/messenger";
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui"; import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, MessengerFriendships, User } from "@/lib/db"; import { db, MessengerFriendships, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
export const metadata = { title: "Friends" }; export const metadata = { title: "Friends" };
@@ -118,10 +118,6 @@ export default async function FriendsPage({
) : ( ) : (
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}> <div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
{friends.map((friend) => { {friends.map((friend) => {
const avatar = avatarImageUrl(friend.look, {
size: "s",
headOnly: true,
});
const isOnline = friend.online === "1"; const isOnline = friend.online === "1";
return ( return (
<div <div
@@ -133,13 +129,9 @@ export default async function FriendsPage({
alignItems: "center", alignItems: "center",
}} }}
> >
{/* eslint-disable-next-line @next/next/no-img-element */} <UserAvatarThumbnail
<img figure={friend.look}
className="avatar"
src={avatar}
alt={`${friend.username} avatar`} alt={`${friend.username} avatar`}
width={50}
height={50}
/> />
<div style={{ minWidth: 0, flex: 1 }}> <div style={{ minWidth: 0, flex: 1 }}>
<h3 style={{ margin: "0 0 0.3rem", fontSize: "1rem" }}> <h3 style={{ margin: "0 0 0.3rem", fontSize: "1rem" }}>
+3 -8
View File
@@ -3,8 +3,8 @@ import Link from "next/link";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui"; import { ContentCard, EmptyState, StatBlock } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { db, Guilds, GuildsMembers, User } from "@/lib/db"; import { db, Guilds, GuildsMembers, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
type GuildView = { type GuildView = {
@@ -202,14 +202,9 @@ export default async function GuildPage({
alignItems: "center", alignItems: "center",
}} }}
> >
{/* eslint-disable-next-line @next/next/no-img-element */} <UserAvatarThumbnail
<img figure={m.look}
className="avatar"
src={avatarImageUrl(m.look)}
alt={`${m.username} avatar`} alt={`${m.username} avatar`}
width={50}
height={75}
style={{ flex: "0 0 auto" }}
/> />
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
<h3 style={{ margin: "0 0 0.2rem", fontSize: "1rem" }}> <h3 style={{ margin: "0 0 0.2rem", fontSize: "1rem" }}>
+3 -11
View File
@@ -2,8 +2,8 @@ import { desc, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { db, User, UsersCurrency, UsersSettings } from "@/lib/db"; import { db, User, UsersCurrency, UsersSettings } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
export const metadata = { title: "Leaderboard" }; export const metadata = { title: "Leaderboard" };
@@ -206,23 +206,15 @@ export default async function LeaderboardPage({
</thead> </thead>
<tbody> <tbody>
{rows.map((row, i) => { {rows.map((row, i) => {
const avatar = avatarImageUrl(row.look, {
size: "s",
headOnly: true,
});
return ( return (
<tr key={row.username}> <tr key={row.username}>
<td> <td>
<RankBadge position={i + 1} /> <RankBadge position={i + 1} />
</td> </td>
<td> <td>
{/* eslint-disable-next-line @next/next/no-img-element */} <UserAvatarThumbnail
<img figure={row.look}
className="avatar"
src={avatar}
alt={`${row.username} avatar`} alt={`${row.username} avatar`}
width={40}
height={40}
/> />
</td> </td>
<td> <td>
+7 -19
View File
@@ -7,10 +7,10 @@ import { LoginForm } from "@/components/auth/login-form";
import { Clock } from "@/components/clock"; import { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher"; import { LanguageSwitcher } from "@/components/language-switcher";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { ThemeSwitcher } from "@/components/theme-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { db, User } from "@/lib/db"; import { db, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha"; import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
@@ -227,17 +227,11 @@ export default async function LoginPage() {
"color-mix(in srgb, var(--color-primary) 4%, transparent)", "color-mix(in srgb, var(--color-primary) 4%, transparent)",
}} }}
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 2,
size: "s",
})}
alt="" alt=""
width={26} options={{ direction: 2 }}
height={32}
className="rounded-md" className="rounded-md"
unoptimized
/> />
<span <span
className="text-[8px] font-bold truncate w-full text-center leading-tight" className="text-[8px] font-bold truncate w-full text-center leading-tight"
@@ -298,17 +292,11 @@ export default async function LoginPage() {
"color-mix(in srgb, var(--color-primary) 4%, transparent)", "color-mix(in srgb, var(--color-primary) 4%, transparent)",
}} }}
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 2,
size: "s",
})}
alt="" alt=""
width={26} options={{ direction: 2 }}
height={32}
className="rounded-md" className="rounded-md"
unoptimized
/> />
<span <span
className="text-[8px] font-bold truncate w-full text-center leading-tight" className="text-[8px] font-bold truncate w-full text-center leading-tight"
+3 -8
View File
@@ -6,6 +6,7 @@ import type { CSSProperties, ReactNode } from "react";
import { claimReferral } from "@/actions/referral"; import { claimReferral } from "@/actions/referral";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { import {
db, db,
@@ -496,16 +497,10 @@ export default async function MePage({
textDecoration: "none", textDecoration: "none",
}} }}
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(friend.look, { figure={friend.look}
size: "s",
headOnly: true,
})}
alt="" alt=""
width={40}
height={50}
className="rounded-lg shrink-0" className="rounded-lg shrink-0"
unoptimized
/> />
<div className="min-w-0"> <div className="min-w-0">
<p <p
+5 -21
View File
@@ -9,6 +9,7 @@ import {
sendOfflineMessage, sendOfflineMessage,
} from "@/actions/messenger"; } from "@/actions/messenger";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { import {
db, db,
@@ -17,7 +18,6 @@ import {
MessengerOffline, MessengerOffline,
User, User,
} from "@/lib/db"; } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
type SearchParams = Promise<{ type SearchParams = Promise<{
@@ -270,10 +270,6 @@ export default async function MessagesPage({
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}> <div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
{requests.map((req) => { {requests.map((req) => {
const from = userById.get(req.userFromId); const from = userById.get(req.userFromId);
const avatar = avatarImageUrl(from?.look ?? "", {
size: "s",
headOnly: true,
});
return ( return (
<div <div
key={req.id} key={req.id}
@@ -284,13 +280,9 @@ export default async function MessagesPage({
alignItems: "center", alignItems: "center",
}} }}
> >
{/* eslint-disable-next-line @next/next/no-img-element */} <UserAvatarThumbnail
<img figure={from?.look ?? ""}
className="avatar"
src={avatar}
alt={`${from?.username ?? "User"} avatar`} alt={`${from?.username ?? "User"} avatar`}
width={50}
height={50}
/> />
<div style={{ minWidth: 0, flex: 1 }}> <div style={{ minWidth: 0, flex: 1 }}>
<p style={{ margin: 0 }}> <p style={{ margin: 0 }}>
@@ -360,10 +352,6 @@ export default async function MessagesPage({
<div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}> <div className="grid" style={{ padding: "1rem", gap: "0.75rem" }}>
{messages.map((msg) => { {messages.map((msg) => {
const from = userById.get(msg.userFromId); const from = userById.get(msg.userFromId);
const avatar = avatarImageUrl(from?.look ?? "", {
size: "s",
headOnly: true,
});
const when = formatDate(new Date(msg.sendedOn * 1000)); const when = formatDate(new Date(msg.sendedOn * 1000));
return ( return (
<article <article
@@ -375,13 +363,9 @@ export default async function MessagesPage({
alignItems: "flex-start", alignItems: "flex-start",
}} }}
> >
{/* eslint-disable-next-line @next/next/no-img-element */} <UserAvatarThumbnail
<img figure={from?.look ?? ""}
className="avatar"
src={avatar}
alt={`${from?.username ?? "User"} avatar`} alt={`${from?.username ?? "User"} avatar`}
width={50}
height={50}
/> />
<div style={{ minWidth: 0, flex: 1 }}> <div style={{ minWidth: 0, flex: 1 }}>
<p <p
+7 -19
View File
@@ -10,12 +10,12 @@ import { HomeLoginForm } from "@/components/auth/home-login-form";
import { Clock } from "@/components/clock"; import { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher"; import { LanguageSwitcher } from "@/components/language-switcher";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { ThemeSwitcher } from "@/components/theme-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher";
import { TypewriterText } from "@/components/typewriter-text"; import { TypewriterText } from "@/components/typewriter-text";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { CameraWeb, db, Rooms, User, WebsiteArticles } from "@/lib/db"; import { CameraWeb, db, Rooms, User, WebsiteArticles } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha"; import { captchaConfig } from "@/lib/services/captcha";
@@ -478,16 +478,10 @@ export default async function Home() {
className="flex flex-col items-center gap-1" className="flex flex-col items-center gap-1"
> >
<div className="relative overflow-visible"> <div className="relative overflow-visible">
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 3,
})}
alt={u.username} alt={u.username}
width={64} options={{ direction: 3 }}
height={78}
style={{ marginTop: "-8px" }}
unoptimized
/> />
</div> </div>
<span <span
@@ -656,17 +650,11 @@ export default async function Home() {
"color-mix(in srgb, var(--color-primary) 5%, transparent)", "color-mix(in srgb, var(--color-primary) 5%, transparent)",
}} }}
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 2,
size: "s",
})}
alt={u.username} alt={u.username}
width={30} options={{ direction: 2 }}
height={36}
className="rounded-md" className="rounded-md"
unoptimized
/> />
<span <span
className="text-[9px] font-bold truncate max-w-[50px] text-center leading-tight" className="text-[9px] font-bold truncate max-w-[50px] text-center leading-tight"
+3 -11
View File
@@ -2,8 +2,8 @@ import { sql } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui"; import { ContentCard, EmptyState, RankBadge } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
type LeaderRow = { type LeaderRow = {
username: string; username: string;
@@ -56,23 +56,15 @@ export default async function RadioLeaderboardPage() {
</thead> </thead>
<tbody> <tbody>
{rows.map((row, i) => { {rows.map((row, i) => {
const avatar = avatarImageUrl(row.look, {
size: "s",
headOnly: true,
});
return ( return (
<tr key={row.username}> <tr key={row.username}>
<td> <td>
<RankBadge position={i + 1} /> <RankBadge position={i + 1} />
</td> </td>
<td> <td>
{/* eslint-disable-next-line @next/next/no-img-element */} <UserAvatarThumbnail
<img figure={row.look}
className="avatar"
src={avatar}
alt={`${row.username} avatar`} alt={`${row.username} avatar`}
width={40}
height={40}
/> />
</td> </td>
<td> <td>
+4 -14
View File
@@ -3,9 +3,9 @@ import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { postShout } from "@/actions/radio-shouts"; import { postShout } from "@/actions/radio-shouts";
import { ContentCard, EmptyState } from "@/components/public/ui"; import { ContentCard, EmptyState } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db, RadioShouts, User } from "@/lib/db"; import { db, RadioShouts, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
type SearchParams = Promise<{ posted?: string; error?: string }>; type SearchParams = Promise<{ posted?: string; error?: string }>;
@@ -135,12 +135,6 @@ export default async function RadioShoutsPage({
<div className="card-grid"> <div className="card-grid">
{shouts.map((s) => { {shouts.map((s) => {
const author = authorById.get(Number(s.userId)); const author = authorById.get(Number(s.userId));
const authorAvatar = author
? avatarImageUrl(author.look, {
size: "s",
headOnly: true,
})
: null;
return ( return (
<div <div
key={String(s.id)} key={String(s.id)}
@@ -151,14 +145,10 @@ export default async function RadioShoutsPage({
alignItems: "flex-start", alignItems: "flex-start",
}} }}
> >
{authorAvatar ? ( {author ? (
/* eslint-disable-next-line @next/next/no-img-element */ <UserAvatarThumbnail
<img figure={author.look}
className="avatar"
src={authorAvatar}
alt={author?.username ?? "author"} alt={author?.username ?? "author"}
width={40}
height={40}
/> />
) : null} ) : null}
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
+3 -11
View File
@@ -1,5 +1,4 @@
import { desc } from "drizzle-orm"; import { desc } from "drizzle-orm";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -8,8 +7,8 @@ import {
OnlineBadge, OnlineBadge,
RankBadge, RankBadge,
} from "@/components/public/ui"; } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { db, User } from "@/lib/db"; import { db, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
export const metadata = { title: "Rankings" }; export const metadata = { title: "Rankings" };
@@ -50,9 +49,6 @@ export default async function RankingsPage() {
) : ( ) : (
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}> <div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
{users.map((u, i) => { {users.map((u, i) => {
const avatar = avatarImageUrl(u.look, {
size: "m",
});
return ( return (
<div <div
key={u.username} key={u.username}
@@ -64,13 +60,9 @@ export default async function RankingsPage() {
}} }}
> >
<RankBadge position={i + 1} /> <RankBadge position={i + 1} />
<Image <UserAvatarThumbnail
className="avatar" figure={u.look}
src={avatar}
alt={`${u.username} avatar`} alt={`${u.username} avatar`}
width={50}
height={90}
unoptimized
/> />
<div style={{ minWidth: 0, flex: 1 }}> <div style={{ minWidth: 0, flex: 1 }}>
<h3 style={{ margin: "0 0 0.25rem", fontSize: "1rem" }}> <h3 style={{ margin: "0 0 0.25rem", fontSize: "1rem" }}>
+7 -18
View File
@@ -7,10 +7,10 @@ import { RegisterForm } from "@/components/auth/register-form";
import { Clock } from "@/components/clock"; import { Clock } from "@/components/clock";
import { LanguageSwitcher } from "@/components/language-switcher"; import { LanguageSwitcher } from "@/components/language-switcher";
import { Reveal } from "@/components/motion-reveal"; import { Reveal } from "@/components/motion-reveal";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { ThemeSwitcher } from "@/components/theme-switcher"; import { ThemeSwitcher } from "@/components/theme-switcher";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { db, User } from "@/lib/db"; import { db, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { captchaConfig } from "@/lib/services/captcha"; import { captchaConfig } from "@/lib/services/captcha";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
@@ -224,17 +224,11 @@ export default async function RegisterPage() {
"color-mix(in srgb, var(--color-primary) 4%, transparent)", "color-mix(in srgb, var(--color-primary) 4%, transparent)",
}} }}
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 2,
size: "s",
})}
alt="" alt=""
width={26} options={{ direction: 2 }}
height={32}
className="rounded-md" className="rounded-md"
unoptimized
/> />
<span <span
className="text-[8px] font-bold truncate w-full text-center leading-tight" className="text-[8px] font-bold truncate w-full text-center leading-tight"
@@ -295,16 +289,11 @@ export default async function RegisterPage() {
"color-mix(in srgb, var(--color-primary) 4%, transparent)", "color-mix(in srgb, var(--color-primary) 4%, transparent)",
}} }}
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 2,
})}
alt="" alt=""
width={52} options={{ direction: 2 }}
height={64}
className="rounded-md" className="rounded-md"
unoptimized
/> />
<span <span
className="text-[10px] font-bold truncate w-full text-center leading-tight" className="text-[10px] font-bold truncate w-full text-center leading-tight"
+4 -10
View File
@@ -1,9 +1,8 @@
import { and, eq, like } from "drizzle-orm"; import { and, eq, like } from "drizzle-orm";
import type { Metadata } from "next"; import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { db, Rooms, User } from "@/lib/db"; import { db, Rooms, User } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
export const metadata: Metadata = { title: "Search" }; export const metadata: Metadata = { title: "Search" };
@@ -134,15 +133,10 @@ export default async function SearchPage({
"color-mix(in srgb, var(--color-primary) 4%, transparent)", "color-mix(in srgb, var(--color-primary) 4%, transparent)",
}} }}
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 2,
})}
alt={u.username} alt={u.username}
width={40} options={{ direction: 2 }}
height={48}
unoptimized
className="rounded-lg" className="rounded-lg"
/> />
<span <span
+5 -24
View File
@@ -12,6 +12,7 @@ import {
OnlineBadge, OnlineBadge,
StatBlock, StatBlock,
} from "@/components/public/ui"; } from "@/components/public/ui";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { import {
CameraWeb, CameraWeb,
@@ -477,17 +478,7 @@ export default async function ProfilePage({
textAlign: "center", textAlign: "center",
}} }}
> >
{/* eslint-disable-next-line @next/next/no-img-element */} <UserAvatarThumbnail figure={f.look} alt={f.username} />
<img
src={avatarImageUrl(f.look, {
size: "m",
headOnly: true,
})}
alt={f.username}
width={48}
height={48}
style={{ display: "block" }}
/>
<span <span
style={{ style={{
fontSize: "0.8rem", fontSize: "0.8rem",
@@ -680,12 +671,6 @@ export default async function ProfilePage({
<div className="grid" style={{ gap: "0.75rem" }}> <div className="grid" style={{ gap: "0.75rem" }}>
{guestbook.map((g) => { {guestbook.map((g) => {
const author = authorById.get(g.userId); const author = authorById.get(g.userId);
const authorAvatar = author
? avatarImageUrl(author.look, {
size: "s",
headOnly: true,
})
: null;
return ( return (
<div <div
key={String(g.id)} key={String(g.id)}
@@ -696,14 +681,10 @@ export default async function ProfilePage({
alignItems: "flex-start", alignItems: "flex-start",
}} }}
> >
{authorAvatar ? ( {author ? (
/* eslint-disable-next-line @next/next/no-img-element */ <UserAvatarThumbnail
<img figure={author.look}
className="avatar"
src={authorAvatar}
alt={author?.username ?? "author"} alt={author?.username ?? "author"}
width={40}
height={40}
/> />
) : null} ) : null}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
@@ -0,0 +1,46 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
const compactCallSites = new Map<string, number>([
["src/components/top-header.tsx", 2],
["src/app/(site)/friends/page.tsx", 1],
["src/app/(site)/guilds/[id]/page.tsx", 1],
["src/app/(site)/leaderboard/page.tsx", 1],
["src/app/(site)/login/page.tsx", 2],
["src/app/(site)/me/page.tsx", 1],
["src/app/(site)/messages/page.tsx", 2],
["src/app/(site)/page.tsx", 2],
["src/app/(site)/radio/leaderboard/page.tsx", 1],
["src/app/(site)/radio/shouts/page.tsx", 1],
["src/app/(site)/rankings/page.tsx", 1],
["src/app/(site)/register/page.tsx", 2],
["src/app/(site)/search/page.tsx", 1],
["src/app/(site)/u/[username]/page.tsx", 2],
]);
function source(path: string): string {
return readFileSync(resolve(process.cwd(), path), "utf8");
}
describe("public avatar presentation contract", () => {
it.each([...compactCallSites])(
"uses the shared thumbnail in %s",
(path, expectedCount) => {
const matches = source(path).match(/<UserAvatarThumbnail\b/g) ?? [];
expect(matches).toHaveLength(expectedCount);
},
);
it("preserves explicit full-avatar previews", () => {
expect(source("src/app/(site)/me/page.tsx")).toContain(
"avatarImageUrl(user.look",
);
expect(source("src/app/(site)/settings/page.tsx")).toContain(
"avatarImageUrl(user.look",
);
expect(source("src/app/(site)/u/[username]/page.tsx")).toContain(
"avatarImageUrl(user.look",
);
});
});
+8 -18
View File
@@ -4,6 +4,7 @@ import Link from "next/link";
import type { Session } from "next-auth"; import type { Session } from "next-auth";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { RoomQuickEntry } from "@/components/room-quick-entry"; import { RoomQuickEntry } from "@/components/room-quick-entry";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { signOut } from "@/lib/auth"; import { signOut } from "@/lib/auth";
import { cached } from "@/lib/cache"; import { cached } from "@/lib/cache";
import { import {
@@ -13,7 +14,6 @@ import {
User, User,
UsersCurrency, UsersCurrency,
} from "@/lib/db"; } from "@/lib/db";
import { avatarImageUrl } from "@/lib/format";
import { resolveHotelName } from "@/lib/hotel-name"; import { resolveHotelName } from "@/lib/hotel-name";
import { canAccess, getApiAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getApiAdminContext, PERMS } from "@/lib/permissions";
@@ -77,7 +77,6 @@ export async function TopHeader({ session }: { session: Session | null }) {
} }
} catch {} } catch {}
const avatar = avatarImageUrl(look, { headOnly: true, direction: 2 });
let showAdmin = false; let showAdmin = false;
let showMod = false; let showMod = false;
const ctx = await getApiAdminContext(); const ctx = await getApiAdminContext();
@@ -318,16 +317,13 @@ export async function TopHeader({ session }: { session: Session | null }) {
aria-label="User menu" aria-label="User menu"
className="list-none cursor-pointer flex items-center [&::-webkit-details-marker]:hidden" className="list-none cursor-pointer flex items-center [&::-webkit-details-marker]:hidden"
> >
<Image <UserAvatarThumbnail
className="bg-no-repeat bg-center" figure={look}
src={avatar}
alt="" alt=""
width={54} options={{ direction: 2 }}
height={62}
unoptimized
/> />
<span <span
className="hidden sm:inline -ml-2 font-semibold" className="hidden sm:inline font-semibold"
style={{ color: textColor }} style={{ color: textColor }}
> >
{session.user.name} {session.user.name}
@@ -387,16 +383,10 @@ export async function TopHeader({ session }: { session: Session | null }) {
href="/messages" href="/messages"
className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs transition-colors hover:bg-white/10" className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs transition-colors hover:bg-white/10"
> >
<Image <UserAvatarThumbnail
src={avatarImageUrl(u.look, { figure={u.look}
headOnly: true,
direction: 2,
size: "s",
})}
alt="" alt=""
width={20} options={{ direction: 2 }}
height={24}
unoptimized
/> />
<span style={{ color: textColor }}>{u.username}</span> <span style={{ color: textColor }}>{u.username}</span>
</Link> </Link>