## 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
160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
import { forwardRef, type CSSProperties, type ComponentPropsWithoutRef } from "react";
|
|
import { cn } from "~/lib/utils";
|
|
import type { SeasonStatus } from "~/models/season";
|
|
import { BRACKT_GRADIENT } from "~/lib/brand";
|
|
export type { CoronaState } from "~/lib/corona-states";
|
|
import type { CoronaState } from "~/lib/corona-states";
|
|
|
|
type CoronaVariant =
|
|
| "gradient"
|
|
| "electric"
|
|
| "emerald"
|
|
| "amber"
|
|
| "coral";
|
|
|
|
type DraftPickCellProps = ComponentPropsWithoutRef<"div"> & {
|
|
pickNumber: number;
|
|
round: number;
|
|
pickInRound: number;
|
|
state: "picked" | "current" | "upcoming";
|
|
pick?: {
|
|
participant: { name: string };
|
|
sport: { name: string };
|
|
};
|
|
corona?: CoronaVariant;
|
|
coronaState?: CoronaState;
|
|
seasonStatus?: SeasonStatus;
|
|
draftPaused?: boolean;
|
|
isOvernightPause?: boolean;
|
|
resumesAt?: Date;
|
|
};
|
|
|
|
const coronaClasses: Record<CoronaVariant, string> = {
|
|
gradient: "[background:var(--brackt-gradient)]",
|
|
electric: "bg-electric",
|
|
emerald: "bg-emerald-500",
|
|
amber: "bg-amber-accent",
|
|
coral: "bg-coral-accent",
|
|
};
|
|
|
|
function getCoronaStyle(cs: CoronaState): CSSProperties {
|
|
switch (cs.type) {
|
|
case "eliminated":
|
|
return { background: "oklch(0.45 0.20 25)" };
|
|
case "pending":
|
|
return { background: "rgba(255, 255, 255, 0.08)" };
|
|
case "scored":
|
|
return {
|
|
background: BRACKT_GRADIENT,
|
|
opacity: cs.brightness,
|
|
};
|
|
}
|
|
}
|
|
|
|
export const DraftPickCell = forwardRef<HTMLDivElement, DraftPickCellProps>(
|
|
function DraftPickCell({
|
|
pickNumber,
|
|
round,
|
|
pickInRound,
|
|
state,
|
|
pick,
|
|
corona = "gradient",
|
|
coronaState,
|
|
seasonStatus,
|
|
draftPaused,
|
|
isOvernightPause,
|
|
resumesAt,
|
|
className,
|
|
children,
|
|
...rest
|
|
}, ref) {
|
|
const isCurrent = state === "current";
|
|
const showCorona = !isCurrent;
|
|
|
|
let currentLabel = "On The Clock";
|
|
if (seasonStatus === "pre_draft") {
|
|
currentLabel = "Up Next";
|
|
} else if (seasonStatus === "draft" && isOvernightPause) {
|
|
currentLabel = "🌙 Pause";
|
|
} else if (seasonStatus === "draft" && draftPaused) {
|
|
currentLabel = "Paused Draft";
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex-1 min-w-20 overflow-hidden",
|
|
className,
|
|
)}
|
|
{...rest}
|
|
>
|
|
{isCurrent ? (
|
|
<div className="absolute inset-0 rounded-lg [background:var(--brackt-gradient)] animate-pulse-gradient" />
|
|
) : coronaState ? (
|
|
<div
|
|
className="absolute top-0.5 bottom-0.5 left-0 right-0 rounded-lg"
|
|
style={getCoronaStyle(coronaState)}
|
|
/>
|
|
) : state === "picked" ? (
|
|
<div
|
|
className={cn(
|
|
"absolute top-0.5 bottom-0.5 left-0 right-0 rounded-lg",
|
|
coronaClasses[corona],
|
|
)}
|
|
/>
|
|
) : state === "upcoming" ? (
|
|
<div
|
|
className="absolute top-0.5 bottom-0.5 left-0 right-0 rounded-lg"
|
|
style={{ background: "rgba(255, 255, 255, 0.08)" }}
|
|
/>
|
|
) : null}
|
|
<div
|
|
className={cn(
|
|
"relative z-10 h-14 border-2 rounded-lg p-1.5 transition-all",
|
|
(showCorona || isCurrent) && "mr-1",
|
|
isCurrent
|
|
? "bg-muted/80 border-transparent"
|
|
: showCorona
|
|
? "bg-muted border-transparent"
|
|
: "border-border bg-card",
|
|
)}
|
|
title={`Overall Pick #${pickNumber}`}
|
|
>
|
|
<div className={cn("flex flex-col", isCurrent ? "items-center justify-center h-full" : "gap-0.5")}>
|
|
{!isCurrent && (
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-[10px] font-mono text-muted-foreground leading-3">
|
|
{round}.{String(pickInRound).padStart(2, "0")}
|
|
</div>
|
|
{coronaState && coronaState.type !== "pending" && (
|
|
<span className={`text-[10px] font-mono font-bold ${coronaState.type === "eliminated" ? "text-coral-accent" : "text-emerald-400"}`}>
|
|
{coronaState.type === "scored" && "▲"}{coronaState.points}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
{state === "picked" && pick ? (
|
|
<>
|
|
<div className="font-semibold truncate text-xs leading-4">{pick.participant.name}</div>
|
|
<div className="text-muted-foreground truncate text-[10px] leading-3">
|
|
{pick.sport.name}
|
|
</div>
|
|
</>
|
|
) : isCurrent ? (
|
|
<>
|
|
<div className="text-sm font-bold text-white">{currentLabel}</div>
|
|
{resumesAt && (
|
|
<div className="text-[10px] text-muted-foreground mt-0.5">
|
|
Resumes {resumesAt.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}
|
|
</div>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</div>
|
|
{children && <div className="absolute inset-0 pointer-events-none [&_*]:pointer-events-auto">{children}</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
DraftPickCell.displayName = "DraftPickCell";
|