brackt/app/components/league/StandingsPreview.tsx
Chris Parsons 46a81a6435
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>
2026-05-06 17:03:34 -07:00

134 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ListOrdered } from "lucide-react";
import { Link } from "react-router";
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 ───────────────────────────────────────────────────────────────
const ROW_RANK_CLASSES: Record<number, string> = {
1: "bg-yellow-500/10 hover:bg-yellow-500/15",
2: "bg-white/[0.14] hover:bg-white/[0.18]",
3: "bg-orange-600/[0.08] hover:bg-orange-600/[0.12]",
};
function rowClasses(currentRank: number | undefined, hasHref: boolean): string {
const podium = currentRank !== undefined ? ROW_RANK_CLASSES[currentRank] : undefined;
const base = podium ?? `bg-white/[0.04] ${hasHref ? "hover:bg-white/[0.07]" : ""}`;
return `flex flex-col sm:flex-row sm:items-center gap-0 rounded-lg px-3 py-3 sm:px-5 sm:py-4 transition-colors ${base}`;
}
// ─── Row content ──────────────────────────────────────────────────────────────
function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
return (
<>
{/* Left: avatar + name */}
<div className="flex items-center gap-3 flex-1 min-w-0">
<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 && (
<p className="text-xs text-muted-foreground truncate">{entry.ownerName}</p>
)}
</div>
</div>
{/* Right: stats — second row on mobile */}
<div className="flex items-center gap-4 w-full border-t border-border/30 pt-2 mt-1 sm:w-auto sm:border-0 sm:pt-0 sm:mt-0 sm:shrink-0">
<RankingDisplay displayRank={entry.displayRank} rankChange={entry.rankChange} />
<StatDivider />
<StatColumn label="Points">
<span className="text-2xl font-bold leading-none text-electric">
{Math.round(entry.points).toLocaleString("en-US")}
</span>
{entry.pointChange !== undefined && entry.pointChange !== 0 && (
<PointChangeIndicator delta={entry.pointChange} />
)}
</StatColumn>
</div>
</>
);
}
// ─── Public API ───────────────────────────────────────────────────────────────
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). */
currentRank?: number;
points: number;
href?: string;
/** Positive = moved up, negative = moved down. From TeamStanding.rankChange. */
rankChange?: number;
/** 7-day point delta. From TeamStanding.sevenDayPointChange. */
pointChange?: number;
}
export interface StandingsPreviewProps {
entries: StandingsPreviewEntry[];
description?: string;
fullStandingsHref?: string;
}
export function StandingsPreview({ entries, description, fullStandingsHref }: StandingsPreviewProps) {
return (
<Card className="gap-2">
<CardHeader className="px-3 sm:px-6 pb-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<GradientIcon icon={ListOrdered} className="h-5 w-5 shrink-0" />
<div>
<h2 className="text-xl font-bold leading-none tracking-tight">Standings</h2>
{description && (
<p className="text-sm text-muted-foreground mt-0.5">{description}</p>
)}
</div>
</div>
{fullStandingsHref && (
<Button variant="outline" size="sm" asChild>
<Link to={fullStandingsHref}>Full Standings</Link>
</Button>
)}
</div>
</CardHeader>
<CardContent className="px-3 sm:px-6">
<div className="space-y-3">
{entries.map((entry) =>
entry.href ? (
<Link
key={entry.teamId}
to={entry.href}
className={rowClasses(entry.currentRank, true)}
>
<RowContent entry={entry} />
</Link>
) : (
<div key={entry.teamId} className={rowClasses(entry.currentRank, false)}>
<RowContent entry={entry} />
</div>
)
)}
</div>
</CardContent>
</Card>
);
}