homepage copy
This commit is contained in:
parent
8fadc27c75
commit
654edbf271
1 changed files with 36 additions and 13 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { Users, Shuffle, Trophy } from "lucide-react";
|
import { Users, Shuffle, Trophy, ChevronDown } from "lucide-react";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent } from "~/components/ui/card";
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
import { SectionCardHeader } from "~/components/ui/SectionCardHeader";
|
import { SectionCardHeader } from "~/components/ui/SectionCardHeader";
|
||||||
|
|
@ -21,15 +21,17 @@ function shuffle<T>(arr: T[]): T[] {
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOTAL_TICKS = 36;
|
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 {
|
function getInterval(tick: number): number {
|
||||||
const pct = tick / TOTAL_TICKS;
|
const pct = Math.min((tick / TOTAL_TICKS) * 1.5, 1);
|
||||||
return Math.round(130 - (130 - 38) * (pct * pct));
|
return Math.round(800 - (800 - 340) * (pct * pct));
|
||||||
}
|
}
|
||||||
|
|
||||||
function SlotMachineHeadline() {
|
function SlotMachineHeadline() {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("\u00A0");
|
||||||
const [landed, setLanded] = useState(false);
|
const [landed, setLanded] = useState(false);
|
||||||
const [animKey, setAnimKey] = useState(0);
|
const [animKey, setAnimKey] = useState(0);
|
||||||
const tickRef = useRef(0);
|
const tickRef = useRef(0);
|
||||||
|
|
@ -54,7 +56,7 @@ function SlotMachineHeadline() {
|
||||||
timerRef.current = setTimeout(tick, getInterval(tickRef.current));
|
timerRef.current = setTimeout(tick, getInterval(tickRef.current));
|
||||||
}
|
}
|
||||||
|
|
||||||
timerRef.current = setTimeout(tick, 130);
|
timerRef.current = setTimeout(tick, 800);
|
||||||
return () => {
|
return () => {
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
};
|
};
|
||||||
|
|
@ -207,6 +209,25 @@ function BracketDecor({ flip = false }: { flip?: boolean }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 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 ─────────────────────────────────────────────────────────────
|
// ─── Sport Ticker ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const TICKER_ITEMS = [
|
const TICKER_ITEMS = [
|
||||||
|
|
@ -245,17 +266,17 @@ const HOW_IT_WORKS_STEPS = [
|
||||||
{
|
{
|
||||||
icon: Users,
|
icon: Users,
|
||||||
title: "Create a League",
|
title: "Create a League",
|
||||||
body: "Name it, pick your sports, invite your friends with a link. Takes 30 seconds.",
|
body: "Start a league, pick your sports (we can help), and invite your friends. It takes 30 seconds to set the stage.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Shuffle,
|
icon: Shuffle,
|
||||||
title: "Draft Your Teams",
|
title: "Draft Your Teams",
|
||||||
body: "Snake draft — everyone picks teams from any sport on the calendar. NHL, NBA, World Cup, whatever is running.",
|
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,
|
icon: Trophy,
|
||||||
title: "Watch & Win",
|
title: "Watch & Win",
|
||||||
body: "Your teams earn points as they win real games. The bracket updates live. Last one standing takes it.",
|
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.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -301,8 +322,8 @@ export function LandingPage() {
|
||||||
if (i < HOW_IT_WORKS_STEPS.length - 1) {
|
if (i < HOW_IT_WORKS_STEPS.length - 1) {
|
||||||
// Triangles reference #brackt-primary-gradient defined in <BracktGradients /> (mounted in root.tsx)
|
// Triangles reference #brackt-primary-gradient defined in <BracktGradients /> (mounted in root.tsx)
|
||||||
howItWorksItems.push(
|
howItWorksItems.push(
|
||||||
<div key={`arrow-${step.title}`} className="hidden shrink-0 md:flex md:items-center md:justify-center md:px-1">
|
<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">
|
<svg width="20" height="20" viewBox="0 0 20 20" className="rotate-90 md:rotate-0">
|
||||||
<polygon
|
<polygon
|
||||||
points="3,2 18,10 3,18"
|
points="3,2 18,10 3,18"
|
||||||
fill="url(#brackt-primary-gradient)"
|
fill="url(#brackt-primary-gradient)"
|
||||||
|
|
@ -329,11 +350,13 @@ export function LandingPage() {
|
||||||
<BracketDecor flip />
|
<BracketDecor flip />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ScrollHint />
|
||||||
|
|
||||||
{/* Center content */}
|
{/* Center content */}
|
||||||
<div className="relative z-10 w-full max-w-[680px] text-center">
|
<div className="relative z-10 w-full max-w-[680px] text-center">
|
||||||
<h1
|
<h1
|
||||||
className="mb-0 leading-[0.95] tracking-[-0.04em]"
|
className="mb-0 leading-[0.95] tracking-[-0.04em]"
|
||||||
style={{ fontSize: "clamp(52px, 8vw, 88px)", fontWeight: 900 }}
|
style={{ fontSize: "clamp(38px, 8vw, 88px)", fontWeight: 900 }}
|
||||||
>
|
>
|
||||||
<span className="block text-white">Fantasy</span>
|
<span className="block text-white">Fantasy</span>
|
||||||
<SlotMachineHeadline />
|
<SlotMachineHeadline />
|
||||||
|
|
@ -347,7 +370,7 @@ export function LandingPage() {
|
||||||
Compete with your friends in a league all season long.
|
Compete with your friends in a league all season long.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center justify-center gap-2.5">
|
<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">
|
<Button asChild size="lg" className="text-sm font-extrabold">
|
||||||
<Link to="/leagues/new">Create a League</Link>
|
<Link to="/leagues/new">Create a League</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue