Add StandingsPreview card component with podium row styling
- New StandingsPreview component with gold/silver/bronze row tints for top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points) with rank and 7-day point change indicators - Fix GradientIcon in Storybook by adding BracktGradients decorator to preview.tsx (renamed from .ts to support JSX) - Fix degenerate SVG gradient on horizontal strokes by switching BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space coordinates (0→24) - Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only fix was sufficient once gradientUnits was corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aacf0b29c8
commit
f9c03c8bf1
5 changed files with 523 additions and 51 deletions
|
|
@ -1,9 +1,18 @@
|
|||
import React from 'react';
|
||||
import { withRouter } from 'storybook-addon-remix-react-router';
|
||||
import type { Preview } from '@storybook/react-vite'
|
||||
import type { Preview, Decorator } from '@storybook/react-vite'
|
||||
import '../app/app.css'
|
||||
import { BracktGradients } from '../app/components/ui/BracktGradients';
|
||||
|
||||
const withBracktGradients: Decorator = (Story) => (
|
||||
<>
|
||||
<BracktGradients />
|
||||
<Story />
|
||||
</>
|
||||
);
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [withRouter], // This wraps all stories in a Router context
|
||||
decorators: [withRouter, withBracktGradients],
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
265
app/components/league/StandingsPreview.stories.tsx
Normal file
265
app/components/league/StandingsPreview.stories.tsx
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { StandingsPreview } from "./StandingsPreview";
|
||||
|
||||
const meta: Meta<typeof StandingsPreview> = {
|
||||
title: "League/StandingsPreview",
|
||||
component: StandingsPreview,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof StandingsPreview>;
|
||||
|
||||
export const TopThreePodium: Story = {
|
||||
args: {
|
||||
entries: [
|
||||
{
|
||||
teamId: "t1",
|
||||
teamName: "Lightning Wolves",
|
||||
ownerName: "alice",
|
||||
displayRank: 1,
|
||||
currentRank: 1,
|
||||
points: 2810,
|
||||
href: "/leagues/1/standings/1/teams/t1",
|
||||
},
|
||||
{
|
||||
teamId: "t2",
|
||||
teamName: "Shadow Hawks",
|
||||
ownerName: "bob",
|
||||
displayRank: 2,
|
||||
currentRank: 2,
|
||||
points: 2654.5,
|
||||
href: "/leagues/1/standings/1/teams/t2",
|
||||
},
|
||||
{
|
||||
teamId: "t3",
|
||||
teamName: "Iron Eagles",
|
||||
ownerName: "carol",
|
||||
displayRank: 3,
|
||||
currentRank: 3,
|
||||
points: 2493,
|
||||
href: "/leagues/1/standings/1/teams/t3",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const FullLeague: Story = {
|
||||
args: {
|
||||
entries: [
|
||||
{
|
||||
teamId: "t1",
|
||||
teamName: "Lightning Wolves",
|
||||
ownerName: "alice",
|
||||
displayRank: 1,
|
||||
currentRank: 1,
|
||||
points: 2810,
|
||||
rankChange: 2,
|
||||
pointChange: 87.5,
|
||||
href: "/leagues/1/standings/1/teams/t1",
|
||||
},
|
||||
{
|
||||
teamId: "t2",
|
||||
teamName: "Shadow Hawks",
|
||||
ownerName: "bob",
|
||||
displayRank: 2,
|
||||
currentRank: 2,
|
||||
points: 2654.5,
|
||||
rankChange: 0,
|
||||
pointChange: 42.0,
|
||||
href: "/leagues/1/standings/1/teams/t2",
|
||||
},
|
||||
{
|
||||
teamId: "t3",
|
||||
teamName: "Iron Eagles",
|
||||
ownerName: "carol",
|
||||
displayRank: 3,
|
||||
currentRank: 3,
|
||||
points: 2493,
|
||||
rankChange: -1,
|
||||
pointChange: -12.3,
|
||||
href: "/leagues/1/standings/1/teams/t3",
|
||||
},
|
||||
{
|
||||
teamId: "t4",
|
||||
teamName: "Cyber Foxes",
|
||||
ownerName: "dave",
|
||||
displayRank: 4,
|
||||
currentRank: 4,
|
||||
points: 2311.8,
|
||||
rankChange: 1,
|
||||
pointChange: 23.1,
|
||||
href: "/leagues/1/standings/1/teams/t4",
|
||||
},
|
||||
{
|
||||
teamId: "t5",
|
||||
teamName: "Neon Tigers",
|
||||
ownerName: "eve",
|
||||
displayRank: 5,
|
||||
currentRank: 5,
|
||||
points: 2108,
|
||||
rankChange: -2,
|
||||
pointChange: -55.0,
|
||||
href: "/leagues/1/standings/1/teams/t5",
|
||||
},
|
||||
{
|
||||
teamId: "t6",
|
||||
teamName: "Phantom Sharks",
|
||||
ownerName: "frank",
|
||||
displayRank: 6,
|
||||
currentRank: 6,
|
||||
points: 1987.3,
|
||||
rankChange: 0,
|
||||
pointChange: 0,
|
||||
href: "/leagues/1/standings/1/teams/t6",
|
||||
},
|
||||
{
|
||||
teamId: "t7",
|
||||
teamName: "Crimson Tide FC",
|
||||
ownerName: "grace",
|
||||
displayRank: 7,
|
||||
currentRank: 7,
|
||||
points: 1834,
|
||||
href: "/leagues/1/standings/1/teams/t7",
|
||||
},
|
||||
{
|
||||
teamId: "t8",
|
||||
teamName: "Arctic Wolves United",
|
||||
ownerName: "hank",
|
||||
displayRank: 8,
|
||||
currentRank: 8,
|
||||
points: 1692.6,
|
||||
href: "/leagues/1/standings/1/teams/t8",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTies: Story = {
|
||||
args: {
|
||||
entries: [
|
||||
{
|
||||
teamId: "t1",
|
||||
teamName: "Lightning Wolves",
|
||||
ownerName: "alice",
|
||||
displayRank: "T1",
|
||||
currentRank: 1,
|
||||
points: 2810,
|
||||
href: "/leagues/1/standings/1/teams/t1",
|
||||
},
|
||||
{
|
||||
teamId: "t2",
|
||||
teamName: "Shadow Hawks",
|
||||
ownerName: "bob",
|
||||
displayRank: "T1",
|
||||
currentRank: 1,
|
||||
points: 2810,
|
||||
href: "/leagues/1/standings/1/teams/t2",
|
||||
},
|
||||
{
|
||||
teamId: "t3",
|
||||
teamName: "Iron Eagles",
|
||||
ownerName: "carol",
|
||||
displayRank: "T3",
|
||||
currentRank: 3,
|
||||
points: 2493,
|
||||
href: "/leagues/1/standings/1/teams/t3",
|
||||
},
|
||||
{
|
||||
teamId: "t4",
|
||||
teamName: "Cyber Foxes",
|
||||
ownerName: "dave",
|
||||
displayRank: "T3",
|
||||
currentRank: 3,
|
||||
points: 2493,
|
||||
href: "/leagues/1/standings/1/teams/t4",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const AllUnranked: Story = {
|
||||
args: {
|
||||
entries: [
|
||||
{
|
||||
teamId: "t1",
|
||||
teamName: "Lightning Wolves",
|
||||
ownerName: "alice",
|
||||
displayRank: "T1",
|
||||
points: 0,
|
||||
href: "/leagues/1/standings/1/teams/t1",
|
||||
},
|
||||
{
|
||||
teamId: "t2",
|
||||
teamName: "Shadow Hawks",
|
||||
ownerName: "bob",
|
||||
displayRank: "T1",
|
||||
points: 0,
|
||||
href: "/leagues/1/standings/1/teams/t2",
|
||||
},
|
||||
{
|
||||
teamId: "t3",
|
||||
teamName: "Iron Eagles",
|
||||
ownerName: "carol",
|
||||
displayRank: "T1",
|
||||
points: 0,
|
||||
href: "/leagues/1/standings/1/teams/t3",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoOwnerNames: Story = {
|
||||
args: {
|
||||
entries: [
|
||||
{
|
||||
teamId: "t1",
|
||||
teamName: "Lightning Wolves",
|
||||
displayRank: 1,
|
||||
currentRank: 1,
|
||||
points: 2810,
|
||||
},
|
||||
{
|
||||
teamId: "t2",
|
||||
teamName: "Shadow Hawks",
|
||||
displayRank: 2,
|
||||
currentRank: 2,
|
||||
points: 2654.5,
|
||||
},
|
||||
{
|
||||
teamId: "t3",
|
||||
teamName: "Iron Eagles",
|
||||
displayRank: 3,
|
||||
currentRank: 3,
|
||||
points: 2493,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const LongTeamNames: Story = {
|
||||
args: {
|
||||
entries: [
|
||||
{
|
||||
teamId: "t1",
|
||||
teamName: "The Unstoppable Championship Winning Lightning Wolves",
|
||||
ownerName: "alice_with_a_really_long_username",
|
||||
displayRank: 1,
|
||||
currentRank: 1,
|
||||
points: 2810,
|
||||
href: "/leagues/1/standings/1/teams/t1",
|
||||
},
|
||||
{
|
||||
teamId: "t2",
|
||||
teamName: "Shadow Hawks of the Northern Division",
|
||||
ownerName: "bob",
|
||||
displayRank: 2,
|
||||
currentRank: 2,
|
||||
points: 2654.5,
|
||||
href: "/leagues/1/standings/1/teams/t2",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
221
app/components/league/StandingsPreview.tsx
Normal file
221
app/components/league/StandingsPreview.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
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";
|
||||
|
||||
// ─── TeamAvatar ───────────────────────────────────────────────────────────────
|
||||
|
||||
const TEAM_AVATAR_COLORS = [
|
||||
"#adf661",
|
||||
"#2ce1c1",
|
||||
"#8b5cf6",
|
||||
"#f59e0b",
|
||||
"#ef4444",
|
||||
"#3b82f6",
|
||||
];
|
||||
|
||||
function hashTeamId(id: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < id.length; i++) {
|
||||
hash = (hash * 31 + id.charCodeAt(i)) & 0xffff;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
function TeamAvatar({ teamId, teamName }: { teamId: string; teamName: string }) {
|
||||
const color = TEAM_AVATAR_COLORS[hashTeamId(teamId) % TEAM_AVATAR_COLORS.length];
|
||||
const initials =
|
||||
teamName
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0].toUpperCase())
|
||||
.join("") || "?";
|
||||
|
||||
return (
|
||||
<div
|
||||
role="img"
|
||||
aria-label={teamName}
|
||||
className="h-9 w-9 flex shrink-0 items-center justify-center font-bold text-sm"
|
||||
style={{ backgroundColor: "#000", color }}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Stat helpers (mirrors LeagueRow) ────────────────────────────────────────
|
||||
|
||||
function StatColumn({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="text-right shrink-0 flex-1 sm:flex-none">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{label}
|
||||
</p>
|
||||
<div className="flex items-baseline justify-end gap-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatDivider() {
|
||||
return <div className="h-8 w-px bg-border shrink-0 self-center" />;
|
||||
}
|
||||
|
||||
function RankChangeIndicator({ change }: { change: number }) {
|
||||
if (change === 0) return null;
|
||||
if (change > 0) {
|
||||
return (
|
||||
<span className="text-xs font-semibold text-primary" aria-label={`up ${change}`}>
|
||||
▲{change}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: "var(--coral-accent, #ef4444)" }}
|
||||
aria-label={`down ${Math.abs(change)}`}
|
||||
>
|
||||
▼{Math.abs(change)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function PointChangeIndicator({ change }: { change: number }) {
|
||||
const sign = change >= 0 ? "+" : "";
|
||||
if (change >= 0) {
|
||||
return <span className="text-xs font-semibold text-primary">{sign}{change.toFixed(1)}</span>;
|
||||
}
|
||||
return (
|
||||
<span className="text-xs font-semibold" style={{ color: "var(--coral-accent, #ef4444)" }}>
|
||||
{sign}{change.toFixed(1)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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-2.5 sm:px-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} />
|
||||
<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">
|
||||
<StatColumn label="Ranking">
|
||||
<span className="text-2xl font-bold leading-none">{entry.displayRank}</span>
|
||||
{entry.rankChange !== undefined && entry.rankChange !== 0 && (
|
||||
<RankChangeIndicator change={entry.rankChange} />
|
||||
)}
|
||||
</StatColumn>
|
||||
<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 change={entry.pointChange} />
|
||||
)}
|
||||
</StatColumn>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface StandingsPreviewEntry {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
ownerName?: string | null;
|
||||
/** Pre-computed display rank, e.g. 1, "T2", "T5". Use getDisplayRank(). */
|
||||
displayRank: string | number;
|
||||
/** Numeric rank used to select the gold/silver/bronze row tint (1–3 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,8 +8,10 @@ export function BracktGradients() {
|
|||
return (
|
||||
<svg width="0" height="0" aria-hidden className="absolute">
|
||||
<defs>
|
||||
{/* Green (top) → Cyan (bottom) — the main Brackt brand gradient */}
|
||||
<linearGradient id="brackt-primary-gradient" x1="0" y1="0" x2="0" y2="1">
|
||||
{/* Green (top) → Cyan (bottom) — the main Brackt brand gradient.
|
||||
userSpaceOnUse + 0→24 matches the Lucide icon viewBox so horizontal
|
||||
strokes (zero bounding-box height) don't produce a degenerate gradient. */}
|
||||
<linearGradient id="brackt-primary-gradient" x1="0" y1="0" x2="0" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stopColor="#adf661" />
|
||||
<stop offset="100%" stopColor="#2ce1c1" />
|
||||
</linearGradient>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import {
|
|||
} from "~/components/ui/card";
|
||||
import { SportSeasonCard } from "~/components/sports/SportSeasonCard";
|
||||
import { UpcomingCalendarPanel } from "~/components/sport-season/UpcomingCalendarPanel";
|
||||
import { TeamNameDisplay } from "~/components/ui/team-name-display";
|
||||
import { buildTiedRankChecker, getDisplayRank } from "~/lib/standings-display";
|
||||
import { StandingsPreview, type StandingsPreviewEntry } from "~/components/league/StandingsPreview";
|
||||
import { formatAuditDetail } from "~/lib/audit-log-display";
|
||||
|
||||
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
||||
|
|
@ -97,6 +97,21 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
return rankA - rankB;
|
||||
});
|
||||
|
||||
const standingsEntries: StandingsPreviewEntry[] = sortedTeams.map((team) => {
|
||||
const standing = standingsMap.get(team.id);
|
||||
return {
|
||||
teamId: team.id,
|
||||
teamName: team.name,
|
||||
ownerName: team.ownerId ? ownerMap[team.ownerId] : null,
|
||||
displayRank: getDisplayRank(standing, standings.length, standing ? isTiedRank(standing.currentRank) : false),
|
||||
currentRank: standing?.currentRank,
|
||||
points: standing ? (standing.actualPoints ?? standing.totalPoints) : 0,
|
||||
href: season ? `/leagues/${league.id}/standings/${season.id}/teams/${team.id}` : undefined,
|
||||
rankChange: standing?.rankChange,
|
||||
pointChange: standing?.sevenDayPointChange,
|
||||
};
|
||||
});
|
||||
|
||||
// Pre-compute sorted sports seasons: active → upcoming → completed,
|
||||
// season_standings last within active, then alphabetical by sport name
|
||||
const sortedSportsSeasons = [...sportsSeasons].toSorted((a, b) => {
|
||||
|
|
@ -171,51 +186,11 @@ export default function LeagueHome({ loaderData }: Route.ComponentProps) {
|
|||
<div className="md:col-span-2 space-y-6">
|
||||
{/* Standings Panel - active/completed seasons */}
|
||||
{season && isActiveOrCompleted && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Standings</CardTitle>
|
||||
<CardDescription>
|
||||
{season.status === "completed" ? "Final standings" : "Current season standings"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link to={`/leagues/${league.id}/standings/${season.id}`}>
|
||||
Full Standings
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
{sortedTeams.map((team) => {
|
||||
const standing = standingsMap.get(team.id);
|
||||
const ownerName = team.ownerId ? ownerMap[team.ownerId] : null;
|
||||
return (
|
||||
<div
|
||||
key={team.id}
|
||||
className="flex items-center gap-3 py-2 border-b last:border-0"
|
||||
>
|
||||
<div className="w-8 text-center text-sm font-bold text-muted-foreground">
|
||||
{getDisplayRank(standing, standings.length, standing ? isTiedRank(standing.currentRank) : false)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<TeamNameDisplay
|
||||
teamName={team.name}
|
||||
ownerName={ownerName}
|
||||
href={`/leagues/${league.id}/standings/${season.id}/teams/${team.id}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{standing ? (standing.actualPoints ?? standing.totalPoints) : 0} pts
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<StandingsPreview
|
||||
entries={standingsEntries}
|
||||
description={season.status === "completed" ? "Final standings" : undefined}
|
||||
fullStandingsHref={`/leagues/${league.id}/standings/${season.id}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sports Seasons Section */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue