Spaces:
Running
Running
| import React, { useMemo, useState } from "react"; | |
| // TEEHL Sportsbook — React app (TailwindCSS) | |
| // FanDuel palette + team logos everywhere + Teams page with bios | |
| // ===================== | |
| // FanDuel Colors | |
| // ===================== | |
| const COLORS = { | |
| primary: "#1BB152", | |
| secondary: "#1493FF", | |
| navy: "#1F375B", | |
| bg: "#F0F3F8", | |
| border: "#CFD6DB", | |
| muted: "#818E95", | |
| }; | |
| // ===================== | |
| // League Data (with Logos & Bios) | |
| // Place logo files under /public/ (root) | |
| // ===================== | |
| const TEAMS = { | |
| SB: { | |
| name: "Screaming Beavers", | |
| color: "#1493FF", | |
| logo: "/$1", | |
| founded: 2012, | |
| arena: "East End Arena — Rink C", | |
| coach: "Casey Morgan", | |
| captain: "B. Laird", | |
| colors: ["#E11D48", "#111827", "#FFFFFF"], // red/black/white kit | |
| bio: | |
| "Blue-collar hustle and forecheck monsters. The Beavers live for greasy goals and third-period comeback wins.", | |
| }, | |
| GD: { | |
| name: "Green Dragons", | |
| color: "#1BB152", | |
| logo: "/$1", | |
| founded: 2015, | |
| arena: "East End Arena — Rink A", | |
| coach: "Alyssa Park", | |
| captain: "R. Chen", | |
| colors: ["#1BB152", "#0F172A", "#F8FAFC"], | |
| bio: | |
| "Up-tempo transition and lethal on special teams. When the rush is flying, the Dragons torch opponents in bunches.", | |
| }, | |
| BD: { | |
| name: "Bulldogs", | |
| color: "#1F375B", | |
| logo: "/$1", | |
| founded: 2010, | |
| arena: "East End Arena — Rink B", | |
| coach: "D. Simone", | |
| captain: "M. O'Rourke", | |
| colors: ["#1F375B", "#C2410C", "#B91C1C"], | |
| bio: | |
| "Heavy forecheck, heavier hits. Bulldogs grind teams down low and feast on second chances in the slot.", | |
| }, | |
| FC: { | |
| name: "The French Connection", | |
| color: "#CFD6DB", | |
| logo: "/$1", | |
| founded: 2018, | |
| arena: "East End Arena — Community Pad", | |
| coach: "Luc Tremblay", | |
| captain: "P. Charron", | |
| colors: ["#1D4ED8", "#F43F5E", "#F5F5F5"], | |
| bio: | |
| "Silky hands and flair for the dramatic. FC loves the extra pass and live for OT heroics and shootouts.", | |
| }, | |
| }; | |
| const initialStandings = [ | |
| { team: "SB", gp: 12, w: 8, l: 3, ot: 1, gf: 46, ga: 33 }, | |
| { team: "GD", gp: 12, w: 7, l: 4, ot: 1, gf: 44, ga: 36 }, | |
| { team: "BD", gp: 12, w: 5, l: 7, ot: 0, gf: 40, ga: 45 }, | |
| { team: "FC", gp: 12, w: 3, l: 8, ot: 1, gf: 31, ga: 47 }, | |
| ]; | |
| const CUP_ODDS = [ | |
| { team: "SB", american: "+175" }, | |
| { team: "GD", american: "+225" }, | |
| { team: "BD", american: "+450" }, | |
| { team: "FC", american: "+700" }, | |
| ]; | |
| const UPCOMING_GAMES = [ | |
| { | |
| id: "g1", | |
| date: "2025-09-18", | |
| time: "19:30", | |
| venue: "East End Arena Rink A", | |
| home: "GD", | |
| away: "SB", | |
| markets: { | |
| moneyline: { SB: "+105", GD: "-120" }, | |
| spread: { line: -0.5, GD: "+135", SB: "-155" }, | |
| total: { line: 5.5, over: "-110", under: "-110" }, | |
| }, | |
| }, | |
| { | |
| id: "g2", | |
| date: "2025-09-18", | |
| time: "21:00", | |
| venue: "East End Arena Rink B", | |
| home: "BD", | |
| away: "FC", | |
| markets: { | |
| moneyline: { FC: "+140", BD: "-165" }, | |
| spread: { line: -1.5, BD: "+170", FC: "-190" }, | |
| total: { line: 6.0, over: "-105", under: "-115" }, | |
| }, | |
| }, | |
| ]; | |
| const RESULTS = [ | |
| { date: "2025-09-11", home: "SB", away: "BD", score: "4–2" }, | |
| { date: "2025-09-11", home: "FC", away: "GD", score: "3–5" }, | |
| { date: "2025-09-04", home: "GD", away: "BD", score: "2–1 (SO)" }, | |
| { date: "2025-09-04", home: "SB", away: "FC", score: "6–3" }, | |
| ]; | |
| // ===================== | |
| // Odds Utilities | |
| // ===================== | |
| function americanToDecimal(aStr) { | |
| const n = parseInt(String(aStr).replace(/[^-+0-9]/g, ""), 10); | |
| if (Number.isNaN(n) || n === 0) return 1.0; | |
| return n > 0 ? 1 + n / 100 : 1 + 100 / Math.abs(n); | |
| } | |
| function impliedProbability(american) { | |
| const n = parseInt(String(american).replace(/[^-+0-9]/g, ""), 10); | |
| if (Number.isNaN(n) || n === 0) return 0; | |
| return n > 0 ? 100 / (n + 100) : Math.abs(n) / (Math.abs(n) + 100); | |
| } | |
| // ===================== | |
| // UI Bits | |
| // ===================== | |
| const Badge = ({ children, className = "" }) => ( | |
| <span | |
| className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${className}`} | |
| style={{ backgroundColor: COLORS.bg, color: COLORS.navy, border: `1px solid ${COLORS.border}` }} | |
| > | |
| {children} | |
| </span> | |
| ); | |
| const Card = ({ children, className = "" }) => ( | |
| <div className={`rounded-2xl bg-white shadow-sm ${className}`} style={{ border: `1px solid ${COLORS.border}` }}> | |
| {children} | |
| </div> | |
| ); | |
| const CardHeader = ({ title, subtitle, right }) => ( | |
| <div className="flex items-start justify-between gap-4 p-4" style={{ borderBottom: `1px solid ${COLORS.border}` }}> | |
| <div> | |
| <h3 className="text-lg font-semibold leading-tight" style={{ color: COLORS.navy }}>{title}</h3> | |
| {subtitle && <p className="text-sm mt-0.5" style={{ color: COLORS.muted }}>{subtitle}</p>} | |
| </div> | |
| {right} | |
| </div> | |
| ); | |
| const TeamPill = ({ code, size = 24 }) => ( | |
| <div className="flex items-center gap-2"> | |
| <img | |
| src={TEAMS[code].logo} | |
| alt={TEAMS[code].name} | |
| className="rounded-full ring-2 ring-white object-cover" | |
| style={{ width: size, height: size }} | |
| /> | |
| <span className="text-sm font-medium" style={{ color: COLORS.navy }}>{TEAMS[code].name}</span> | |
| </div> | |
| ); | |
| const OddsButton = ({ label, sublabel, active, onClick }) => ( | |
| <button | |
| onClick={onClick} | |
| className="w-full rounded-xl border px-3 py-2 text-left transition hover:shadow-sm active:scale-[0.99]" | |
| style={{ | |
| borderColor: active ? COLORS.primary : COLORS.secondary, | |
| backgroundColor: active ? "#E6F8ED" : "#FFFFFF", | |
| color: active ? COLORS.primary : COLORS.navy, | |
| }} | |
| > | |
| <div className="flex items-baseline justify-between"> | |
| <span className="font-semibold tracking-tight">{label}</span> | |
| {sublabel && <span className="text-xs" style={{ color: COLORS.muted }}>{sublabel}</span>} | |
| </div> | |
| </button> | |
| ); | |
| const NavTabs = ({ page, setPage }) => ( | |
| <div className="mx-auto max-w-7xl px-4 mt-4"> | |
| <div className="inline-flex gap-2 rounded-xl p-1 bg-white" style={{ border: `1px solid ${COLORS.border}` }}> | |
| {[ | |
| { key: "Games", label: "Games" }, | |
| { key: "Teams", label: "Teams" }, | |
| ].map((t) => ( | |
| <button | |
| key={t.key} | |
| onClick={() => setPage(t.key)} | |
| className="px-3 py-1.5 rounded-lg text-sm font-medium" | |
| style={{ | |
| backgroundColor: page === t.key ? COLORS.primary : "transparent", | |
| color: page === t.key ? "#FFFFFF" : COLORS.navy, | |
| border: `1px solid ${page === t.key ? COLORS.primary : "transparent"}`, | |
| }} | |
| > | |
| {t.label} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| // ===================== | |
| // Bet Slip logic | |
| // ===================== | |
| function useBetSlip() { | |
| const [legs, setLegs] = useState([]); | |
| const addLeg = (leg) => { | |
| const key = `${leg.id}:${leg.type}:${leg.label}`; | |
| setLegs((prev) => (prev.some((l) => `${l.id}:${l.type}:${l.label}` === key) ? prev : [...prev, leg])); | |
| }; | |
| const removeLeg = (idx) => setLegs((prev) => prev.filter((_, i) => i !== idx)); | |
| const clear = () => setLegs([]); | |
| const decimalOdds = useMemo(() => { | |
| if (!legs.length) return 0; | |
| return legs.reduce((acc, l) => acc * americanToDecimal(l.american), 1); | |
| }, [legs]); | |
| const [stake, setStake] = useState(20); | |
| const estReturn = stake * decimalOdds; | |
| const estProfit = Math.max(0, estReturn - stake); | |
| return { legs, addLeg, removeLeg, clear, stake, setStake, decimalOdds, estReturn, estProfit }; | |
| } | |
| // ===================== | |
| // Main App | |
| // ===================== | |
| export default function App() { | |
| const slip = useBetSlip(); | |
| const [format, setFormat] = useState("American"); | |
| const [page, setPage] = useState("Games"); | |
| return ( | |
| <div className="min-h-screen" style={{ backgroundColor: COLORS.bg, color: COLORS.navy }}> | |
| <header className="sticky top-0 z-40 backdrop-blur bg-white/90" style={{ borderBottom: `1px solid ${COLORS.border}` }}> | |
| <div className="mx-auto max-w-7xl px-4 py-4 flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="h-9 w-9 rounded-xl grid place-items-center text-white font-black" style={{ backgroundColor: COLORS.primary }}> | |
| T | |
| </div> | |
| <div> | |
| <h1 className="text-xl font-bold tracking-tight" style={{ color: COLORS.navy }}>TEEHL Sportsbook</h1> | |
| <p className="text-xs -mt-0.5" style={{ color: COLORS.muted }}>The East End Hockey League — Men’s Division</p> | |
| </div> | |
| <Badge className="ml-3">Beta</Badge> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <label className="text-sm" style={{ color: COLORS.muted }}>Odds format</label> | |
| <select | |
| className="rounded-lg border bg-white px-2.5 py-1.5 text-sm" | |
| style={{ borderColor: COLORS.border, color: COLORS.navy }} | |
| value={format} | |
| onChange={(e) => setFormat(e.target.value)} | |
| > | |
| <option>American</option> | |
| <option>Decimal</option> | |
| </select> | |
| </div> | |
| </div> | |
| </header> | |
| <NavTabs page={page} setPage={setPage} /> | |
| {/* Pages */} | |
| {page === "Games" ? ( | |
| <GamesPage format={format} addLeg={slip.addLeg} slip={slip} /> | |
| ) : ( | |
| <TeamsPage /> | |
| )} | |
| <footer className="mx-auto max-w-7xl p-6 text-center text-xs" style={{ color: COLORS.muted }}> | |
| © {new Date().getFullYear()} TEEHL Sportsbook — Demo UI. All odds are fictional. | |
| </footer> | |
| </div> | |
| ); | |
| } | |
| function GamesPage({ format, addLeg, slip }) { | |
| return ( | |
| <main className="mx-auto max-w-7xl p-4 grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| {/* Left Column */} | |
| <div className="lg:col-span-2 space-y-6"> | |
| <LeagueTicker /> | |
| <Card> | |
| <CardHeader | |
| title="Tonight’s Games" | |
| subtitle="Moneyline • Puck Line • Total Goals" | |
| right={<Badge>{new Date().toLocaleDateString()}</Badge>} | |
| /> | |
| <div className="divide-y" style={{ borderColor: COLORS.border }}> | |
| {UPCOMING_GAMES.map((g) => ( | |
| <GameRow key={g.id} game={g} format={format} onSelectLeg={addLeg} /> | |
| ))} | |
| </div> | |
| </Card> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <Card> | |
| <CardHeader title="Standings" subtitle="W–L–OT, goal differential" /> | |
| <StandingsTable /> | |
| </Card> | |
| <Card> | |
| <CardHeader title="TEEHL Cup Winner" subtitle="Futures market" /> | |
| <FuturesTable format={format} onSelectLeg={addLeg} /> | |
| </Card> | |
| </div> | |
| <Card> | |
| <CardHeader title="Recent Results" subtitle="Last 2 weeks" /> | |
| <ResultsList /> | |
| </Card> | |
| </div> | |
| {/* Right Column */} | |
| <div className="lg:col-span-1"> | |
| <BetSlipPanel slip={slip} /> | |
| <SafetyNotice /> | |
| </div> | |
| </main> | |
| ); | |
| } | |
| // ===================== | |
| // Sections | |
| // ===================== | |
| function LeagueTicker() { | |
| const teams = Object.entries(TEAMS); | |
| return ( | |
| <Card> | |
| <div className="p-4 flex flex-wrap items-center gap-4 md:gap-6"> | |
| {teams.map(([code, t]) => ( | |
| <div key={code} className="flex items-center gap-2"> | |
| <img src={t.logo} alt={t.name} className="h-6 w-6 rounded-full object-cover" /> | |
| <span className="text-sm font-medium" style={{ color: COLORS.navy }}>{t.name}</span> | |
| </div> | |
| ))} | |
| <div className="ml-auto text-xs" style={{ color: COLORS.muted }}>Venue: East End Arena • Toronto, ON</div> | |
| </div> | |
| </Card> | |
| ); | |
| } | |
| function GameRow({ game, format, onSelectLeg }) { | |
| const { home, away, markets } = game; | |
| const fmt = (american) => (format === "Decimal" ? americanToDecimal(american).toFixed(2) : american); | |
| return ( | |
| <div className="p-4"> | |
| <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-3"> | |
| <TeamPill code={away} size={28} /> | |
| <span style={{ color: COLORS.muted }}>@</span> | |
| <TeamPill code={home} size={28} /> | |
| </div> | |
| <div className="text-xs" style={{ color: COLORS.muted }}>{game.date} • {game.time} • {game.venue}</div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Badge>ML</Badge> | |
| <Badge>PL</Badge> | |
| <Badge>Total</Badge> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3 mt-4"> | |
| {/* Moneyline */} | |
| <OddsButton | |
| label={`${TEAMS[away].name} ${fmt(markets.moneyline[away])}`} | |
| sublabel="Moneyline" | |
| onClick={() => onSelectLeg({ id: game.id, type: "ML", label: `${TEAMS[away].name} ML`, american: markets.moneyline[away] })} | |
| /> | |
| <OddsButton | |
| label={`${TEAMS[home].name} ${fmt(markets.moneyline[home])}`} | |
| sublabel="Moneyline" | |
| onClick={() => onSelectLeg({ id: game.id, type: "ML", label: `${TEAMS[home].name} ML`, american: markets.moneyline[home] })} | |
| /> | |
| {/* Spread / Puck Line */} | |
| <OddsButton | |
| label={`${TEAMS[home].name} ${markets.spread.line > 0 ? "+" : ""}${markets.spread.line} (${fmt(markets.spread[home])})`} | |
| sublabel="Puck Line" | |
| onClick={() => onSelectLeg({ id: game.id, type: "PL", label: `${TEAMS[home].name} ${markets.spread.line}`, american: markets.spread[home] })} | |
| /> | |
| <OddsButton | |
| label={`${TEAMS[away].name} ${markets.spread.line > 0 ? "-" : "+"}${Math.abs(markets.spread.line)} (${fmt(markets.spread[away])})`} | |
| sublabel="Puck Line" | |
| onClick={() => onSelectLeg({ id: game.id, type: "PL", label: `${TEAMS[away].name} ${-markets.spread.line}`, american: markets.spread[away] })} | |
| /> | |
| {/* Total */} | |
| <OddsButton | |
| label={`Over ${markets.total.line} (${fmt(markets.total.over)})`} | |
| sublabel="Total Goals" | |
| onClick={() => onSelectLeg({ id: game.id, type: "TOT", label: `Over ${markets.total.line}`, american: markets.total.over })} | |
| /> | |
| <OddsButton | |
| label={`Under ${markets.total.line} (${fmt(markets.total.under)})`} | |
| sublabel="Total Goals" | |
| onClick={() => onSelectLeg({ id: game.id, type: "TOT", label: `Under ${markets.total.line}`, american: markets.total.under })} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function StandingsTable() { | |
| const rows = initialStandings | |
| .map((s) => ({ ...s, pts: s.w * 2 + s.ot, diff: s.gf - s.ga })) | |
| .sort((a, b) => b.pts - a.pts || b.diff - a.diff); | |
| return ( | |
| <div className="p-4 overflow-x-auto"> | |
| <table className="min-w-full text-sm"> | |
| <thead> | |
| <tr className="text-left" style={{ color: COLORS.muted, borderBottom: `1px solid ${COLORS.border}` }}> | |
| <th className="py-2 pr-4">Team</th> | |
| <th className="py-2 pr-4">GP</th> | |
| <th className="py-2 pr-4">W</th> | |
| <th className="py-2 pr-4">L</th> | |
| <th className="py-2 pr-4">OT</th> | |
| <th className="py-2 pr-4">GF</th> | |
| <th className="py-2 pr-4">GA</th> | |
| <th className="py-2 pr-4">Diff</th> | |
| <th className="py-2 pr-0">PTS</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {rows.map((r, idx) => ( | |
| <tr key={r.team} style={{ borderBottom: `1px solid ${COLORS.border}` }}> | |
| <td className="py-2 pr-4"> | |
| <div className="flex items-center gap-2"> | |
| <img src={TEAMS[r.team].logo} alt={TEAMS[r.team].name} className="h-6 w-6 rounded-full object-cover" /> | |
| <span className="font-medium" style={{ color: COLORS.navy }}>{TEAMS[r.team].name}</span> | |
| {idx === 0 && <Badge className="ml-2">1st</Badge>} | |
| </div> | |
| </td> | |
| <td className="py-2 pr-4">{r.gp}</td> | |
| <td className="py-2 pr-4">{r.w}</td> | |
| <td className="py-2 pr-4">{r.l}</td> | |
| <td className="py-2 pr-4">{r.ot}</td> | |
| <td className="py-2 pr-4">{r.gf}</td> | |
| <td className="py-2 pr-4">{r.ga}</td> | |
| <td className="py-2 pr-4" style={{ color: r.diff >= 0 ? COLORS.primary : "#E11D48" }}>{r.diff}</td> | |
| <td className="py-2 pr-0 font-semibold">{r.pts}</td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| ); | |
| } | |
| function FuturesTable({ format, onSelectLeg }) { | |
| const rows = CUP_ODDS.map((o) => ({ team: o.team, american: o.american, dec: americanToDecimal(o.american), imp: impliedProbability(o.american) })).sort((a, b) => a.imp - b.imp); | |
| const fmt = (american, dec) => (format === "Decimal" ? dec.toFixed(2) : american); | |
| return ( | |
| <div className="p-4"> | |
| <div className="grid grid-cols-1 gap-3"> | |
| {rows.map((r) => ( | |
| <div key={r.team} className="flex items-center justify-between gap-3"> | |
| <TeamPill code={r.team} size={20} /> | |
| <div className="flex items-center gap-3"> | |
| <div className="text-xs w-20 text-right" style={{ color: COLORS.muted }}>{(r.imp * 100).toFixed(1)}%</div> | |
| <OddsButton | |
| label={fmt(r.american, r.dec)} | |
| sublabel="To lift the Cup" | |
| onClick={() => onSelectLeg({ id: `FUT:${r.team}`, type: "FUT", label: `${TEAMS[r.team].name} – Cup`, american: r.american })} | |
| /> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ResultsList() { | |
| return ( | |
| <div className="p-4 space-y-3"> | |
| {RESULTS.map((r, i) => ( | |
| <div key={i} className="flex items-center justify-between"> | |
| <div className="text-sm w-28" style={{ color: COLORS.muted }}>{r.date}</div> | |
| <div className="flex-1"> | |
| <div className="flex items-center gap-3"> | |
| <div className="flex items-center gap-2"> | |
| <img src={TEAMS[r.away].logo} alt={TEAMS[r.away].name} className="h-5 w-5 rounded-full object-cover" /> | |
| <span className="font-medium" style={{ color: COLORS.navy }}>{TEAMS[r.away].name}</span> | |
| </div> | |
| <span style={{ color: COLORS.muted }}>@</span> | |
| <div className="flex items-center gap-2"> | |
| <img src={TEAMS[r.home].logo} alt={TEAMS[r.home].name} className="h-5 w-5 rounded-full object-cover" /> | |
| <span className="font-medium" style={{ color: COLORS.navy }}>{TEAMS[r.home].name}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="text-sm font-semibold tabular-nums" style={{ color: COLORS.navy }}>{r.score}</div> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function TeamsPage() { | |
| const entries = Object.entries(TEAMS); | |
| return ( | |
| <main className="mx-auto max-w-7xl p-4"> | |
| <Card> | |
| <CardHeader title="Teams" subtitle="Logos, bios, and team details" /> | |
| <div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {entries.map(([code, t]) => ( | |
| <div key={code} className="rounded-xl p-4 bg-white" style={{ border: `1px solid ${COLORS.border}` }}> | |
| <div className="flex items-center gap-3"> | |
| <img src={t.logo} alt={t.name} className="h-14 w-14 rounded-xl object-cover" /> | |
| <div> | |
| <div className="text-lg font-semibold" style={{ color: COLORS.navy }}>{t.name}</div> | |
| <div className="text-xs" style={{ color: COLORS.muted }}>Founded {t.founded} • {t.arena}</div> | |
| </div> | |
| </div> | |
| <p className="mt-3 text-sm" style={{ color: COLORS.navy }}>{t.bio}</p> | |
| <div className="mt-3 grid grid-cols-2 gap-3 text-sm"> | |
| <div><span className="text-xs" style={{ color: COLORS.muted }}>Coach:</span><br />{t.coach}</div> | |
| <div><span className="text-xs" style={{ color: COLORS.muted }}>Captain:</span><br />{t.captain}</div> | |
| <div className="col-span-2"> | |
| <span className="text-xs" style={{ color: COLORS.muted }}>Team Colors</span> | |
| <div className="mt-1 flex items-center gap-2"> | |
| {t.colors.map((c, idx) => ( | |
| <div key={idx} className="h-5 w-5 rounded-full border" style={{ backgroundColor: c, borderColor: COLORS.border }} title={c} /> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="mt-4 flex flex-wrap gap-2"> | |
| <span className="text-xs" style={{ color: COLORS.muted }}>Recent Form:</span> | |
| {/* Placeholder pips */} | |
| {Array.from({ length: 5 }).map((_, i) => ( | |
| <span key={i} className="px-2 py-0.5 rounded-full text-xs" style={{ backgroundColor: i % 2 === 0 ? "#E6F8ED" : "#FEE2E2", color: i % 2 === 0 ? COLORS.primary : "#B91C1C", border: `1px solid ${COLORS.border}` }}> | |
| {i % 2 === 0 ? "W" : "L"} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </Card> | |
| </main> | |
| ); | |
| } | |
| function BetSlipPanel({ slip }) { | |
| return ( | |
| <Card className="sticky top-20"> | |
| <CardHeader | |
| title="Bet Slip" | |
| subtitle={slip.legs.length ? `${slip.legs.length} selection${slip.legs.length > 1 ? "s" : ""}` : "No selections yet"} | |
| right={slip.legs.length ? ( | |
| <button onClick={slip.clear} className="text-xs hover:underline" style={{ color: "#E11D48" }}>Clear</button> | |
| ) : null} | |
| /> | |
| <div className="p-4 space-y-3"> | |
| {slip.legs.length === 0 && ( | |
| <div className="text-sm" style={{ color: COLORS.muted }}>Tap odds to add picks. Parlays multiply returns.</div> | |
| )} | |
| {slip.legs.map((l, idx) => ( | |
| <div key={`${l.id}-${idx}`} className="flex items-start justify-between gap-3"> | |
| <div> | |
| <div className="text-sm font-semibold" style={{ color: COLORS.navy }}>{l.label}</div> | |
| <div className="text-xs" style={{ color: COLORS.muted }}>{l.type} • {l.american}</div> | |
| </div> | |
| <button onClick={() => slip.removeLeg(idx)} className="text-xs" style={{ color: COLORS.muted }}>Remove</button> | |
| </div> | |
| ))} | |
| {slip.legs.length > 0 && ( | |
| <> | |
| <div className="my-2" style={{ height: 1, backgroundColor: COLORS.border }} /> | |
| <div className="flex items-center justify-between text-sm"> | |
| <span>Parlay Odds (Decimal)</span> | |
| <span className="font-semibold tabular-nums">{slip.decimalOdds.toFixed(2)}</span> | |
| </div> | |
| <div className="flex items-center justify-between text-sm"> | |
| <span>Stake ($)</span> | |
| <input | |
| type="number" | |
| min={1} | |
| step={1} | |
| value={slip.stake} | |
| onChange={(e) => slip.setStake(Number(e.target.value))} | |
| className="w-28 rounded-lg px-2 py-1 text-right border" | |
| style={{ borderColor: COLORS.border, color: COLORS.navy }} | |
| /> | |
| </div> | |
| <div className="flex items-center justify-between text-sm"> | |
| <span>Estimated Return</span> | |
| <span className="font-semibold tabular-nums">${slip.estReturn.toFixed(2)}</span> | |
| </div> | |
| <div className="flex items-center justify-between text-sm"> | |
| <span>Estimated Profit</span> | |
| <span className="font-semibold tabular-nums">${slip.estProfit.toFixed(2)}</span> | |
| </div> | |
| <button className="mt-3 w-full rounded-xl py-2 font-semibold disabled:opacity-50 text-white" style={{ backgroundColor: COLORS.primary }} disabled> | |
| Place Bet (Demo) | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| </Card> | |
| ); | |
| } | |
| function SafetyNotice() { | |
| return ( | |
| <Card className="mt-6"> | |
| <div className="p-4 text-xs" style={{ color: COLORS.muted }}> | |
| <div className="font-semibold" style={{ color: COLORS.navy }}>Fair Play & Responsible Betting</div> | |
| <p className="mt-1">This is a fictional sportsbook UI for a local men’s league. Odds and markets are for demo purposes only and do not represent real wagering.</p> | |
| <p className="mt-1">If you adapt this for real-money use, comply with all regional regulations, age verification, data privacy, and integrity rules.</p> | |
| </div> | |
| </Card> | |
| ); | |
| } | |