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:
parent
9ed0282fd0
commit
6926fb96bc
7 changed files with 76 additions and 101 deletions
|
|
@ -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 />}
|
||||
|
|
|
|||
|
|
@ -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 (1–3 only). */
|
||||
currentRank?: number;
|
||||
|
|
|
|||
59
app/components/league/StatHelpers.tsx
Normal file
59
app/components/league/StatHelpers.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue