brackt/app/components/league/StandingsPreview.tsx
Chris Parsons e8e4981465
Add opencode oxlint plugin and UI refinements (#323)
* Add opencode oxlint plugin and UI refinements

- Add .opencode/plugins/oxlint.ts to run oxlint automatically on file edits
- Refactor RankingDisplay into shared StatHelpers component
- Add tied rank display support in home loader
- Update navbar with NavLink for active state styling
- Replace .reverse() with .toReversed() in RecentPicksFeed

* Remove unused RankChangeIndicator import in LeagueRow
2026-04-24 11:11:20 -07:00

122 lines
5 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 { 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} />
<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;
/** 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>
);
}