Fix avatar display bugs in navbar, standings, and draft views (#387)

- Remove rounded clipping on navbar user avatar button (was showing dark circular background)
- Fix "My Avatar" preview in team settings showing circular instead of square shape
- Pass owner avatar data through to TeamAvatar in standings, draft room, and draft board so teams with avatarType="owner" correctly show the user's avatar instead of the generated flag
- Consolidate user lookups in draft board loader to avoid duplicate DB queries
- Fix DraftSlotWithTeam interface to use RawFlagConfig type and include logoUrl/flagConfig/avatarType fields

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-05-06 17:03:34 -07:00 committed by GitHub
parent 1bd87c7fb1
commit 46a81a6435
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 73 additions and 10 deletions

View file

@ -7,7 +7,7 @@ import {
import { DraftPickCell, type CoronaState } from "~/components/draft/DraftPickCell";
import { TeamAvatar } from "~/components/TeamAvatar";
import type { SeasonStatus } from "~/models/season";
import type { RawFlagConfig } from "~/lib/flag-types";
import type { AvatarData, RawFlagConfig } from "~/lib/flag-types";
interface DraftCell {
pickNumber: number;
@ -26,6 +26,7 @@ interface DraftGridProps {
logoUrl?: string | null;
flagConfig?: RawFlagConfig | null;
avatarType?: string | null;
ownerAvatarData?: AvatarData | null;
};
}>;
draftGrid: Array<Array<DraftCell | null>>;
@ -84,6 +85,7 @@ export function DraftGrid({
logoUrl={slot.team.logoUrl}
flagConfig={slot.team.flagConfig}
avatarType={slot.team.avatarType}
ownerAvatarData={slot.team.ownerAvatarData}
size="lg"
/>
</div>

View file

@ -32,7 +32,7 @@ export function UserMenu({
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" className="relative h-9 w-9 p-0 overflow-hidden">
<Button variant="ghost" className="relative h-9 w-9 p-0 rounded-none">
<UserAvatar
userId={userId}
name={name}

View file

@ -1,6 +1,6 @@
import { memo, useMemo, useState } from "react";
import { TeamAvatar } from "~/components/TeamAvatar";
import type { RawFlagConfig } from "~/lib/flag-types";
import type { AvatarData, RawFlagConfig } from "~/lib/flag-types";
import {
ContextMenu,
ContextMenuContent,
@ -34,6 +34,7 @@ interface DraftGridSectionProps {
logoUrl?: string | null;
flagConfig?: RawFlagConfig | null;
avatarType?: string | null;
ownerAvatarData?: AvatarData | null;
};
}>;
draftGrid: Array<
@ -121,6 +122,7 @@ export const DraftGridSection = memo(function DraftGridSection({
logoUrl={slot.team.logoUrl}
flagConfig={slot.team.flagConfig}
avatarType={slot.team.avatarType}
ownerAvatarData={slot.team.ownerAvatarData}
size="md"
/>
</div>

View file

@ -4,6 +4,7 @@ import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader } from "~/components/ui/card";
import { GradientIcon } from "~/components/ui/GradientIcon";
import { TeamAvatar } from "~/components/TeamAvatar";
import type { AvatarData, RawFlagConfig } from "~/lib/flag-types";
import { StatColumn, StatDivider, PointChangeIndicator, RankingDisplay } from "./StatHelpers";
// ─── Row styles ───────────────────────────────────────────────────────────────
@ -27,7 +28,14 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
<>
{/* Left: avatar + name */}
<div className="flex items-center gap-3 flex-1 min-w-0">
<TeamAvatar teamId={entry.teamId} teamName={entry.teamName} />
<TeamAvatar
teamId={entry.teamId}
teamName={entry.teamName}
logoUrl={entry.logoUrl}
flagConfig={entry.flagConfig}
avatarType={entry.avatarType}
ownerAvatarData={entry.ownerAvatarData}
/>
<div className="min-w-0">
<p className="font-medium text-sm leading-tight truncate">{entry.teamName}</p>
{entry.ownerName && (
@ -59,6 +67,10 @@ export interface StandingsPreviewEntry {
teamId: string;
teamName: string;
ownerName?: string | null;
logoUrl?: string | null;
flagConfig?: RawFlagConfig | null;
avatarType?: string | null;
ownerAvatarData?: AvatarData | null;
/** Pre-computed display rank. T prefix (if any) is stripped at render time. */
displayRank: string | number;
/** Numeric rank used to select the gold/silver/bronze row tint (13 only). */

View file

@ -116,7 +116,7 @@ function OwnerAvatarPreview({ avatarData }: { avatarData: AvatarData }) {
<img
src={cloudinaryAvatarUrl(avatarData.url, 64)}
alt="Your avatar"
className="h-16 w-16 rounded-full object-cover ring-2 ring-border"
className="h-16 w-16 object-cover ring-2 ring-border"
/>
);
}
@ -127,7 +127,7 @@ function OwnerAvatarPreview({ avatarData }: { avatarData: AvatarData }) {
config={avatarData.config}
size={64}
title="Your avatar"
className="h-16 w-16 rounded-full ring-2 ring-border"
className="h-16 w-16 ring-2 ring-border"
/>
);
}

View file

@ -1,6 +1,7 @@
import { eq, count } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import type { RawFlagConfig } from "~/lib/flag-types";
export type DraftSlot = typeof schema.draftSlots.$inferSelect;
export type NewDraftSlot = typeof schema.draftSlots.$inferInsert;
@ -10,6 +11,9 @@ export interface DraftSlotWithTeam extends DraftSlot {
id: string;
name: string;
ownerId: string | null;
logoUrl?: string | null;
flagConfig?: RawFlagConfig | null;
avatarType?: string | null;
};
}
@ -51,6 +55,9 @@ export async function findDraftSlotsBySeasonId(
id: true,
name: true,
ownerId: true,
logoUrl: true,
flagConfig: true,
avatarType: true,
},
},
},

View file

@ -6,7 +6,8 @@ import { requireLeagueAccess } from "~/lib/auth";
import { DraftGrid } from "~/components/DraftGrid";
import { useDraftSocket } from "~/hooks/useDraftSocket";
import { useState, useEffect } from "react";
import { buildOwnerMap } from "~/lib/owner-map";
import { findUsersByIds, getUserDisplayName } from "~/models/user";
import { resolveUserAvatarData } from "~/lib/avatar-data";
import { Button } from "~/components/ui/button";
import { ArrowLeft } from "lucide-react";
import logomarkUrl from "../../../public/logomark.svg?url";
@ -94,7 +95,22 @@ export async function loader(args: Route.LoaderArgs) {
.where(eq(schema.draftPicks.seasonId, seasonId))
.orderBy(asc(schema.draftPicks.pickNumber));
const ownerMap = await buildOwnerMap(draftSlots);
const uniqueOwnerIds = [...new Set(
draftSlots.map((s) => s.team.ownerId).filter((id): id is string => id !== null)
)];
const ownerUserRows = uniqueOwnerIds.length ? await findUsersByIds(uniqueOwnerIds) : [];
const ownerUserById = new Map(ownerUserRows.map((u) => [u.id, u]));
const ownerMap: Record<string, string> = {};
const enrichedDraftSlots = draftSlots.map((slot) => {
const ownerUser = slot.team.ownerId ? ownerUserById.get(slot.team.ownerId) : undefined;
const displayName = ownerUser ? getUserDisplayName(ownerUser) : null;
if (slot.team.ownerId && displayName) ownerMap[slot.team.id] = displayName;
return {
...slot,
team: { ...slot.team, ownerAvatarData: ownerUser ? resolveUserAvatarData(ownerUser) : null },
};
});
let coronaStates: Record<string, CoronaState> = {};
@ -152,7 +168,7 @@ export async function loader(args: Route.LoaderArgs) {
return {
season,
draftSlots,
draftSlots: enrichedDraftSlots,
draftPicks,
ownerMap,
coronaStates,

View file

@ -22,6 +22,7 @@ import { getSeasonTimers } from "~/models/draft-timer";
import { getSeasonAutodraftSettings } from "~/models/autodraft-settings";
import { hasCommissionerRecord } from "~/models/commissioner";
import { findUsersByIds, getUserDisplayName } from "~/models/user";
import { resolveUserAvatarData } from "~/lib/avatar-data";
import { isInOvernightWindow } from "~/lib/overnight-pause";
import logomarkUrl from "../../../public/logomark.svg?url";
import { DraftSidebar } from "~/components/DraftSidebar";
@ -156,9 +157,20 @@ export async function loader(args: Route.LoaderArgs) {
)
: {};
const enrichedDraftSlots = draftSlots.map((slot) => {
const ownerUser = slot.team.ownerId ? userById.get(slot.team.ownerId) : undefined;
return {
...slot,
team: {
...slot.team,
ownerAvatarData: ownerUser ? resolveUserAvatarData(ownerUser) : null,
},
};
});
return {
season,
draftSlots,
draftSlots: enrichedDraftSlots,
draftPicks,
availableParticipants,
userTeam,

View file

@ -18,6 +18,7 @@ import { getUpcomingGroupStageMatchesForParticipants } from "~/models/group-stag
import { getDraftedParticipantsBySportsSeason, getDraftedParticipantsWithPoints, type DraftedParticipantWithPoints } from "~/models/draft-pick";
import { getAuditLogForSeason } from "~/models/audit-log";
import { findUserById } from "~/models/user";
import { resolveUserAvatarData } from "~/lib/avatar-data";
import type { Route } from "./+types/$leagueId";
export async function loader(args: Route.LoaderArgs) {
@ -93,6 +94,10 @@ export async function loader(args: Route.LoaderArgs) {
.map(([id, user]) => [id, getUserDisplayName(user)])
);
const ownerAvatarDataByUserId = Object.fromEntries(
userRows.map((u) => [u.id, resolveUserAvatarData(u)])
);
const commissionerMap = new Map(
commissionerIds
.map((id) => [id, userById.get(id)] as const)
@ -223,6 +228,7 @@ export async function loader(args: Route.LoaderArgs) {
currentUserId: userId,
isUserCommissioner,
ownerMap: Object.fromEntries(ownerMap),
ownerAvatarDataByUserId,
commissionerMap: Object.fromEntries(commissionerMap),
availableTeamCount,
sportsCount,

View file

@ -31,6 +31,7 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
currentUserId,
isUserCommissioner,
ownerMap,
ownerAvatarDataByUserId,
commissionerMap,
availableTeamCount,
sportsCount,
@ -101,10 +102,15 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
const standingsEntries: StandingsPreviewEntry[] = sortedTeams.map((team) => {
const standing = standingsMap.get(team.id);
const ownerAvatarData = team.ownerId ? (ownerAvatarDataByUserId[team.ownerId] ?? null) : null;
return {
teamId: team.id,
teamName: team.name,
ownerName: team.ownerId ? ownerMap[team.ownerId] : null,
logoUrl: team.logoUrl,
flagConfig: team.flagConfig,
avatarType: team.avatarType,
ownerAvatarData,
displayRank: getDisplayRank(standing, standings.length, standing ? isTiedRank(standing.currentRank) : false),
currentRank: standing?.currentRank,
points: standing ? (standing.actualPoints ?? standing.totalPoints) : 0,