* Redesign home page with new layout and component system - Two-column layout (My Leagues 2/3, Upcoming Events 1/3) with mobile stack - LeagueRow: square avatar, gradient draft highlight, rank/points display, progress bar - MyLeaguesCard, CreateLeagueCard with shared SectionCardHeader - UpcomingEventsCard: vertical timeline with grouped multi-league events - Shared gradient system: BracktGradients SVG defs, GradientIcon wrapper, brand.ts constants - Button default variant updated to green→cyan gradient - Navbar: plain nav links with gradient hover, support/admin icon buttons - Accessibility fixes: semantic h2 headings, aria-label on LeagueAvatar and nav elements - Storybook stories for all new components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Responsive league row layout and mobile polish - League rows stack avatar+name on top, stats full-width below on mobile - Stats spread to right side on sm+ screens with border separator on mobile - Tighter padding on mobile (px-3/py-3), full padding on sm+ - Card headers and content use px-3 sm:px-6 to reduce mobile gutters - Two-column home layout deferred to lg breakpoint (tablet gets stacked) - Active leagues sorted by completion percentage descending - Default rank 1 / 0 points for active leagues with no scoring events yet - Fix ordinal bug for 11th/12th/13th; add aria-labels to rank change indicators - Remove dead StatDivider className prop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Improve claude file. * Add StandingsPreview card component with podium row styling - New StandingsPreview component with gold/silver/bronze row tints for top 3, team avatar, and LeagueRow-style stat columns (Ranking + Points) with rank and 7-day point change indicators - Fix GradientIcon in Storybook by adding BracktGradients decorator to preview.tsx (renamed from .ts to support JSX) - Fix degenerate SVG gradient on horizontal strokes by switching BracktGradients to gradientUnits="userSpaceOnUse" with Lucide-space coordinates (0→24) - Revert erroneous fill: url(#gradient) from GradientIcon; stroke-only fix was sufficient once gradientUnits was corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update components on league homepage. * Finish up league page styling. * Work on standings page. * Add story for RecentScoresCard * Update Point Progression Chart. * Sort point progression legend by ranking and add team links to standings rows * Fix standings discrepancy on change. * Create draft cell component. * Update draft board page * Draft room improvements. * Update some draft room styling. * Fix context menu missing. * Move tab navigation and autodraft to header row, narrow sidebar * Virtualize available participants list, memoize draft room props Adds @tanstack/react-virtual to replace separate mobile/desktop lists with a single unified virtual scroll loop. Also memoizes miniDraftGrid and availableParticipantsSectionProps, and switches pick lookup from Array.find to a Map for O(1) access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update draft room UI. * More draft room fixes. * Draft room tweaks. * Fix Rosters page. * Queue Section fixes. * Mobile Draft fixes. * Fix draft board page. * Create bracket look. * Bracket work. * Finish bracket page. * Homepage initial styling * homepage copy * Add privacy policy. Fixes #88. * how to play copy * rules copy * Fix brackets on homepage. * Add footer to website. * Glow on dots. * Landing page copy. * Fix sidebar. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
446 lines
18 KiB
TypeScript
446 lines
18 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { Link } from "react-router";
|
|
import { Users, Shuffle, Trophy, ChevronDown } from "lucide-react";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Card, CardContent } from "~/components/ui/card";
|
|
import { SectionCardHeader } from "~/components/ui/SectionCardHeader";
|
|
|
|
// ─── Slot Machine ────────────────────────────────────────────────────────────
|
|
|
|
const SPORTS = [
|
|
"Football", "Baseball", "Hockey", "Basketball", "Soccer", "Tennis", "Golf",
|
|
"eSports", "Formula 1", "Aussie Rules", "Lacrosse", "Darts", "College Football", "Snooker", "March Madness",
|
|
];
|
|
|
|
function shuffle<T>(arr: T[]): T[] {
|
|
const a = [...arr];
|
|
for (let i = a.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[a[i], a[j]] = [a[j], a[i]];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
const TOTAL_TICKS = 12;
|
|
|
|
// WCAG 2.3.1: no more than 3 flashes/second → minimum interval 334 ms.
|
|
// Curve runs 800 ms → 340 ms over 12 ticks (ramps to full speed at tick 8).
|
|
function getInterval(tick: number): number {
|
|
const pct = Math.min((tick / TOTAL_TICKS) * 1.5, 1);
|
|
return Math.round(800 - (800 - 340) * (pct * pct));
|
|
}
|
|
|
|
function SlotMachineHeadline() {
|
|
const [text, setText] = useState(SPORTS[0]);
|
|
const [landed, setLanded] = useState(false);
|
|
const [animKey, setAnimKey] = useState(0);
|
|
const tickRef = useRef(0);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const sportsSeq = useRef<string[]>([]);
|
|
|
|
useEffect(() => {
|
|
sportsSeq.current = shuffle(SPORTS);
|
|
let idx = 0;
|
|
|
|
function tick() {
|
|
tickRef.current++;
|
|
const sport = sportsSeq.current[idx % sportsSeq.current.length];
|
|
idx++;
|
|
setText(sport);
|
|
setAnimKey((k) => k + 1);
|
|
|
|
if (tickRef.current >= TOTAL_TICKS) {
|
|
setLanded(true);
|
|
return;
|
|
}
|
|
timerRef.current = setTimeout(tick, getInterval(tickRef.current));
|
|
}
|
|
|
|
timerRef.current = setTimeout(tick, 800);
|
|
return () => {
|
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
};
|
|
}, []);
|
|
|
|
if (landed) {
|
|
return (
|
|
<span
|
|
className="animate-sport-land block bg-clip-text text-transparent pb-[0.12em]"
|
|
style={{ backgroundImage: "linear-gradient(to bottom, #adf661, #2ce1c1)" }}
|
|
>
|
|
Every Sport.
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span key={animKey} className="animate-spin-in block text-white">
|
|
{text}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// ─── Bracket Decoration ──────────────────────────────────────────────────────
|
|
|
|
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;
|
|
|
|
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;
|
|
|
|
const x0 = 4;
|
|
const x1 = 77;
|
|
const x2 = 161;
|
|
const x3 = 252;
|
|
|
|
const gradId = `grad-${id}`;
|
|
const glowId = `glow-${id}`;
|
|
const dotsGlowId = `dots-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}`;
|
|
|
|
|
|
return (
|
|
<svg
|
|
viewBox={`0 0 ${W} ${H}`}
|
|
style={{
|
|
position: "absolute",
|
|
top: "50%",
|
|
transform: `translateY(-52%)${flip ? " scaleX(-1)" : ""}`,
|
|
[flip ? "right" : "left"]: 0,
|
|
pointerEvents: "none",
|
|
height: mobileTall ? "auto" : "75vh",
|
|
width: mobileTall ? "32vw" : "auto",
|
|
}}
|
|
>
|
|
<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>
|
|
<filter id={dotsGlowId} x="-200%" y="-200%" width="500%" height="500%">
|
|
<feGaussianBlur stdDeviation="5" result="blur" />
|
|
<feMerge>
|
|
<feMergeNode in="blur" />
|
|
<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" filter={`url(#${dotsGlowId})`} />
|
|
<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" filter={`url(#${dotsGlowId})`} />
|
|
<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" filter={`url(#${dotsGlowId})`} />
|
|
<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" filter={`url(#${dotsGlowId})`} />
|
|
</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>
|
|
);
|
|
}
|
|
|
|
// ─── Scroll Hint ─────────────────────────────────────────────────────────────
|
|
|
|
function ScrollHint() {
|
|
const [visible, setVisible] = useState(true);
|
|
useEffect(() => {
|
|
const onScroll = () => { if (window.scrollY > 50) setVisible(false); };
|
|
window.addEventListener("scroll", onScroll, { passive: true });
|
|
return () => window.removeEventListener("scroll", onScroll);
|
|
}, []);
|
|
return (
|
|
<div
|
|
className="pointer-events-none absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce transition-opacity duration-500"
|
|
style={{ opacity: visible ? 0.35 : 0 }}
|
|
>
|
|
<ChevronDown className="h-6 w-6 text-white" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Sport Ticker ─────────────────────────────────────────────────────────────
|
|
|
|
const TICKER_ITEMS = [
|
|
...SPORTS.map((s) => ({ sport: s.toUpperCase(), key: `${s}-a` })),
|
|
...SPORTS.map((s) => ({ sport: s.toUpperCase(), key: `${s}-b` })),
|
|
];
|
|
|
|
function SportTicker() {
|
|
return (
|
|
<div className="relative overflow-hidden pb-px" style={{ marginTop: "52px" }}>
|
|
<div
|
|
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-[60px]"
|
|
style={{ background: "linear-gradient(to right, #14171e, transparent)" }}
|
|
/>
|
|
<div
|
|
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-[60px]"
|
|
style={{ background: "linear-gradient(to left, #14171e, transparent)" }}
|
|
/>
|
|
<div className="flex animate-ticker" style={{ width: "max-content" }}>
|
|
{TICKER_ITEMS.map(({ sport, key }) => (
|
|
<span
|
|
key={key}
|
|
className="mx-1.5 shrink-0 rounded-[3px] px-3 py-1 text-[11px] font-bold uppercase tracking-[0.05em] border bg-card text-muted-foreground"
|
|
>
|
|
{sport}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Static data ──────────────────────────────────────────────────────────────
|
|
|
|
const HOW_IT_WORKS_STEPS = [
|
|
{
|
|
icon: Users,
|
|
title: "Create a League",
|
|
body: "Start a league, pick your sports (we can help), and invite your friends. It takes 30 seconds to set the stage.",
|
|
},
|
|
{
|
|
icon: Shuffle,
|
|
title: "Draft Your Teams",
|
|
body: "Pick teams from all the sports in order to build a superteam to win you bragging rights! Draft your way, slow or fast, with standard timers or high-pressure chess clocks.",
|
|
},
|
|
{
|
|
icon: Trophy,
|
|
title: "Watch & Win",
|
|
body: "Your teams earn points for how well they perform in the playoffs. Follow your rooting interests along the way, and gloat over your leaguemates when they fall.",
|
|
},
|
|
];
|
|
|
|
const FEATURES = [
|
|
{
|
|
title: "Dozens of Sports and Counting",
|
|
body: "Draft from a massive selection of sports including major US leagues, college athletics, and global niches like professional darts or snooker. We are constantly adding new sports to ensure your league has competitive action throughout the entire year.",
|
|
},
|
|
{
|
|
title: "The Smart Draft Room",
|
|
body: "Our interface supports any pace. Use a tactical chess clock with Fischer increments to reward fast decisions, or run a multi-day slow draft with customizable overnight pauses. The system ensures no one is forced to make a pick in the middle of the night.",
|
|
},
|
|
{
|
|
title: "Your Master Calendar",
|
|
body: "Stop switching between different apps to track your roster. Every game, match, and tournament for your specific teams is synced into a single view. You will always know exactly when your points are on the line without any manual tracking.",
|
|
},
|
|
{
|
|
title: "Zero Maintenance",
|
|
body: "Brackt is a draft and done experience. Once the draft is finished, your work is over because we handle all scoring and rankings automatically. You can even paste a Discord webhook URL to get live scoring updates sent directly to your group chat.",
|
|
},
|
|
];
|
|
|
|
// ─── Main Landing Page ────────────────────────────────────────────────────────
|
|
|
|
export function LandingPage() {
|
|
// Interleave step cards with gradient triangle separators
|
|
const howItWorksItems: React.ReactNode[] = [];
|
|
HOW_IT_WORKS_STEPS.forEach((step, i) => {
|
|
howItWorksItems.push(
|
|
<Card key={step.title} className="flex-1 gap-2">
|
|
<SectionCardHeader icon={step.icon} title={step.title} />
|
|
<CardContent className="px-3 sm:px-6">
|
|
<p className="text-sm text-muted-foreground">{step.body}</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
if (i < HOW_IT_WORKS_STEPS.length - 1) {
|
|
// Triangles reference #brackt-primary-gradient defined in <BracktGradients /> (mounted in root.tsx)
|
|
howItWorksItems.push(
|
|
<div key={`arrow-${step.title}`} className="flex shrink-0 items-center justify-center py-1 md:px-1 md:py-0">
|
|
<svg width="20" height="20" viewBox="0 0 20 20" className="rotate-90 md:rotate-0">
|
|
<polygon
|
|
points="3,2 18,10 3,18"
|
|
fill="url(#brackt-primary-gradient)"
|
|
stroke="url(#brackt-primary-gradient)"
|
|
strokeWidth={3}
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<div className="animate-page-fade-in">
|
|
{/* ── Hero ── */}
|
|
<section
|
|
className="relative flex flex-col items-center justify-center overflow-hidden px-8"
|
|
style={{ minHeight: "calc(100vh - var(--navbar-height))" }}
|
|
>
|
|
{/* 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 />
|
|
</div>
|
|
|
|
<ScrollHint />
|
|
|
|
{/* Center content */}
|
|
<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 }}
|
|
>
|
|
<span className="block text-white">Fantasy</span>
|
|
<SlotMachineHeadline />
|
|
</h1>
|
|
|
|
<p
|
|
className="mx-auto mt-8 text-muted-foreground"
|
|
style={{ fontSize: "16px", fontWeight: 400, lineHeight: 1.65, maxWidth: "414px" }}
|
|
>
|
|
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">
|
|
<Button asChild size="lg" className="text-sm font-extrabold">
|
|
<Link to="/leagues/new">Create a League</Link>
|
|
</Button>
|
|
<Button asChild variant="outline" size="lg" className="text-sm font-semibold">
|
|
<Link to="/how-to-play">How it works</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
<SportTicker />
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── How It Works ── */}
|
|
<section className="mx-auto w-full max-w-[1100px] px-10 py-18">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-stretch">
|
|
{howItWorksItems}
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Features Grid ── */}
|
|
<section className="mx-auto w-full max-w-[1100px] px-10 pb-18">
|
|
<div className="overflow-hidden rounded-[6px] border">
|
|
<div className="grid grid-cols-1 md:grid-cols-2" style={{ gap: "1px", background: "rgb(255 255 255 / 10%)" }}>
|
|
{FEATURES.map(({ title, body }) => (
|
|
<div key={title} className="bg-card p-7 transition-colors hover:bg-[#22252e]">
|
|
<h3 className="mb-2 text-base font-extrabold tracking-[-0.01em]">{title}</h3>
|
|
<p className="text-sm leading-[1.65] text-muted-foreground">{body}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Footer CTA ── */}
|
|
<section className="mx-auto w-full max-w-[1100px] px-10 pb-20">
|
|
<div className="flex flex-col items-center gap-6 p-12 text-center">
|
|
<h2
|
|
className="font-black tracking-[-0.025em]"
|
|
style={{ fontSize: "clamp(22px, 3vw, 34px)" }}
|
|
>
|
|
Stop managing your league and start watching it.
|
|
</h2>
|
|
<p className="text-sm leading-[1.65] text-muted-foreground" style={{ maxWidth: "600px" }}>
|
|
Brackt is built for the sports fan who wants a stake in everything without the daily chore of a traditional fantasy roster. Setting up a league takes less than five minutes. Choose your sports, invite your friends, and let the draft decide the winner.
|
|
</p>
|
|
<Button asChild size="lg" className="text-sm font-extrabold">
|
|
<Link to="/leagues/new">Create a League</Link>
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|