brackt/app/components/league/LeagueRow.tsx

251 lines
9.2 KiB
TypeScript
Raw Normal View History

import { formatDistanceToNow } from "date-fns";
import { Link } from "react-router";
import { Button } from "~/components/ui/button";
import { LeagueAvatar } from "./LeagueAvatar";
export interface LeagueRowProps {
leagueId: string;
leagueName: string;
numSports: number;
status: "draft" | "active" | "pre_draft" | "completed";
seasonId?: string;
currentRank?: number;
totalPoints?: number;
previousRank?: number;
completionPercentage?: number;
draftDateTime?: string | null;
picksUntilMyTurn?: number;
draftPosition?: number;
}
// ─── 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"}`;
}
// ─── 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" }) {
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>
);
}
// ─── 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) {
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">
<p className="font-semibold leading-tight truncate">{leagueName}</p>
<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,
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">
{currentRank !== undefined && (
<StatColumn label="Ranking">
<span className="text-2xl font-bold leading-none">#{currentRank}</span>
<RankChange current={currentRank} previous={previousRank} />
</StatColumn>
)}
{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>
);
}
// ─── Public export ────────────────────────────────────────────────────────────
export function LeagueRow(props: LeagueRowProps) {
if (props.status === "draft") return <DraftRow {...props} />;
if (props.status === "active") return <ActiveRow {...props} />;
if (props.status === "pre_draft") return <PreDraftRow {...props} />;
return <CompletedRow {...props} />;
}