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

View file

@ -32,7 +32,7 @@ export function UserMenu({
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <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 <UserAvatar
userId={userId} userId={userId}
name={name} name={name}

View file

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

View file

@ -4,6 +4,7 @@ import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { Card, CardContent, CardHeader } from "~/components/ui/card";
import { GradientIcon } from "~/components/ui/GradientIcon"; import { GradientIcon } from "~/components/ui/GradientIcon";
import { TeamAvatar } from "~/components/TeamAvatar"; import { TeamAvatar } from "~/components/TeamAvatar";
import type { AvatarData, RawFlagConfig } from "~/lib/flag-types";
import { StatColumn, StatDivider, PointChangeIndicator, RankingDisplay } from "./StatHelpers"; import { StatColumn, StatDivider, PointChangeIndicator, RankingDisplay } from "./StatHelpers";
// ─── Row styles ─────────────────────────────────────────────────────────────── // ─── Row styles ───────────────────────────────────────────────────────────────
@ -27,7 +28,14 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
<> <>
{/* Left: avatar + name */} {/* Left: avatar + name */}
<div className="flex items-center gap-3 flex-1 min-w-0"> <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"> <div className="min-w-0">
<p className="font-medium text-sm leading-tight truncate">{entry.teamName}</p> <p className="font-medium text-sm leading-tight truncate">{entry.teamName}</p>
{entry.ownerName && ( {entry.ownerName && (
@ -59,6 +67,10 @@ export interface StandingsPreviewEntry {
teamId: string; teamId: string;
teamName: string; teamName: string;
ownerName?: string | null; 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. */ /** Pre-computed display rank. T prefix (if any) is stripped at render time. */
displayRank: string | number; displayRank: string | number;
/** Numeric rank used to select the gold/silver/bronze row tint (13 only). */ /** 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 <img
src={cloudinaryAvatarUrl(avatarData.url, 64)} src={cloudinaryAvatarUrl(avatarData.url, 64)}
alt="Your avatar" 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} config={avatarData.config}
size={64} size={64}
title="Your avatar" 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 { eq, count } from "drizzle-orm";
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import type { RawFlagConfig } from "~/lib/flag-types";
export type DraftSlot = typeof schema.draftSlots.$inferSelect; export type DraftSlot = typeof schema.draftSlots.$inferSelect;
export type NewDraftSlot = typeof schema.draftSlots.$inferInsert; export type NewDraftSlot = typeof schema.draftSlots.$inferInsert;
@ -10,6 +11,9 @@ export interface DraftSlotWithTeam extends DraftSlot {
id: string; id: string;
name: string; name: string;
ownerId: string | null; ownerId: string | null;
logoUrl?: string | null;
flagConfig?: RawFlagConfig | null;
avatarType?: string | null;
}; };
} }
@ -51,6 +55,9 @@ export async function findDraftSlotsBySeasonId(
id: true, id: true,
name: true, name: true,
ownerId: 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 { DraftGrid } from "~/components/DraftGrid";
import { useDraftSocket } from "~/hooks/useDraftSocket"; import { useDraftSocket } from "~/hooks/useDraftSocket";
import { useState, useEffect } from "react"; 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 { Button } from "~/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import logomarkUrl from "../../../public/logomark.svg?url"; import logomarkUrl from "../../../public/logomark.svg?url";
@ -94,7 +95,22 @@ export async function loader(args: Route.LoaderArgs) {
.where(eq(schema.draftPicks.seasonId, seasonId)) .where(eq(schema.draftPicks.seasonId, seasonId))
.orderBy(asc(schema.draftPicks.pickNumber)); .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> = {}; let coronaStates: Record<string, CoronaState> = {};
@ -152,7 +168,7 @@ export async function loader(args: Route.LoaderArgs) {
return { return {
season, season,
draftSlots, draftSlots: enrichedDraftSlots,
draftPicks, draftPicks,
ownerMap, ownerMap,
coronaStates, coronaStates,

View file

@ -22,6 +22,7 @@ import { getSeasonTimers } from "~/models/draft-timer";
import { getSeasonAutodraftSettings } from "~/models/autodraft-settings"; import { getSeasonAutodraftSettings } from "~/models/autodraft-settings";
import { hasCommissionerRecord } from "~/models/commissioner"; import { hasCommissionerRecord } from "~/models/commissioner";
import { findUsersByIds, getUserDisplayName } from "~/models/user"; import { findUsersByIds, getUserDisplayName } from "~/models/user";
import { resolveUserAvatarData } from "~/lib/avatar-data";
import { isInOvernightWindow } from "~/lib/overnight-pause"; import { isInOvernightWindow } from "~/lib/overnight-pause";
import logomarkUrl from "../../../public/logomark.svg?url"; import logomarkUrl from "../../../public/logomark.svg?url";
import { DraftSidebar } from "~/components/DraftSidebar"; 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 { return {
season, season,
draftSlots, draftSlots: enrichedDraftSlots,
draftPicks, draftPicks,
availableParticipants, availableParticipants,
userTeam, userTeam,

View file

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

View file

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