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:
parent
1bd87c7fb1
commit
46a81a6435
10 changed files with 73 additions and 10 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 (1–3 only). */
|
/** Numeric rank used to select the gold/silver/bronze row tint (1–3 only). */
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue