// bets.jsx
// Complete module: normalize picks from API, odds helpers, and BetRow component
// React is loaded globally via a CDN script in index.html.
// ================== FORMAT HELPERS ==================
function dollars(v) {
if (v == null || Number.isNaN(Number(v))) return "$0.00";
return `$${Number(v).toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}
// ================== ODDS HELPERS ==================
function americanToDecimal(american) {
const x = Number(american);
if (!Number.isFinite(x)) return null;
if (x > 0) return 1 + x / 100;
if (x < 0) return 1 + 100 / Math.abs(x);
return 2.0;
}
function potentialParlayWin(stake, legsAmericanOdds) {
if (typeof calculateParlayProfit === "function") {
return calculateParlayProfit(stake, legsAmericanOdds);
}
const s = Number(stake);
if (!Number.isFinite(s) || s <= 0) return 0;
if (!Array.isArray(legsAmericanOdds) || legsAmericanOdds.length === 0) return 0;
let dec = 1.0;
for (const o of legsAmericanOdds) {
const d = americanToDecimal(o);
if (d == null) return null;
dec *= d;
}
return s * (dec - 1.0);
}
// ================== NORMALIZER ==================
function normalizePicks(apiPicks, { includeHandle = false } = {}) {
return (apiPicks || []).map((p) => {
const isParlay = Array.isArray(p.legs) && p.legs.length > 0;
const rawMarket = String(p.market || "").trim().toLowerCase();
const line = p.line ?? p.spread ?? p.spread_line ?? p.point ?? null;
const total = p.total ?? p.total_points ?? p.totalPoints ?? null;
const inferredMarket =
rawMarket ||
(line != null
? "spread"
: total != null
? "total"
: p.odds != null
? "moneyline"
: "");
return {
id: p.id,
type: isParlay ? "parlay" : "single",
handle: includeHandle ? p.handle : undefined,
stake: Number(p.stake ?? 0),
status: (p.status || "open").toLowerCase(),
start: p.start || p.kickoff || p.start_time || p.starts_at || p.game_time,
createdAt: new Date(p.created_at || p.createdAt || Date.now()),
// Single fields
league: isParlay ? undefined : p.league,
event: isParlay ? undefined : p.event,
selection: isParlay ? undefined : p.selection,
odds: isParlay ? undefined : p.odds,
market: isParlay ? undefined : inferredMarket,
line: isParlay ? undefined : line,
total: isParlay ? undefined : total,
// Parlay legs
legs: isParlay
? p.legs.map((l, idx) => ({
idx,
league: l.league,
event: l.event,
selection: l.selection,
market: (l.market || "").toLowerCase(),
side: l.side,
odds: l.odds,
line: l.line,
total: l.total,
start: l.start || l.kickoff || l.start_time || l.starts_at || l.game_time,
}))
: null,
};
});
}
async function createParlay({ apiBase, token, stake, legs, withCode }) {
const numericStake = Number(stake);
if (!Number.isFinite(numericStake) || numericStake <= 0) {
throw new Error("Stake must be greater than zero");
}
if (!Array.isArray(legs) || legs.length < 2) {
throw new Error("Parlay requires at least two legs");
}
const sanitizedLegs = legs.map((raw, idx) => {
const league = String(raw.league || "").trim().toUpperCase();
const event = String(raw.event || "").trim().toUpperCase();
const selection = String(raw.selection || "").trim().toUpperCase();
const market = String(raw.market || "").trim().toLowerCase();
if (!league || !event || !selection || !market) {
throw new Error(`Leg ${idx + 1} is missing required fields`);
}
const allowedMarkets = ["moneyline", "spread", "total"];
if (!allowedMarkets.includes(market)) {
throw new Error(`Leg ${idx + 1} has an invalid market`);
}
const sanitized = { league, event, selection, market };
const side = raw.side != null ? String(raw.side).trim() : "";
if (side) sanitized.side = side.toUpperCase();
if (raw.odds != null && String(raw.odds).trim() !== "") {
const odds = Number(raw.odds);
if (!Number.isFinite(odds)) {
throw new Error(`Leg ${idx + 1} has invalid odds`);
}
sanitized.odds = odds;
}
const hasLine = raw.line != null && String(raw.line).trim() !== "";
const hasTotal = raw.total != null && String(raw.total).trim() !== "";
let lineValue = null;
let totalValue = null;
if (hasLine) {
lineValue = Number(raw.line);
if (!Number.isFinite(lineValue)) {
throw new Error(`Leg ${idx + 1} has an invalid line`);
}
}
if (hasTotal) {
totalValue = Number(raw.total);
if (!Number.isFinite(totalValue)) {
throw new Error(`Leg ${idx + 1} has an invalid total`);
}
}
if (market === "spread") {
if (lineValue == null) {
throw new Error(`Leg ${idx + 1} requires a spread line`);
}
sanitized.line = lineValue;
} else if (market === "total") {
if (totalValue == null && lineValue != null) {
totalValue = lineValue;
}
if (totalValue == null) {
throw new Error(`Leg ${idx + 1} requires a total value`);
}
sanitized.total = totalValue;
if (lineValue != null) {
sanitized.line = lineValue;
}
}
return sanitized;
});
const payload = { stake: numericStake, legs: sanitizedLegs };
const urlBase = `${apiBase.replace(/\/$/, "")}/picks/parlay`;
const url = typeof withCode === "function" ? withCode(urlBase) : urlBase;
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(payload),
});
let text = "";
try {
text = await res.text();
} catch {}
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {}
if (!res.ok) {
let message =
(data && typeof data.error === "string" && data.error) ||
(Array.isArray(data?.detail)
? data.detail
.map((d) => (typeof d?.msg === "string" ? d.msg : ""))
.filter(Boolean)
.join("; ")
: null) ||
(text && text.trim()) ||
`HTTP ${res.status}`;
throw new Error(message);
}
if (!data?.ok) throw new Error((data && data.error) || "Failed to create parlay");
return data.id;
}
// ================== LIVE SCORE HELPERS ==================
function normalizeName(s) {
return String(s || "")
.toLowerCase()
.replace(/[^a-z0-9 ]+/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function teamsRoughMatch(a, b) {
const na = normalizeName(a);
const nb = normalizeName(b);
return na === nb || na.includes(nb) || nb.includes(na);
}
function parseTeamsFromEvent(ev) {
const src = String(ev || "").trim();
if (!src.includes("@")) return { away: "", home: "" };
const parts = src.split("@");
if (parts.length < 2) return { away: "", home: "" };
const away = String(parts[0] || "").trim();
let home = String(parts.slice(1).join("@")).trim();
home = home
.replace(/[\u2022,].*$/, "")
.replace(/\s+\d{1,2}\/\d{1,2}(?:\/\d{2,4})?.*$/i, "")
.replace(/\s+\d{4}-\d{2}-\d{2}.*$/i, "")
.replace(/\s+\d{1,2}:\d{2}(?::\d{2})?\s*(?:AM|PM)?(?:\s*[A-Z]{2,4})?.*$/i, "")
.trim();
if (away && home) return { away, home };
return { away: "", home: "" };
}
function teamInitials(name) {
const parts = String(name || "").trim().split(/\s+/).filter(Boolean);
if (!parts.length) return "TM";
if (parts.length === 1) return parts[0].slice(0, 3).toUpperCase();
return parts.slice(0, 3).map((p) => p[0]).join("").toUpperCase();
}
function TeamLabel({ team, league, compact = false }) {
const displayName = (typeof getTeamDisplayName === "function" ? getTeamDisplayName(team, { leagueKey: league }) : String(team || "")) || String(team || "");
const [logoSrc, setLogoSrc] = React.useState(() => {
if (typeof window !== "undefined" && typeof window.getCachedTeamLogo === "function") {
return window.getCachedTeamLogo(league, displayName) || "";
}
return "";
});
React.useEffect(() => {
let cancelled = false;
const cached = typeof window !== "undefined" && typeof window.getCachedTeamLogo === "function"
? window.getCachedTeamLogo(league, displayName)
: "";
if (cached) {
setLogoSrc(cached);
return () => { cancelled = true; };
}
if (typeof window !== "undefined" && typeof window.resolveTeamLogo === "function") {
window.resolveTeamLogo(league, displayName)
.then((resolved) => {
if (!cancelled && resolved) setLogoSrc(String(resolved));
})
.catch(() => {});
}
return () => { cancelled = true; };
}, [league, displayName]);
if (!displayName) return null;
return (
{logoSrc ? (
) : (
{teamInitials(displayName)}
)}
{displayName}
);
}
function MatchupFromEvent({ league, event }) {
const { away, home } = parseTeamsFromEvent(event);
if (!away || !home) return {event};
return (