Cache-bust logos via Vite ?url import and extract shared stat helpers (#310)

Logo SVGs referenced by string path (/logo.svg) were cached by CDNs and
browsers after deployments. Import them with Vite's ?url suffix so the
build emits content-hashed filenames that force a refresh on change.

LeagueRow and StandingsPreview had duplicated StatColumn, StatDivider,
and rank-change indicator helpers. Extract them into a shared
StatHelpers module so both pages render rank and points identically
(#3 style with consistent rank-change arrows).
This commit is contained in:
Chris Parsons 2026-04-23 13:52:34 -07:00 committed by GitHub
parent 9ed0282fd0
commit 6926fb96bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 76 additions and 101 deletions

View file

@ -2,6 +2,7 @@ import { formatDistanceToNow } from "date-fns";
import { Link } from "react-router"; import { Link } from "react-router";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { LeagueAvatar } from "./LeagueAvatar"; import { LeagueAvatar } from "./LeagueAvatar";
import { StatColumn, StatDivider, RankChangeIndicator } from "./StatHelpers";
export interface LeagueRowProps { export interface LeagueRowProps {
leagueId: string; leagueId: string;
@ -29,29 +30,6 @@ function ordinal(n: number): string {
} }
// ─── Shared stat column ───────────────────────────────────────────────────────
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" />;
}
// ─── Season progress bar ────────────────────────────────────────────────────── // ─── Season progress bar ──────────────────────────────────────────────────────
function SeasonProgress({ pct, status }: { pct: number; status: "active" | "completed" }) { function SeasonProgress({ pct, status }: { pct: number; status: "active" | "completed" }) {
@ -70,21 +48,6 @@ function SeasonProgress({ pct, status }: { pct: number; status: "active" | "comp
); );
} }
// ─── Rank change indicator ────────────────────────────────────────────────────
function RankChange({ current, previous }: { current: number; previous: number | undefined }) {
if (previous === undefined || previous === current) return null;
const delta = previous - current;
if (delta > 0) {
return <span className="text-xs font-semibold text-primary" aria-label={`ranked up ${delta}`}>{delta}</span>;
}
return (
<span className="text-xs font-semibold" style={{ color: "var(--coral-accent, #ef4444)" }} aria-label={`ranked down ${Math.abs(delta)}`}>
{Math.abs(delta)}
</span>
);
}
// ─── Row variants ───────────────────────────────────────────────────────────── // ─── Row variants ─────────────────────────────────────────────────────────────
function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRowProps) { function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRowProps) {
@ -156,7 +119,9 @@ function ActiveRow({
{currentRank !== undefined && ( {currentRank !== undefined && (
<StatColumn label="Ranking"> <StatColumn label="Ranking">
<span className="text-2xl font-bold leading-none">#{currentRank}</span> <span className="text-2xl font-bold leading-none">#{currentRank}</span>
<RankChange current={currentRank} previous={previousRank} /> {previousRank !== undefined && previousRank !== currentRank && (
<RankChangeIndicator delta={previousRank - currentRank} />
)}
</StatColumn> </StatColumn>
)} )}
{currentRank !== undefined && totalPoints !== undefined && <StatDivider />} {currentRank !== undefined && totalPoints !== undefined && <StatDivider />}

View file

@ -4,60 +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 { StatColumn, StatDivider, RankChangeIndicator, PointChangeIndicator } from "./StatHelpers";
// ─── 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 }) {
if (change >= 0) {
return <span className="text-xs font-semibold text-primary">{Math.round(change)}</span>;
}
return (
<span className="text-xs font-semibold" style={{ color: "var(--coral-accent, #ef4444)" }}>
{Math.abs(Math.round(change))}
</span>
);
}
// ─── Row styles ─────────────────────────────────────────────────────────────── // ─── Row styles ───────────────────────────────────────────────────────────────
@ -92,9 +39,9 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
{/* Right: stats — second row on mobile */} {/* 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"> <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"> <StatColumn label="Ranking">
<span className="text-2xl font-bold leading-none">{entry.displayRank}</span> <span className="text-2xl font-bold leading-none">#{String(entry.displayRank).replace(/^T/, "")}</span>
{entry.rankChange !== undefined && entry.rankChange !== 0 && ( {entry.rankChange !== undefined && entry.rankChange !== 0 && (
<RankChangeIndicator change={entry.rankChange} /> <RankChangeIndicator delta={entry.rankChange} />
)} )}
</StatColumn> </StatColumn>
<StatDivider /> <StatDivider />
@ -103,7 +50,7 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
{Math.round(entry.points).toLocaleString("en-US")} {Math.round(entry.points).toLocaleString("en-US")}
</span> </span>
{entry.pointChange !== undefined && entry.pointChange !== 0 && ( {entry.pointChange !== undefined && entry.pointChange !== 0 && (
<PointChangeIndicator change={entry.pointChange} /> <PointChangeIndicator delta={entry.pointChange} />
)} )}
</StatColumn> </StatColumn>
</div> </div>
@ -117,7 +64,7 @@ export interface StandingsPreviewEntry {
teamId: string; teamId: string;
teamName: string; teamName: string;
ownerName?: string | null; ownerName?: string | null;
/** Pre-computed display rank, e.g. 1, "T2", "T5". Use getDisplayRank(). */ /** 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 (13 only). */ /** Numeric rank used to select the gold/silver/bronze row tint (13 only). */
currentRank?: number; currentRank?: number;

View file

@ -0,0 +1,59 @@
export 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>
);
}
export function StatDivider() {
return <div className="h-8 w-px bg-border shrink-0 self-center" />;
}
export function RankChangeIndicator({ delta }: { delta: number }) {
if (delta === 0) return null;
if (delta > 0) {
return (
<span className="text-xs font-semibold text-primary" aria-label={`up ${delta}`}>
{delta}
</span>
);
}
return (
<span
className="text-xs font-semibold"
style={{ color: "var(--coral-accent, #ef4444)" }}
aria-label={`down ${Math.abs(delta)}`}
>
{Math.abs(delta)}
</span>
);
}
export function PointChangeIndicator({ delta }: { delta: number }) {
if (delta >= 0) {
return (
<span className="text-xs font-semibold text-primary" aria-label={`+${Math.round(delta)} points`}>
{Math.round(delta)}
</span>
);
}
return (
<span
className="text-xs font-semibold"
style={{ color: "var(--coral-accent, #ef4444)" }}
aria-label={`${Math.round(delta)} points`}
>
{Math.abs(Math.round(delta))}
</span>
);
}

View file

@ -10,6 +10,7 @@ import {
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/react-router"; import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/react-router";
import { HelpCircle, MenuIcon, Settings } from "lucide-react"; import { HelpCircle, MenuIcon, Settings } from "lucide-react";
import logoUrl from "../../public/logo.svg?url";
interface NavbarProps { interface NavbarProps {
isAdmin: boolean; isAdmin: boolean;
@ -24,7 +25,7 @@ export function Navbar({ isAdmin }: NavbarProps) {
{/* Left: Logo + Nav links */} {/* Left: Logo + Nav links */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link to="/" className="flex items-center mr-2"> <Link to="/" className="flex items-center mr-2">
<img src="/logo.svg" alt="Brackt" className="h-14" /> <img src={logoUrl} alt="Brackt" className="h-14" />
</Link> </Link>
<nav aria-label="Main navigation" className="hidden md:flex items-center gap-1"> <nav aria-label="Main navigation" className="hidden md:flex items-center gap-1">
<Link to="/how-to-play" className="text-sm font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-transparent hover:text-transparent hover:bg-clip-text hover:bg-[linear-gradient(to_bottom,#adf661,#2ce1c1)] px-3 py-2">How To Play</Link> <Link to="/how-to-play" className="text-sm font-semibold uppercase tracking-wide text-muted-foreground transition-colors hover:bg-transparent hover:text-transparent hover:bg-clip-text hover:bg-[linear-gradient(to_bottom,#adf661,#2ce1c1)] px-3 py-2">How To Play</Link>

View file

@ -20,6 +20,7 @@ import { BracktGradients } from "~/components/ui/BracktGradients";
import { Footer } from "~/components/marketing/Footer"; import { Footer } from "~/components/marketing/Footer";
import { isUserAdminByClerkId } from "~/models/user"; import { isUserAdminByClerkId } from "~/models/user";
import { AlertCircle, FileQuestion, Lock, ShieldOff, ServerCrash } from "lucide-react"; import { AlertCircle, FileQuestion, Lock, ShieldOff, ServerCrash } from "lucide-react";
import logoUrl from "../../public/logo.svg?url";
export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()]; export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()];
@ -136,7 +137,7 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
<header className="border-b border-border/40 bg-background/95 backdrop-blur"> <header className="border-b border-border/40 bg-background/95 backdrop-blur">
<div className="flex h-16 items-center px-4 md:px-6 lg:px-8"> <div className="flex h-16 items-center px-4 md:px-6 lg:px-8">
<a href="/"> <a href="/">
<img src="/logo.svg" alt="Brackt" className="h-6" /> <img src={logoUrl} alt="Brackt" className="h-6" />
</a> </a>
</div> </div>
</header> </header>

View file

@ -10,6 +10,7 @@ import { buildOwnerMap } from "~/lib/owner-map";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { calculateFantasyPoints, calculateBracketPoints } from "~/models/scoring-rules"; import { calculateFantasyPoints, calculateBracketPoints } from "~/models/scoring-rules";
import logomarkUrl from "../../../../public/logomark.svg?url";
import type { CoronaState } from "~/components/draft/DraftPickCell"; import type { CoronaState } from "~/components/draft/DraftPickCell";
import type { Route } from "./+types/$leagueId.draft-board.$seasonId"; import type { Route } from "./+types/$leagueId.draft-board.$seasonId";
@ -269,7 +270,7 @@ export default function DraftBoard() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link to="/"> <Link to="/">
<img src="/logomark.svg" alt="Brackt" className="h-8" /> <img src={logomarkUrl} alt="Brackt" className="h-8" />
</Link> </Link>
<h1 className="text-2xl font-bold"> <h1 className="text-2xl font-bold">
{season.league.name} Draft Board {season.league.name} Draft Board

View file

@ -11,6 +11,7 @@ import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "~/components/ui/dialog";
import { getAuth } from "@clerk/react-router/server"; import { getAuth } from "@clerk/react-router/server";
import { getTeamQueue } from "~/models/draft-queue"; import { getTeamQueue } from "~/models/draft-queue";
import logomarkUrl from "../../../../public/logomark.svg?url";
import { buildOwnerMap } from "~/lib/owner-map"; import { buildOwnerMap } from "~/lib/owner-map";
import { DraftSidebar } from "~/components/DraftSidebar"; import { DraftSidebar } from "~/components/DraftSidebar";
import { TeamRosterView } from "~/components/draft/TeamRosterView"; import { TeamRosterView } from "~/components/draft/TeamRosterView";
@ -1657,7 +1658,7 @@ export default function DraftRoom() {
</Button> </Button>
)} )}
<Link to="/"> <Link to="/">
<img src="/logomark.svg" alt="Brackt" className="h-8" /> <img src={logomarkUrl} alt="Brackt" className="h-8" />
</Link> </Link>
<h1 className="text-lg md:text-xl font-bold"> <h1 className="text-lg md:text-xl font-bold">
Draft Room Draft Room