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 { Button } from "~/components/ui/button";
import { LeagueAvatar } from "./LeagueAvatar";
import { StatColumn, StatDivider, RankChangeIndicator } from "./StatHelpers";
export interface LeagueRowProps {
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 ──────────────────────────────────────────────────────
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 ─────────────────────────────────────────────────────────────
function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRowProps) {
@ -156,7 +119,9 @@ function ActiveRow({
{currentRank !== undefined && (
<StatColumn label="Ranking">
<span className="text-2xl font-bold leading-none">#{currentRank}</span>
<RankChange current={currentRank} previous={previousRank} />
{previousRank !== undefined && previousRank !== currentRank && (
<RankChangeIndicator delta={previousRank - currentRank} />
)}
</StatColumn>
)}
{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 { GradientIcon } from "~/components/ui/GradientIcon";
import { TeamAvatar } from "~/components/TeamAvatar";
// ─── 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>
);
}
import { StatColumn, StatDivider, RankChangeIndicator, PointChangeIndicator } from "./StatHelpers";
// ─── Row styles ───────────────────────────────────────────────────────────────
@ -92,9 +39,9 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
{/* 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>
<span className="text-2xl font-bold leading-none">#{String(entry.displayRank).replace(/^T/, "")}</span>
{entry.rankChange !== undefined && entry.rankChange !== 0 && (
<RankChangeIndicator change={entry.rankChange} />
<RankChangeIndicator delta={entry.rankChange} />
)}
</StatColumn>
<StatDivider />
@ -103,7 +50,7 @@ function RowContent({ entry }: { entry: StandingsPreviewEntry }) {
{Math.round(entry.points).toLocaleString("en-US")}
</span>
{entry.pointChange !== undefined && entry.pointChange !== 0 && (
<PointChangeIndicator change={entry.pointChange} />
<PointChangeIndicator delta={entry.pointChange} />
)}
</StatColumn>
</div>
@ -117,7 +64,7 @@ export interface StandingsPreviewEntry {
teamId: string;
teamName: string;
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;
/** Numeric rank used to select the gold/silver/bronze row tint (13 only). */
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 { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/react-router";
import { HelpCircle, MenuIcon, Settings } from "lucide-react";
import logoUrl from "../../public/logo.svg?url";
interface NavbarProps {
isAdmin: boolean;
@ -24,7 +25,7 @@ export function Navbar({ isAdmin }: NavbarProps) {
{/* Left: Logo + Nav links */}
<div className="flex items-center gap-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>
<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>

View file

@ -20,6 +20,7 @@ import { BracktGradients } from "~/components/ui/BracktGradients";
import { Footer } from "~/components/marketing/Footer";
import { isUserAdminByClerkId } from "~/models/user";
import { AlertCircle, FileQuestion, Lock, ShieldOff, ServerCrash } from "lucide-react";
import logoUrl from "../../public/logo.svg?url";
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">
<div className="flex h-16 items-center px-4 md:px-6 lg:px-8">
<a href="/">
<img src="/logo.svg" alt="Brackt" className="h-6" />
<img src={logoUrl} alt="Brackt" className="h-6" />
</a>
</div>
</header>

View file

@ -10,6 +10,7 @@ import { buildOwnerMap } from "~/lib/owner-map";
import { Button } from "~/components/ui/button";
import { ArrowLeft } from "lucide-react";
import { calculateFantasyPoints, calculateBracketPoints } from "~/models/scoring-rules";
import logomarkUrl from "../../../../public/logomark.svg?url";
import type { CoronaState } from "~/components/draft/DraftPickCell";
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 gap-3">
<Link to="/">
<img src="/logomark.svg" alt="Brackt" className="h-8" />
<img src={logomarkUrl} alt="Brackt" className="h-8" />
</Link>
<h1 className="text-2xl font-bold">
{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 { getAuth } from "@clerk/react-router/server";
import { getTeamQueue } from "~/models/draft-queue";
import logomarkUrl from "../../../../public/logomark.svg?url";
import { buildOwnerMap } from "~/lib/owner-map";
import { DraftSidebar } from "~/components/DraftSidebar";
import { TeamRosterView } from "~/components/draft/TeamRosterView";
@ -1657,7 +1658,7 @@ export default function DraftRoom() {
</Button>
)}
<Link to="/">
<img src="/logomark.svg" alt="Brackt" className="h-8" />
<img src={logomarkUrl} alt="Brackt" className="h-8" />
</Link>
<h1 className="text-lg md:text-xl font-bold">
Draft Room