Fix avatar display bugs in navbar, standings, and draft views
- 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:
parent
1bd87c7fb1
commit
8fe9c40850
10 changed files with 73 additions and 10 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (1–3 only). */
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue