brackt/app/components/league/LeagueRow.tsx
chrisp 841760541d
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m27s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m16s
🚀 Deploy / 🔍 Lint (push) Successful in 1m55s
🚀 Deploy / 🐳 Build (push) Successful in 14m23s
🚀 Deploy / 🚀 Deploy (push) Successful in 10s
Fix snake draft Discord pick numbering and small UX tweaks (#2)
## Summary

- **Discord snake draft fix**: pick notifications now show the sequential pick-in-round (e.g. "Round 2, Pick 9") rather than the snake-adjusted slot position ("Round 2, Pick 5"). Also removes the now-unused \`pickInRound\` param from \`notifyPickMadeOnDiscord\` and adds a regression test for the 13-team case.
- **League card**: league name in draft-in-progress cards is now a link to the league homepage (the Enter Draft button still goes to the draft room).
- **Overnight pause label**: shortened to "🌙 Pause" — the "Resumes 4:00 AM" line below already provides context, so "Overnight" was just causing wrapping.
- **Queue & Picks backgrounds**: items use \`bg-card\` instead of \`bg-muted\` for better visual separation from the panel background.

## Test plan

- [x] Discord: in a snake draft, verify pick #22 of 13 shows "Round 2, Pick 9" in Discord
- [x] League card: dashboard with a draft-in-progress league — click name → league homepage, click Enter Draft → draft room
- [x] Overnight pause: pause a draft overnight — cell shows "🌙 Pause" on one line, "Resumes X:XX" below
- [x] Queue/Picks: open draft room and confirm queue items and recent picks visually pop from the sidebar background
- [x] Unit tests: `npm run test:run` passes (13 discord tests)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #2
2026-05-22 04:18:11 +00:00

234 lines
8.8 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 { formatDistanceToNow } from "date-fns";
import { Link } from "react-router";
import { Button } from "~/components/ui/button";
import { LeagueAvatar } from "./LeagueAvatar";
import { StatColumn, StatDivider, RankingDisplay } from "./StatHelpers";
export interface LeagueRowProps {
leagueId: string;
leagueName: string;
numSports: number;
status: "draft" | "active" | "pre_draft" | "completed";
seasonId?: string;
displayRank?: string | number;
currentRank?: number;
totalPoints?: number;
previousRank?: number;
completionPercentage?: number;
draftDateTime?: string | null;
picksUntilMyTurn?: number;
draftPosition?: number;
isCommishOnly?: boolean;
}
// ─── Helpers ──────────────────────────────────────────────────────────────────
function ordinal(n: number): string {
const v = n % 100;
// 1113 are always "th" (e.g. 11th, 12th, 13th)
if (v >= 11 && v <= 13) return `${n}th`;
const s = ["th", "st", "nd", "rd"];
return `${n}${s[n % 10] ?? "th"}`;
}
// ─── Season progress bar ──────────────────────────────────────────────────────
function SeasonProgress({ pct, status }: { pct: number; status: "active" | "completed" }) {
const label = status === "completed" ? "100% Complete" : `${pct}% Complete`;
const fill = status === "completed" ? 100 : pct;
return (
<div className="flex items-center gap-1.5 mt-1.5 w-40">
<div className="flex-1 h-1 rounded-full bg-white/10 overflow-hidden">
<div
className="h-full rounded-full bg-electric transition-all"
style={{ width: `${fill}%` }}
/>
</div>
<span className="text-xs text-muted-foreground shrink-0">{label}</span>
</div>
);
}
// ─── Row variants ─────────────────────────────────────────────────────────────
function DraftRow({ leagueId, leagueName, seasonId, picksUntilMyTurn }: LeagueRowProps) {
const draftUrl = `/leagues/${leagueId}/draft/${seasonId}`;
const picksLabel =
picksUntilMyTurn === 0
? "You're on the clock!"
: picksUntilMyTurn !== undefined
? `Up in ${picksUntilMyTurn} pick${picksUntilMyTurn === 1 ? "" : "s"}`
: null;
return (
<div className="flex items-start gap-3 rounded-lg border border-primary/40 bg-primary/10 px-3 py-3 sm:px-5 sm:py-4">
<LeagueAvatar leagueId={leagueId} leagueName={leagueName} />
<div className="flex-1 min-w-0">
<Link to={`/leagues/${leagueId}`} className="font-semibold leading-tight truncate hover:underline block">{leagueName}</Link>
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
<div className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 animate-pulse rounded-full bg-electric" />
<span className="text-xs font-semibold tracking-wide text-electric uppercase">
Draft in Progress
</span>
</div>
{picksLabel && (
<span className="text-xs text-muted-foreground">{picksLabel}</span>
)}
</div>
{seasonId && (
<Button asChild size="sm" className="mt-3 sm:hidden">
<Link to={draftUrl}>Enter Draft</Link>
</Button>
)}
</div>
{seasonId && (
<Button asChild size="sm" className="shrink-0 hidden sm:inline-flex">
<Link to={draftUrl}>Enter Draft</Link>
</Button>
)}
</div>
);
}
function ActiveRow({
leagueId,
leagueName,
displayRank,
currentRank,
totalPoints,
previousRank,
completionPercentage = 0,
}: LeagueRowProps) {
const showStats = currentRank !== undefined || totalPoints !== undefined;
return (
<Link
to={`/leagues/${leagueId}`}
className="flex flex-col sm:flex-row sm:items-center gap-3 rounded-lg bg-card px-3 py-3 sm:px-5 sm:py-4 hover:bg-white/[0.06] transition-colors"
>
{/* Avatar + name */}
<div className="flex items-center gap-3 min-w-0 flex-1">
<LeagueAvatar leagueId={leagueId} leagueName={leagueName} />
<div className="min-w-0">
<p className="font-semibold leading-tight truncate">{leagueName}</p>
<SeasonProgress pct={completionPercentage} status="active" />
</div>
</div>
{/* Stats — second row on mobile, right side on desktop */}
{showStats && (
<div className="flex items-center gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
{displayRank !== undefined && (
<RankingDisplay
displayRank={displayRank}
rankChange={previousRank !== undefined && currentRank !== undefined && previousRank !== currentRank ? previousRank - currentRank : undefined}
/>
)}
{currentRank !== undefined && totalPoints !== undefined && <StatDivider />}
{totalPoints !== undefined && (
<StatColumn label="Points">
<span className="text-2xl font-bold leading-none text-electric">
{Math.round(totalPoints).toLocaleString("en-US")}
</span>
</StatColumn>
)}
</div>
)}
</Link>
);
}
function PreDraftRow({
leagueId,
leagueName,
draftDateTime,
draftPosition,
}: LeagueRowProps) {
let draftTimeValue = "Not scheduled";
if (draftDateTime) {
const draftDate = new Date(draftDateTime);
draftTimeValue =
draftDate > new Date()
? formatDistanceToNow(draftDate, { addSuffix: true })
: "Starting soon";
}
return (
<Link
to={`/leagues/${leagueId}`}
className="flex flex-col sm:flex-row sm:items-center gap-3 rounded-lg bg-card px-3 py-3 sm:px-5 sm:py-4 hover:bg-white/[0.06] transition-colors"
>
{/* Avatar + name */}
<div className="flex items-center gap-3 min-w-0 flex-1">
<LeagueAvatar leagueId={leagueId} leagueName={leagueName} />
<div className="min-w-0">
<p className="font-semibold leading-tight truncate">{leagueName}</p>
<p className="text-xs text-muted-foreground mt-0.5">Pre-Draft</p>
</div>
</div>
{/* Stats — second row on mobile, right side on desktop */}
<div className="flex items-center gap-4 border-t border-border/50 pt-2 sm:border-0 sm:pt-0 sm:shrink-0">
<StatColumn label="Draft">
<span className="text-2xl font-bold leading-none">{draftTimeValue}</span>
</StatColumn>
{draftPosition !== undefined && (
<>
<StatDivider />
<StatColumn label="Position">
<span className="text-2xl font-bold leading-none">
{ordinal(draftPosition)}
</span>
</StatColumn>
</>
)}
</div>
</Link>
);
}
function CompletedRow({
leagueId,
leagueName,
completionPercentage = 0,
}: LeagueRowProps) {
return (
<Link
to={`/leagues/${leagueId}`}
className="flex items-center gap-3 rounded-lg bg-card px-3 py-3 sm:px-5 sm:py-4 hover:bg-white/[0.06] transition-colors"
>
<LeagueAvatar leagueId={leagueId} leagueName={leagueName} />
<div className="flex-1 min-w-0">
<p className="font-semibold leading-tight truncate">{leagueName}</p>
<SeasonProgress pct={completionPercentage} status="completed" />
</div>
</Link>
);
}
// ─── Commish-only row ─────────────────────────────────────────────────────────
function CommishOnlyRow({ leagueId, leagueName }: Pick<LeagueRowProps, "leagueId" | "leagueName">) {
return (
<Link
to={`/leagues/${leagueId}`}
className="flex items-center gap-3 rounded-lg bg-card px-3 py-3 sm:px-5 sm:py-4 hover:bg-white/[0.06] transition-colors"
>
<LeagueAvatar leagueId={leagueId} leagueName={leagueName} />
<div className="min-w-0">
<p className="font-semibold leading-tight truncate">{leagueName}</p>
<p className="text-xs text-muted-foreground mt-0.5">Commissioner</p>
</div>
</Link>
);
}
// ─── Public export ────────────────────────────────────────────────────────────
export function LeagueRow(props: LeagueRowProps) {
if (props.status === "draft") return <DraftRow {...props} />;
if (props.isCommishOnly) return <CommishOnlyRow {...props} />;
if (props.status === "active") return <ActiveRow {...props} />;
if (props.status === "pre_draft") return <PreDraftRow {...props} />;
return <CompletedRow {...props} />;
}