diff --git a/app/components/league/__tests__/StandingsPreview.test.tsx b/app/components/league/__tests__/StandingsPreview.test.tsx
new file mode 100644
index 0000000..14b7636
--- /dev/null
+++ b/app/components/league/__tests__/StandingsPreview.test.tsx
@@ -0,0 +1,106 @@
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { BrowserRouter } from "react-router";
+import { StandingsPreview, type StandingsPreviewEntry } from "../StandingsPreview";
+
+function renderWithRouter(ui: React.ReactElement) {
+ return render({ui});
+}
+
+const baseEntry: StandingsPreviewEntry = {
+ teamId: "team-1",
+ teamName: "Lightning Wolves",
+ ownerName: "alice",
+ displayRank: 1,
+ currentRank: 1,
+ points: 2500,
+ href: "/leagues/1/standings/1/teams/team-1",
+};
+
+describe("StandingsPreview", () => {
+ describe("Avatar rendering", () => {
+ it("renders an img for uploaded avatar type", () => {
+ const entry: StandingsPreviewEntry = {
+ ...baseEntry,
+ avatarType: "uploaded",
+ logoUrl: "https://example.com/team-logo.png",
+ };
+ renderWithRouter();
+
+ const img = screen.getByAltText("Lightning Wolves");
+ expect(img.tagName).toBe("IMG");
+ });
+
+ it("renders an SVG for flag avatar type", () => {
+ const entry: StandingsPreviewEntry = {
+ ...baseEntry,
+ avatarType: "flag",
+ flagConfig: { pattern: "triband-h", colors: ["#ff0000", "#ffffff", "#0000ff"] },
+ };
+ const { container } = renderWithRouter();
+
+ expect(container.querySelector("svg")).toBeInTheDocument();
+ });
+
+ it("renders owner image when avatarType is owner with image ownerAvatarData", () => {
+ const entry: StandingsPreviewEntry = {
+ ...baseEntry,
+ avatarType: "owner",
+ ownerAvatarData: { type: "image", url: "https://example.com/owner.png" },
+ };
+ renderWithRouter();
+
+ const img = screen.getByAltText("Lightning Wolves");
+ expect(img.tagName).toBe("IMG");
+ });
+
+ it("falls back to a generated flag when no avatar data is provided", () => {
+ const entry: StandingsPreviewEntry = { ...baseEntry, avatarType: undefined };
+ const { container } = renderWithRouter();
+
+ expect(container.querySelector("svg")).toBeInTheDocument();
+ });
+ });
+
+ describe("Basic rendering", () => {
+ it("renders team names for all entries", () => {
+ const entries: StandingsPreviewEntry[] = [
+ { ...baseEntry, teamId: "t1", teamName: "Team Alpha", displayRank: 1 },
+ { ...baseEntry, teamId: "t2", teamName: "Team Beta", displayRank: 2 },
+ ];
+ renderWithRouter();
+
+ // Use selector to target only the
element — the team name also appears
+ // in the SVG
for accessibility, causing multiple matches otherwise.
+ expect(screen.getByText("Team Alpha", { selector: "p" })).toBeInTheDocument();
+ expect(screen.getByText("Team Beta", { selector: "p" })).toBeInTheDocument();
+ });
+
+ it("renders owner name below team name", () => {
+ renderWithRouter();
+
+ expect(screen.getByText("alice")).toBeInTheDocument();
+ });
+
+ it("renders rounded points", () => {
+ renderWithRouter();
+
+ expect(screen.getByText("1,500")).toBeInTheDocument();
+ });
+
+ it("renders description when provided", () => {
+ renderWithRouter();
+
+ expect(screen.getByText("Current Standings")).toBeInTheDocument();
+ });
+
+ it("renders full standings link when fullStandingsHref is provided", () => {
+ renderWithRouter(
+
+ );
+
+ const link = screen.getByRole("link", { name: "Full Standings" });
+ expect(link).toHaveAttribute("href", "/leagues/1/standings/1");
+ });
+ });
+});
diff --git a/app/routes/leagues/$leagueId.standings.$seasonId.tsx b/app/routes/leagues/$leagueId.standings.$seasonId.tsx
index c21bede..00e345a 100644
--- a/app/routes/leagues/$leagueId.standings.$seasonId.tsx
+++ b/app/routes/leagues/$leagueId.standings.$seasonId.tsx
@@ -10,6 +10,7 @@ import { isSeasonComplete, getSeasonCompletionPercentage } from "~/lib/season-he
import { getRecentTeamScoreEvents } from "~/models/team-score-events";
import { PointProgressionChart } from "~/components/standings/PointProgressionChart";
import { findUsersByIds, getUserDisplayName } from "~/models/user";
+import { resolveUserAvatarData } from "~/lib/avatar-data";
import { StandingsPreview } from "~/components/league/StandingsPreview";
import { buildTiedRankChecker, getDisplayRank } from "~/lib/standings-display";
import { RecentScoresCard } from "~/components/standings/RecentScoresCard";
@@ -59,7 +60,7 @@ export async function loader(args: Route.LoaderArgs) {
getSevenDayStandingsChange(seasonId, db),
db.query.teams.findMany({
where: eq(schema.teams.seasonId, seasonId),
- columns: { id: true, ownerId: true },
+ columns: { id: true, ownerId: true, logoUrl: true, flagConfig: true, avatarType: true },
}),
getSeasonPointProgression(seasonId, db),
isSeasonComplete(seasonId, db),
@@ -80,12 +81,23 @@ export async function loader(args: Route.LoaderArgs) {
})
);
+ const ownerAvatarDataByUserId = new Map(userRows.map((u) => [u.id, resolveUserAvatarData(u)]));
+ const teamById = new Map(teams.map((t) => [t.id, t]));
+
// Map to format expected by component (use sevenDayRankChange instead of previousRank)
- const formattedStandings = standingsWithComparison.map((standing) => ({
- ...standing,
- rankChange: standing.sevenDayRankChange,
- ownerName: ownerNameByTeamId.get(standing.teamId) ?? null,
- }));
+ const formattedStandings = standingsWithComparison.map((standing) => {
+ const team = teamById.get(standing.teamId);
+ const ownerAvatarData = team?.ownerId ? (ownerAvatarDataByUserId.get(team.ownerId) ?? null) : null;
+ return {
+ ...standing,
+ rankChange: standing.sevenDayRankChange,
+ ownerName: ownerNameByTeamId.get(standing.teamId) ?? null,
+ logoUrl: team?.logoUrl ?? null,
+ flagConfig: team?.flagConfig ?? null,
+ avatarType: team?.avatarType ?? null,
+ ownerAvatarData,
+ };
+ });
// Enrich recentScoreEvents with owner names
const enrichedScoreEvents = recentScoreEvents.map((event) => ({
@@ -119,6 +131,10 @@ export default function LeagueStandings() {
teamId: s.teamId,
teamName: s.teamName,
ownerName: s.ownerName,
+ logoUrl: s.logoUrl,
+ flagConfig: s.flagConfig,
+ avatarType: s.avatarType,
+ ownerAvatarData: s.ownerAvatarData,
displayRank: getDisplayRank(s, standings.length, isTied(s.currentRank)),
currentRank: s.currentRank,
points: s.totalPoints,