teehl-2 / prompts.txt
KrustyTrashPanda's picture
import React, { useMemo, useState } from "react";
c4b9bb2 verified
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>
);
}