Fix brackets on homepage.

This commit is contained in:
Chris Parsons 2026-04-22 22:17:57 -07:00
parent 924ad5caee
commit 886f9f1847

View file

@ -31,7 +31,7 @@ function getInterval(tick: number): number {
}
function SlotMachineHeadline() {
const [text, setText] = useState("\u00A0");
const [text, setText] = useState(SPORTS[0]);
const [landed, setLanded] = useState(false);
const [animKey, setAnimKey] = useState(0);
const tickRef = useRef(0);
@ -82,129 +82,130 @@ function SlotMachineHeadline() {
// ─── Bracket Decoration ──────────────────────────────────────────────────────
const LIME = "#adf661";
const LIME_DIM = "rgba(173,246,97,0.07)";
const LIME_DIM_BORDER = "rgba(173,246,97,0.25)";
const LIME_CHAMP = "rgba(173,246,97,0.12)";
const LIME_CHAMP_BORDER = "rgba(173,246,97,0.8)";
const SLOT_BG = "rgba(32,33,36,0.95)";
const SLOT_BORDER = "rgba(255,255,255,0.10)";
const LINE_COLOR_R1 = "rgba(255,255,255,0.15)";
const LINE_COLOR_R2 = "rgba(255,255,255,0.28)";
function BracketDecor({ flip = false, mobileTall = false }: { flip?: boolean; mobileTall?: boolean }) {
const base = flip ? "r" : "l";
const id = mobileTall ? `${base}-m` : base;
const W = 399;
const H = mobileTall ? 2000 : 560;
// SVG coordinate-space layout constants
const SW = 80;
const SH = 20;
const GAP = 10;
const PAIR_H = SH * 2 + GAP;
const R1_X = 10;
const R2_X = 120;
const R3_X = 210;
const BRACKET_W = 300;
const BRACKET_H = 360;
const R1_TOP = 50;
const PAIR_GAP = 20;
const pad = 36;
const spacing = (H - 2 * pad) / 7;
const r1Ys = Array.from({ length: 8 }, (_, i) => pad + i * spacing);
const r2Ys = [0, 1, 2, 3].map((i) => (r1Ys[i * 2] + r1Ys[i * 2 + 1]) / 2);
const r3Ys = [0, 1].map((i) => (r2Ys[i * 2] + r2Ys[i * 2 + 1]) / 2);
const champY = (r3Ys[0] + r3Ys[1]) / 2;
type SlotDef = {
x: number; y: number; w: number; h: number;
winner?: boolean; champ?: boolean;
};
const x0 = 4;
const x1 = 77;
const x2 = 161;
const x3 = 252;
function BracketSlot({ x, y, w, h, winner, champ }: SlotDef) {
const bg = champ ? LIME_CHAMP : winner ? LIME_DIM : SLOT_BG;
const border = champ ? LIME_CHAMP_BORDER : winner ? LIME_DIM_BORDER : SLOT_BORDER;
const bw = champ ? 1.5 : 1;
return (
<g>
<rect x={x} y={y} width={w} height={h} rx={2} fill={bg} stroke={border} strokeWidth={bw} />
{(winner || champ) && (
<rect x={x} y={y} width={3} height={h} rx={1} fill={LIME} />
)}
{champ && (
<text
x={x + w / 2} y={y + h + 10}
textAnchor="middle"
fontSize={7} fontWeight={700} fill={LIME}
style={{ fontFamily: "Barlow, sans-serif", letterSpacing: "0.08em" }}
>
CHAMP
</text>
)}
</g>
);
}
const gradId = `grad-${id}`;
const glowId = `glow-${id}`;
const finalsGlowId = `finals-glow-${id}`;
const finalsHaloId = `finals-halo-${id}`;
const finalsFadeMaskGradId = `finals-fade-mask-grad-${id}`;
const finalsFadeMaskId = `finals-fade-mask-${id}`;
const finalsLineGlowId = `finals-line-glow-${id}`;
function hLine(x1: number, y1: number, x2: number, color: string) {
return <line x1={x1} y1={y1} x2={x2} y2={y1} stroke={color} strokeWidth={1} />;
}
function connector(ax: number, ay: number, bx: number, by: number, cx: number, color: string) {
const midY = (ay + by) / 2;
return (
<path d={`M${ax} ${ay} H${cx} V${midY} H${bx}`} fill="none" stroke={color} strokeWidth={1} />
);
}
function BracketDecor({ flip = false }: { flip?: boolean }) {
const r1Pairs: [SlotDef, SlotDef][] = Array.from({ length: 4 }, (_, i) => {
const pairY = R1_TOP + i * (PAIR_H + PAIR_GAP);
const winner = i % 3 !== 1;
return [
{ x: R1_X, y: pairY, w: SW, h: SH, winner: winner && i % 2 === 0 },
{ x: R1_X, y: pairY + SH + GAP, w: SW, h: SH, winner: winner && i % 2 !== 0 },
];
});
const r2Slots: SlotDef[] = [0, 1].map((i) => {
const top1Y = R1_TOP + (i * 2) * (PAIR_H + PAIR_GAP);
const bot2Y = R1_TOP + (i * 2 + 1) * (PAIR_H + PAIR_GAP) + SH + GAP;
const midY = (top1Y + bot2Y) / 2 - SH / 2;
return { x: R2_X, y: midY, w: SW, h: SH, winner: i === 0 };
});
const r3Y = (r2Slots[0].y + r2Slots[1].y + SH) / 2 - SH / 2;
const r3Slot: SlotDef = { x: R3_X, y: r3Y, w: SW, h: SH + 4, champ: true };
return (
<svg
width={BRACKET_W} height={BRACKET_H}
viewBox={`0 0 ${W} ${H}`}
style={{
position: "absolute",
top: "50%",
transform: `translateY(-52%)${flip ? " scaleX(-1)" : ""}`,
[flip ? "right" : "left"]: 0,
opacity: 0.75,
pointerEvents: "none",
height: mobileTall ? "auto" : "75vh",
width: mobileTall ? "32vw" : "auto",
}}
>
{r1Pairs.map(([a, b], i) => (
<g key={`r1-${a.y}`}>
<BracketSlot {...a} />
<BracketSlot {...b} />
{connector(
R1_X + SW, a.y + SH / 2,
r2Slots[Math.floor(i / 2)].x, r2Slots[Math.floor(i / 2)].y + SH / 2,
R1_X + SW + 14,
LINE_COLOR_R1
)}
{hLine(R1_X + SW + 14, (a.y + SH / 2 + b.y + SH / 2) / 2, R2_X, LINE_COLOR_R1)}
</g>
))}
{r2Slots.map((s, ri) => (
<g key={`r2-${s.y}`}>
<BracketSlot {...s} />
{connector(
R2_X + SW, r2Slots[0].y + SH / 2,
r3Slot.x, r3Slot.y + (r3Slot.h ?? SH) / 2,
R2_X + SW + 14,
LINE_COLOR_R2
)}
{ri === 1 && hLine(R2_X + SW + 14, (r2Slots[0].y + SH / 2 + r2Slots[1].y + SH / 2) / 2, R3_X, LINE_COLOR_R2)}
</g>
))}
<BracketSlot {...r3Slot} />
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2={H} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#adf661" />
<stop offset="100%" stopColor="#2ce1c1" />
</linearGradient>
<filter id={glowId} x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id={finalsGlowId} filterUnits="userSpaceOnUse"
x={x2 - 15} y={r3Ys[0] - 15} width={x3 - x2 + 30} height={r3Ys[1] - r3Ys[0] + 30}>
<feGaussianBlur stdDeviation="7" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id={finalsHaloId} filterUnits="userSpaceOnUse"
x={x2 - 40} y={r3Ys[0] - 40} width={x3 - x2 + 80} height={r3Ys[1] - r3Ys[0] + 80}>
<feGaussianBlur stdDeviation="16" />
</filter>
<linearGradient id={finalsFadeMaskGradId} x1={x3} y1="0" x2={W} y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="white" />
<stop offset="50%" stopColor="white" />
<stop offset="100%" stopColor="white" stopOpacity="0" />
</linearGradient>
<mask id={finalsFadeMaskId} maskUnits="userSpaceOnUse" x={x3} y={champY - 20} width={W - x3} height={40}>
<rect x="0" y="0" width={W} height={H} fill={`url(#${finalsFadeMaskGradId})`} />
</mask>
<filter id={finalsLineGlowId} filterUnits="userSpaceOnUse" x={x3 - 10} y={champY - 15} width={W - x3 + 20} height={30}>
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<g filter={`url(#${finalsHaloId})`} stroke={`url(#${gradId})`} strokeWidth={2} fill="none" opacity={0.75}>
{r3Ys.map((midY) => (
<line key={midY} x1={x2} y1={midY} x2={x3} y2={midY} />
))}
<line x1={x3} y1={r3Ys[0]} x2={x3} y2={r3Ys[1]} />
</g>
<g filter={`url(#${finalsGlowId})`} stroke={`url(#${gradId})`} strokeWidth={1.5} fill="none">
{r3Ys.map((midY) => (
<line key={midY} x1={x2} y1={midY} x2={x3} y2={midY} />
))}
<line x1={x3} y1={r3Ys[0]} x2={x3} y2={r3Ys[1]} />
</g>
<g filter={`url(#${glowId})`} stroke={`url(#${gradId})`} strokeWidth={1.5} fill="none">
{r1Ys.map((y) => (
<g key={y}>
<circle cx={x0} cy={y} r={3} fill={`url(#${gradId})`} stroke="none" />
<line x1={x0} y1={y} x2={x1} y2={y} />
</g>
))}
{r2Ys.map((midY, i) => (
<g key={midY}>
<line x1={x1} y1={r1Ys[i * 2]} x2={x1} y2={r1Ys[i * 2 + 1]} />
<circle cx={x1} cy={midY} r={3} fill={`url(#${gradId})`} stroke="none" />
<line x1={x1} y1={midY} x2={x2} y2={midY} />
</g>
))}
{r3Ys.map((midY, i) => (
<g key={midY}>
<line x1={x2} y1={r2Ys[i * 2]} x2={x2} y2={r2Ys[i * 2 + 1]} />
<circle cx={x2} cy={midY} r={3} fill={`url(#${gradId})`} stroke="none" />
<line x1={x2} y1={midY} x2={x3} y2={midY} />
</g>
))}
<line x1={x3} y1={r3Ys[0]} x2={x3} y2={r3Ys[1]} />
<circle cx={x3} cy={champY} r={3} fill={`url(#${gradId})`} stroke="none" />
</g>
<line
x1={x3} y1={champY} x2={W} y2={champY}
stroke={`url(#${gradId})`}
strokeWidth={1.5}
fill="none"
filter={`url(#${finalsLineGlowId})`}
mask={`url(#${finalsFadeMaskId})`}
/>
</svg>
);
}
@ -303,6 +304,7 @@ const FOOTER_LINKS = [
{ label: "Home", to: "/" },
{ label: "Rules", to: "/rules" },
{ label: "How to Play", to: "/how-to-play" },
{ label: "Support", to: "/support" },
{ label: "Privacy Policy", to: "/privacy-policy" },
];
@ -345,7 +347,23 @@ export function LandingPage() {
className="relative flex flex-col items-center justify-center overflow-hidden px-8"
style={{ minHeight: "calc(100vh - var(--navbar-height))" }}
>
{/* Bracket decorations — lg+ only */}
{/* Dot-grid texture */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage: "radial-gradient(circle, rgba(255,255,255,0.07) 1px, transparent 1px)",
backgroundSize: "32px 32px",
maskImage: "radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 100%)",
WebkitMaskImage: "radial-gradient(ellipse 85% 85% at 50% 50%, black 30%, transparent 100%)",
}}
/>
{/* Bracket decorations */}
<div className="lg:hidden">
<BracketDecor mobileTall />
<BracketDecor flip mobileTall />
</div>
<div className="hidden lg:block">
<BracketDecor />
<BracketDecor flip />
@ -354,7 +372,7 @@ export function LandingPage() {
<ScrollHint />
{/* Center content */}
<div className="relative z-10 w-full max-w-[680px] text-center">
<div className="relative z-10 w-[80%] lg:w-full max-w-[680px] text-center px-6 py-4 lg:px-0 lg:py-0">
<h1
className="mb-0 leading-[0.95] tracking-[-0.04em]"
style={{ fontSize: "clamp(38px, 8vw, 88px)", fontWeight: 900 }}
@ -365,10 +383,10 @@ export function LandingPage() {
<p
className="mx-auto mt-8 text-muted-foreground"
style={{ fontSize: "16px", fontWeight: 400, lineHeight: 1.65, maxWidth: "460px" }}
style={{ fontSize: "16px", fontWeight: 400, lineHeight: 1.65, maxWidth: "414px" }}
>
Draft teams across every sport NHL, NBA, NFL, EPL, World Cup, and more.
Compete with your friends in a league all season long.
Draft teams across every league and sport, like the NFL, NHL, EPL, F1, and more.
Compete against your friends in a season-long league.
</p>
<div className="mt-8 flex flex-col items-center justify-center gap-2.5 sm:flex-row">