pokertrip/docs/manila-poker-tracker.jsx
Mikkel Georgsen fef6f5318e Initial project docs — design spec, reference JSX, and GSD kickoff
Personal poker tracker: Go + React + PostgreSQL PWA for tracking
home sessions and poker trips with AI-powered schedule research.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:36:56 +00:00

387 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useMemo } from "react";
const C = {
base: "#1e1e2e", mantle: "#181825", crust: "#11111b",
surface0: "#313244", surface1: "#45475a", surface2: "#585b70",
overlay0: "#6c7086", overlay1: "#7f849c", overlay2: "#9399b2",
subtext0: "#a6adc8", subtext1: "#bac2de", text: "#cdd6f4",
lavender: "#b4befe", blue: "#89b4fa", sapphire: "#74c7ec",
sky: "#89dceb", teal: "#94e2d5", green: "#a6e3a1",
yellow: "#f9e2af", peach: "#fab387", maroon: "#eba0ac",
red: "#f38ba8", mauve: "#cba6f7", pink: "#f5c2e7",
flamingo: "#f2cdcd", rosewater: "#f5e0dc",
};
const PHP_TO_USD = 0.0175;
const PHP_TO_DKK = 0.12;
const VENUES = {
okada: { name: "Okada Manila (PokerStars LIVE)", short: "Okada", color: C.blue, area: "Entertainment City", type: "tournament", note: "Main series venue — Okada Manila Millions" },
metro: { name: "Metro Card Club", short: "Metro CC", color: C.teal, area: "Ortigas Center, Pasig", type: "tournament", note: "35 tables, amazing food nearby, live music area. Early bird promo: +20% chips if registered before 2PM, +10% before end of lvl 1. 5% service charge on prize pool." },
letsin: { name: "Let's All In Poker Club", short: "Let's All In", color: C.peach, area: "Macapagal Blvd, Pasay", type: "both", note: "Daily tournaments ~6PM, ₱250+ buy-in. Cash from 10/20. PAGCOR licensed." },
masters: { name: "Masters Poker Club", short: "Masters", color: C.flamingo, area: "J. Bocobo St, Malate", type: "cash", note: "Cash games from late afternoon into early hours. Malate = great nightlife district. Occasional small tourneys." },
dynasty: { name: "Dynasty Poker Club", short: "Dynasty", color: C.pink, area: "Malate, Manila", type: "cash", note: "Cash games from late afternoon. Same Malate area as Masters." },
solaire: { name: "Solaire Resort & Casino", short: "Solaire", color: C.mauve, area: "Entertainment City", type: "cash", note: "24hr cash room (Poker King Club). Stakes 50/100 to 500/1000+ PHP. High-stakes action. Near Okada." },
cod: { name: "City of Dreams Manila", short: "CoD Manila", color: C.yellow, area: "Entertainment City", type: "cash", note: "APT venue historically. Cash games may run — verify on arrival. Poker room status unclear in 2026." },
newport: { name: "Newport World Resorts", short: "Newport", color: C.sapphire, area: "Pasay (across NAIA T3)", type: "cash", note: "24/7 cash games, 50/100 and 100/200 PHP. Literally across from the airport." },
nineD: { name: "9D Poker Club", short: "9D PC", color: C.sky, area: "Aseana Business Park, Parañaque", type: "both", note: "Luxury club near Entertainment City. Opens 2PM6AM daily. Cash 50/100200+. Daily tournaments with GTDs (check FB for schedule). Reopened Jan 2026. Bad Beat Jackpot running. Ph: +63 917 184 9417" },
prime: { name: "Prime Poker Club", short: "Prime", color: C.rosewater, area: "Macapagal Blvd, Pasay", type: "cash", note: "Cash games. HK SunPlaza location. Ph: +63 917 722 8667" },
twoace: { name: "2Ace Poker Manila", short: "2Ace", color: C.overlay2, area: "Manila", type: "both", note: "Community-focused club with lower rakes. Local tournaments and cash games. Check socials for schedule." },
};
const DAYS = [
{ date: "2026-05-04", label: "Monday, May 4", short: "Mon 4", dow: "Monday" },
{ date: "2026-05-05", label: "Tuesday, May 5", short: "Tue 5", dow: "Tuesday" },
{ date: "2026-05-06", label: "Wednesday, May 6", short: "Wed 6", dow: "Wednesday" },
{ date: "2026-05-07", label: "Thursday, May 7", short: "Thu 7", dow: "Thursday" },
{ date: "2026-05-08", label: "Friday, May 8", short: "Fri 8", dow: "Friday" },
{ date: "2026-05-09", label: "Saturday, May 9", short: "Sat 9", dow: "Saturday" },
{ date: "2026-05-10", label: "Sunday, May 10", short: "Sun 10", dow: "Sunday" },
];
const METRO_DAILY = {
Monday: { buyin: 750, guarantee: 50000, chips: 8000, fee: "700+50" },
Tuesday: { buyin: 1100, guarantee: 75000, chips: 10000, fee: "1,000+100" },
Wednesday: { buyin: 1500, guarantee: 100000, chips: 10000, fee: "1,400+100" },
Thursday: { buyin: 1750, guarantee: 120000, chips: 10000, fee: "1,600+150" },
Friday: { buyin: 1500, guarantee: 100000, chips: 10000, fee: "1,400+100" },
Saturday: { buyin: 1750, guarantee: 120000, chips: 10000, fee: "1,600+150" },
Sunday: { buyin: 1750, guarantee: 120000, chips: 10000, fee: "1,600+150" },
};
function buildTournaments() {
const ts = [];
let id = 0;
const nid = () => `t${id++}`;
const ok = (date, time, event, name, buyin, guarantee, format, notes, md, mdInfo) =>
ts.push({ id: nid(), date, time, venue: "okada", event, name, buyin, guarantee, format, notes, multiDay: !!md, multiDayInfo: mdInfo || null });
// MAY 4 Mon
ok("2026-05-04","12:00","#21","Ultra Stack",10000,780000,"NLH","₱780K GTD",false);
ok("2026-05-04","17:00","#22","Deepstack Turbo",8000,null,"NLH","Turbo",false);
ok("2026-05-04","20:00","#23","Okada Millions Qualifier",1900,null,"NLH","WYS at 25K Chips",false);
ok("2026-05-04","21:00","#24","Win the Button Hyper",4000,null,"NLH","Hyper Turbo",false);
// MAY 5 Tue
ok("2026-05-05","12:00","#25","Knockout [₱5K Bounty]",10000,780000,"NLH PKO","₱780K GTD · ₱5K bounty per KO",false);
ok("2026-05-05","17:00","#26","6 Handed Turbo",8000,null,"NLH","6-Max Turbo",false);
ok("2026-05-05","20:00","#27","Millions Qualifier",1900,null,"NLH","WYS at 25K Chips",false);
ok("2026-05-05","21:00","#28","Run it Twice Hyper",4000,null,"NLH","Hyper Turbo",false);
// MAY 6 Wed
ok("2026-05-06","12:00","#29A","Okada Millions Flt A ★",8000,6000000,"NLH","₱6M GTD — MAIN EVENT Flight A",true,"Day 2: May 9 · Final: May 10");
ok("2026-05-06","15:00","#30","NLH Freezeout",10000,null,"NLH","Freezeout — no re-entry",false);
ok("2026-05-06","17:00","#29B","Okada Millions Flt B ★",8000,6000000,"NLH","₱6M GTD — Flt B [20min lvls]",true,"Day 2: May 9 · Final: May 10");
ok("2026-05-06","19:00","#31","Megastack Turbo",5000,null,"NLH","Turbo",false);
ok("2026-05-06","20:00","#32","Millions Qualifier",1900,null,"NLH","WYS at 25K Chips",false);
// MAY 7 Thu
ok("2026-05-07","12:00","#29C","Okada Millions Flt C ★",8000,6000000,"NLH","₱6M GTD — Flight C",true,"Day 2: May 9 · Final: May 10");
ok("2026-05-07","15:00","#33","Shot Clock",10000,null,"NLH","Shot Clock format",false);
ok("2026-05-07","17:00","#29D","Okada Millions Flt D ★",8000,6000000,"NLH","₱6M GTD — Flt D [20min lvls]",true,"Day 2: May 9 · Final: May 10");
ok("2026-05-07","19:00","#34","KO Turbo [₱1K Bounty]",5000,null,"NLH PKO","₱1K Bounty Turbo",false);
ok("2026-05-07","20:00","#35","Millions Qualifier",1900,null,"NLH","WYS at 25K Chips",false);
// MAY 8 Fri
ok("2026-05-08","12:00","#29E","Okada Millions Flt E ★",8000,6000000,"NLH","₱6M GTD — Flight E",true,"Day 2: May 9 · Final: May 10");
ok("2026-05-08","15:00","#36A","Mystery Bounty Flt A",15000,2000000,"NLH MB","₱2M GTD — Mystery Bounty",true,"Day 2 Final: May 10");
ok("2026-05-08","15:00","#37","Millions Qualifier",1900,null,"NLH","WYS at 25K Chips",false);
ok("2026-05-08","17:00","#29F","Okada Millions Flt F ★",8000,6000000,"NLH","₱6M GTD — Flt F [20min]",true,"Day 2: May 9 · Final: May 10");
ok("2026-05-08","19:00","#38","SuperStack Turbo FO",5000,null,"NLH","Turbo Freezeout",false);
ok("2026-05-08","21:00","#29G","Okada Millions Flt G ★",8000,6000000,"NLH","₱6M GTD — Turbo Flight G",true,"Day 2: May 9 · Final: May 10");
ok("2026-05-08","22:00","#39","Mystery Bounty Qual",3000,null,"NLH","WYS at 30K Chips",false);
// MAY 9 Sat
ok("2026-05-09","12:00","#36B","Mystery Bounty Flt B",15000,2000000,"NLH MB","₱2M GTD",true,"Day 2 Final: May 10");
ok("2026-05-09","12:00","#40","Superstack Freezeout",8000,null,"NLH","Freezeout",false);
ok("2026-05-09","13:00","#29 D2","Millions — Day 2",0,6000000,"NLH","Day 2 — qualified only",true,"Final 9: May 10");
ok("2026-05-09","15:00","#41","Pot Limit Omaha",8000,null,"PLO","",false);
ok("2026-05-09","15:00","#42","Mystery Bounty Qual",3000,null,"NLH","WYS at 30K",false);
ok("2026-05-09","17:00","#36C","Mystery Bounty Flt C",15000,2000000,"NLH MB","₱2M GTD [20/15min]",true,"Day 2 Final: May 10");
ok("2026-05-09","20:00","#43","Swap Hold'em Turbo",5000,null,"Swap","Turbo",false);
ok("2026-05-09","21:00","#44","NLH 10/10/10",10000,null,"NLH","10/10/10 format",false);
// MAY 10 Sun
ok("2026-05-10","12:00","#45","Survivor KO [₱25K!]",12500,1000000,"NLH PKO","₱1M GTD · ₱25K bounty!",false);
ok("2026-05-10","12:00","#36 Fin","Mystery Bounty Final",0,2000000,"NLH MB","Day 2 Final — qualified only",true,"FINAL DAY");
ok("2026-05-10","13:00","#29 Fin","Millions — Final 9",0,6000000,"NLH","Final Table — qualified only",true,"FINAL DAY");
ok("2026-05-10","17:00","#46","6 Handed Turbo",8000,null,"NLH","6-Max Turbo",false);
ok("2026-05-10","19:00","#47","Last Chance Super Hyper",5000,null,"NLH","LAST EVENT of the series!",false);
// Metro Card Club dailies
DAYS.forEach(d => {
const m = METRO_DAILY[d.dow];
ts.push({ id: nid(), date: d.date, time: "15:00", venue: "metro", event: "Daily", name: `${d.dow} ${(m.guarantee/1000)}K GTD`, buyin: m.buyin, guarantee: m.guarantee, format: "NLH", notes: `${m.fee} PHP · ${m.chips.toLocaleString()} chips · 20min blinds · Late reg thru lvl 9 · Early bird: +20% chips before 2PM`, multiDay: false, multiDayInfo: null });
});
// Let's All In dailies
DAYS.forEach(d => {
ts.push({ id: nid(), date: d.date, time: "18:00", venue: "letsin", event: "Daily", name: "Evening Tournament", buyin: 500, guarantee: null, format: "NLH", notes: "~₱250-₱500 range (varies day-to-day). Cash games also from 10/20 PHP.", multiDay: false, multiDayInfo: null });
});
// 9D Poker Club dailies (schedule varies — check FB)
DAYS.forEach(d => {
ts.push({ id: nid(), date: d.date, time: "17:00", venue: "nineD", event: "Daily", name: "Daily GTD Tournament", buyin: 2000, guarantee: 50000, format: "NLH", notes: "~₱1K-₱3K range (varies). GTDs from ₱50K₱500K+ depending on day. Check 9D FB page for exact schedule. Opens 2PM.", multiDay: false, multiDayInfo: null });
});
return ts;
}
const ALL_T = buildTournaments();
const STATUS = {
upcoming: { label: "Upcoming", color: C.overlay1, icon: "⏳" },
registered: { label: "Registered", color: C.yellow, icon: "🎫" },
playing: { label: "Playing", color: C.green, icon: "🃏" },
busted: { label: "Busted", color: C.red, icon: "💀" },
cashed: { label: "Cashed!", color: C.teal, icon: "💰" },
day2: { label: "Made Day 2", color: C.mauve, icon: "🌟" },
skipped: { label: "Skipped", color: C.surface2, icon: "⏭️" },
};
const SK = "manila-poker-v3";
const load = () => { try { return JSON.parse(localStorage.getItem(SK)); } catch { return null; } };
const save = s => { try { localStorage.setItem(SK, JSON.stringify(s)); } catch {} };
const cv = (php, cur) => { if (php == null) return "—"; if (cur==="USD") return `$${Math.round(php*PHP_TO_USD).toLocaleString()}`; if (cur==="DKK") return `${Math.round(php*PHP_TO_DKK).toLocaleString()} kr`; return `${php.toLocaleString()}`; };
export default function App() {
const sv = load();
const [day, setDay] = useState("2026-05-04");
const [sts, setSts] = useState(sv?.sts || {});
const [prizes, setPrizes] = useState(sv?.prizes || {});
const [places, setPlaces] = useState(sv?.places || {});
const [reents, setReents] = useState(sv?.reents || {});
const [editId, setEditId] = useState(null);
const [showBudget, setShowBudget] = useState(false);
const [cur, setCur] = useState("PHP");
const [vf, setVf] = useState("all");
const [showCash, setShowCash] = useState(false);
const [dimOthers, setDimOthers] = useState(true);
useEffect(() => { save({ sts, prizes, places, reents }); }, [sts, prizes, places, reents]);
const gs = id => sts[id] || "upcoming";
const ss = (id, s) => { setSts(p => ({...p,[id]:s})); if(s==="cashed") setEditId(id); else setEditId(null); };
const dayTs = useMemo(() => {
let t = ALL_T.filter(t => t.date === day);
if (vf !== "all") t = t.filter(t => t.venue === vf);
return t.sort((a,b) => a.time.localeCompare(b.time) || a.venue.localeCompare(b.venue));
}, [day, vf]);
const hasActive = dayTs.some(t => ["playing","registered"].includes(gs(t.id)));
const budget = useMemo(() => {
let sp=0, wn=0, cnt=0, cs=0;
ALL_T.forEach(t => {
const s = gs(t.id);
if (["registered","playing","busted","cashed","day2"].includes(s)) { sp += t.buyin*(1+(reents[t.id]||0)); cnt++; }
if (s==="cashed" && prizes[t.id]) { wn += prizes[t.id]; cs++; }
});
return { sp, wn, net: wn-sp, cnt, cs };
}, [sts, prizes, reents]);
const dStats = date => {
const t = ALL_T.filter(t => t.date === date);
const p = t.filter(t => !["upcoming","skipped"].includes(gs(t.id)));
const w = t.filter(t => gs(t.id)==="cashed");
return { tot: t.length, pl: p.length, wn: w.length };
};
const $cv = php => cv(php, cur);
return (
<div style={{ fontFamily: "'JetBrains Mono','Fira Code','SF Mono',monospace", background: `linear-gradient(170deg,${C.crust},${C.base} 40%,${C.mantle})`, color: C.text, minHeight: "100vh" }}>
{/* HEADER */}
<div style={{ background: `linear-gradient(135deg,${C.mantle},${C.crust}ee)`, borderBottom: `1px solid ${C.surface0}`, padding: "14px 16px 10px", position: "sticky", top: 0, zIndex: 100, backdropFilter: "blur(16px)" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", flexWrap: "wrap", gap: 8 }}>
<div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 22 }}>🃏</span>
<h1 style={{ margin: 0, fontSize: 17, fontWeight: 700, background: `linear-gradient(135deg,${C.blue},${C.mauve})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }}>Manila Poker Trip</h1>
</div>
<p style={{ margin: "2px 0 0", fontSize: 9, color: C.overlay1, letterSpacing: "0.5px" }}>MAY 410 · OKADA MILLIONS + METRO CC + LOCAL ROOMS</p>
</div>
<div style={{ display: "flex", gap: 5, alignItems: "center" }}>
<button onClick={() => setShowBudget(!showBudget)} style={{ background: showBudget ? C.surface1 : C.surface0, border: `1px solid ${C.surface1}`, color: C.text, padding: "4px 8px", borderRadius: 5, fontSize: 10, cursor: "pointer", fontFamily: "inherit" }}>💰</button>
<select value={cur} onChange={e => setCur(e.target.value)} style={{ background: C.surface0, border: `1px solid ${C.surface1}`, color: C.text, padding: "4px 5px", borderRadius: 5, fontSize: 10, cursor: "pointer", fontFamily: "inherit" }}>
<option value="PHP"></option><option value="USD">$</option><option value="DKK">kr</option>
</select>
</div>
</div>
{showBudget && (
<div style={{ marginTop: 8, padding: 10, background: C.surface0, borderRadius: 8, display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(100px,1fr))", gap: 6 }}>
<SB l="Buy-ins" v={$cv(budget.sp)} s={`${budget.cnt} events`} c={C.red} />
<SB l="Won" v={$cv(budget.wn)} s={`${budget.cs} cashes`} c={C.green} />
<SB l="Net" v={(budget.net>=0?"+":"-")+$cv(Math.abs(budget.net))} s={budget.net>=0?"PROFIT":"LOSS"} c={budget.net>=0?C.teal:C.maroon} />
<SB l="ROI" v={budget.sp>0?`${Math.round(((budget.wn-budget.sp)/budget.sp)*100)}%`:"—"} s="" c={C.lavender} />
</div>
)}
</div>
{/* DAY TABS */}
<div style={{ display: "flex", gap: 3, padding: "8px 10px", overflowX: "auto" }}>
{DAYS.map(d => {
const s = dStats(d.date); const a = d.date===day;
return <button key={d.date} onClick={() => setDay(d.date)} style={{ flex: "0 0 auto", padding: "5px 10px", borderRadius: 7, border: a?`2px solid ${C.blue}`:`1px solid ${C.surface1}50`, background: a?`${C.blue}18`:"transparent", color: a?C.blue:C.subtext1, cursor: "pointer", fontFamily: "inherit", fontSize: 10, fontWeight: a?700:500, textAlign: "center" }}>
<div>{d.short}</div>
{s.pl > 0 && <div style={{ fontSize: 8, marginTop: 1, color: s.wn>0?C.green:C.overlay0 }}>{s.pl}/{s.tot}{s.wn>0?` 💰${s.wn}`:""}</div>}
</button>;
})}
</div>
{/* FILTERS */}
<div style={{ padding: "3px 10px", display: "flex", gap: 3, overflowX: "auto", flexWrap: "wrap" }}>
{[{k:"all",l:"All",c:C.text},{k:"okada",l:"Okada",c:VENUES.okada.color},{k:"metro",l:"Metro CC",c:VENUES.metro.color},{k:"letsin",l:"Let's All In",c:VENUES.letsin.color},{k:"nineD",l:"9D PC",c:VENUES.nineD.color}].map(f =>
<button key={f.k} onClick={() => { setVf(f.k); setShowCash(false); }} style={{ padding: "2px 7px", borderRadius: 4, border: `1px solid ${vf===f.k?f.c:C.surface1}40`, background: vf===f.k?`${f.c}20`:"transparent", color: vf===f.k?f.c:C.overlay1, cursor: "pointer", fontFamily: "inherit", fontSize: 9, fontWeight: vf===f.k?600:400, whiteSpace: "nowrap" }}>{f.l}</button>
)}
<button onClick={() => setShowCash(!showCash)} style={{ padding: "2px 7px", borderRadius: 4, border: `1px solid ${showCash?C.peach:C.surface1}40`, background: showCash?`${C.peach}20`:"transparent", color: showCash?C.peach:C.overlay1, cursor: "pointer", fontFamily: "inherit", fontSize: 9, whiteSpace: "nowrap" }}>
🎰 More Venues {showCash?"▴":"▾"}
</button>
</div>
{/* OTHER VENUES PANEL */}
{showCash && (
<div style={{ margin: "6px 10px", padding: 10, background: C.surface0, borderRadius: 8, fontSize: 10 }}>
<div style={{ fontWeight: 700, marginBottom: 6, color: C.subtext1, fontSize: 11 }}>Other Poker Venues (cash + walk-in tourneys)</div>
{["nineD","masters","dynasty","solaire","cod","newport","prime","twoace"].map(k => {
const v = VENUES[k];
return <div key={k} style={{ marginBottom: 6, padding: "6px 8px", background: `${v.color}08`, borderRadius: 6, borderLeft: `3px solid ${v.color}` }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ fontWeight: 600, color: v.color, fontSize: 11 }}>{v.name}</span>
{v.type === "both" && <span style={{ fontSize: 8, padding: "1px 4px", borderRadius: 3, background: `${C.green}20`, color: C.green, fontWeight: 600 }}>Tourneys</span>}
{v.type === "cash" && <span style={{ fontSize: 8, padding: "1px 4px", borderRadius: 3, background: `${C.overlay0}20`, color: C.overlay0, fontWeight: 600 }}>Cash only</span>}
</div>
<div style={{ color: C.overlay1, marginTop: 1 }}>{v.area}</div>
<div style={{ color: C.subtext0, marginTop: 1, lineHeight: 1.3 }}>{v.note}</div>
</div>;
})}
</div>
)}
{/* TOURNAMENTS */}
<div style={{ padding: "6px 10px 90px" }}>
{hasActive && (
<div style={{ padding: "5px 8px", background: `${C.green}10`, border: `1px solid ${C.green}30`, borderRadius: 7, marginBottom: 8, fontSize: 10, color: C.green, display: "flex", alignItems: "center", gap: 5 }}>
<span style={{ fontSize: 13 }}>🎯</span> Active session!
<label style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 3, color: C.overlay1, cursor: "pointer", fontSize: 9 }}>
<input type="checkbox" checked={dimOthers} onChange={e => setDimOthers(e.target.checked)} style={{ accentColor: C.green, width: 12, height: 12 }} /> Dim others
</label>
</div>
)}
{dayTs.length === 0 ? <div style={{ textAlign: "center", padding: 30, color: C.overlay0, fontSize: 12 }}>No tournaments for this day/filter.</div> :
dayTs.map(t => <TC key={t.id} t={t} status={gs(t.id)} ss={s => ss(t.id,s)} editId={editId} setEditId={setEditId} prize={prizes[t.id]} setPrize={v => setPrizes(p => ({...p,[t.id]:v}))} place={places[t.id]} setPlace={v => setPlaces(p => ({...p,[t.id]:v}))} re={reents[t.id]||0} setRe={v => setReents(p => ({...p,[t.id]:v}))} $cv={$cv} cur={cur} dim={hasActive && dimOthers} />)
}
</div>
{/* FOOTER */}
<div style={{ position: "fixed", bottom: 0, left: 0, right: 0, background: `${C.crust}f0`, backdropFilter: "blur(12px)", borderTop: `1px solid ${C.surface0}`, padding: "5px 10px", fontSize: 8, color: C.overlay0, display: "flex", justifyContent: "space-between" }}>
<span>Okada Manila Millions · Apr 29May 10</span>
<span>3% staffing (Okada) · 5% svc (Metro) · All PHP</span>
</div>
</div>
);
}
function SB({ l, v, s, c }) {
return <div style={{ padding: "6px 8px", background: `${c}08`, borderRadius: 5, border: `1px solid ${c}20` }}>
<div style={{ fontSize: 8, color: C.overlay1, textTransform: "uppercase", letterSpacing: 0.7 }}>{l}</div>
<div style={{ fontSize: 14, fontWeight: 700, color: c }}>{v}</div>
{s && <div style={{ fontSize: 8, color: C.overlay0 }}>{s}</div>}
</div>;
}
function TC({ t, status, ss, editId, setEditId, prize, setPrize, place, setPlace, re, setRe, $cv, cur, dim }) {
const v = VENUES[t.venue];
const cfg = STATUS[status];
const isAct = ["playing","registered","day2"].includes(status);
const isDone = ["busted","cashed","skipped"].includes(status);
const dimmed = dim && !isAct && status==="upcoming";
const isEd = editId === t.id;
const free = t.buyin === 0;
return (
<div style={{ marginBottom: 5, borderRadius: 8, border: `1px solid ${isAct?cfg.color:C.surface0}${isAct?"60":"80"}`, background: isAct?`linear-gradient(135deg,${cfg.color}10,${C.surface0}60)`:`${C.surface0}60`, overflow: "hidden", opacity: isDone&&status!=="cashed"?0.45:dimmed?0.35:1, transition: "all 0.2s" }}>
<div style={{ height: 2, background: v.color, opacity: 0.5 }} />
<div style={{ padding: "8px 10px" }}>
{/* Row 1 */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 3, flexWrap: "wrap", gap: 3 }}>
<div style={{ display: "flex", alignItems: "center", gap: 5, flexWrap: "wrap" }}>
<span style={{ fontWeight: 700, fontSize: 12, color: C.blue, fontVariantNumeric: "tabular-nums" }}>{t.time}</span>
<Bdg text={v.short} color={v.color} />
{t.multiDay && <Bdg text="Multi-Day" color={C.mauve} />}
<Bdg text={t.format} color={C.overlay1} bg={`${C.surface1}60`} />
</div>
<span style={{ fontSize: 9, color: cfg.color, fontWeight: 600 }}>{cfg.icon} {cfg.label}</span>
</div>
{/* Row 2 */}
<div style={{ marginBottom: 3 }}>
<span style={{ color: C.overlay0, fontSize: 9, marginRight: 4 }}>{t.event}</span>
<span style={{ fontWeight: 600, fontSize: 12, color: t.name.includes("★")?C.yellow:C.text }}>{t.name}</span>
</div>
{/* Row 3 */}
<div style={{ display: "flex", gap: 12, alignItems: "center", marginBottom: 2, flexWrap: "wrap" }}>
{!free && <div style={{ fontSize: 10 }}><span style={{ color: C.overlay0 }}>Buy-in: </span><span style={{ fontWeight: 600, color: C.yellow }}>{$cv(t.buyin)}</span>{re>0 && <span style={{ color: C.peach, fontSize: 9, marginLeft: 2 }}>(+{re}re={$cv(t.buyin*(1+re))})</span>}</div>}
{t.guarantee && <div style={{ fontSize: 10 }}><span style={{ color: C.overlay0 }}>GTD: </span><span style={{ fontWeight: 600, color: C.green }}>{$cv(t.guarantee)}</span></div>}
</div>
{t.notes && <div style={{ fontSize: 9, color: C.overlay1, marginBottom: 2, lineHeight: 1.3 }}>{t.notes}</div>}
{t.multiDay && t.multiDayInfo && <div style={{ fontSize: 9, color: C.mauve, marginBottom: 3 }}>📅 {t.multiDayInfo}</div>}
{status==="cashed" && prize>0 && (
<div style={{ padding: "5px 7px", background: `${C.teal}15`, borderRadius: 5, marginBottom: 5, display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
<div><span style={{ fontSize: 8, color: C.overlay0 }}>Prize: </span><span style={{ fontWeight: 700, color: C.teal, fontSize: 12 }}>{$cv(prize)}</span></div>
{place>0 && <div><span style={{ fontSize: 8, color: C.overlay0 }}>Finish: </span><span style={{ fontWeight: 700, color: C.lavender, fontSize: 12 }}>#{place}</span></div>}
<div style={{ marginLeft: "auto", fontSize: 9, color: prize>t.buyin*(1+(re||0))?C.green:C.red, fontWeight: 600 }}>
{prize>t.buyin*(1+(re||0))?"+":""}{$cv(prize-t.buyin*(1+(re||0)))} net
</div>
</div>
)}
{isEd && (
<div style={{ padding: 8, background: C.surface0, borderRadius: 5, marginBottom: 5 }}>
<div style={{ fontSize: 9, color: C.subtext0, marginBottom: 5, fontWeight: 600 }}>Record Result</div>
<div style={{ display: "flex", gap: 5, flexWrap: "wrap", alignItems: "flex-end" }}>
<div>
<label style={{ fontSize: 8, color: C.overlay0 }}>Prize ()</label>
<input type="number" value={prize||""} onChange={e => setPrize(parseInt(e.target.value)||0)} placeholder="0" style={{ display: "block", marginTop: 2, padding: "3px 5px", background: C.surface1, border: `1px solid ${C.surface2}`, borderRadius: 3, color: C.text, fontFamily: "inherit", fontSize: 11, width: 80 }} />
</div>
<div>
<label style={{ fontSize: 8, color: C.overlay0 }}>Finish #</label>
<input type="number" value={place||""} onChange={e => setPlace(parseInt(e.target.value)||0)} placeholder="#" style={{ display: "block", marginTop: 2, padding: "3px 5px", background: C.surface1, border: `1px solid ${C.surface2}`, borderRadius: 3, color: C.text, fontFamily: "inherit", fontSize: 11, width: 45 }} />
</div>
<button onClick={() => setEditId(null)} style={{ padding: "3px 10px", background: C.teal, border: "none", borderRadius: 3, color: C.crust, fontFamily: "inherit", fontSize: 10, fontWeight: 700, cursor: "pointer" }}>Save</button>
</div>
</div>
)}
{/* Actions */}
<div style={{ display: "flex", gap: 3, flexWrap: "wrap", marginTop: 2 }}>
{status==="upcoming" && !free && <><AB l="🎫 Register" c={C.yellow} o={() => ss("registered")} /><AB l="⏭️ Skip" c={C.surface2} o={() => ss("skipped")} /></>}
{status==="registered" && <><AB l="🃏 Playing" c={C.green} o={() => ss("playing")} /><AB l="↩️" c={C.overlay0} o={() => ss("upcoming")} /></>}
{status==="playing" && <>
<AB l="💀 Bust" c={C.red} o={() => ss("busted")} />
<AB l="💰 Cash!" c={C.teal} o={() => ss("cashed")} />
{t.multiDay && <AB l="🌟 Day 2" c={C.mauve} o={() => ss("day2")} />}
<div style={{ display: "flex", alignItems: "center", gap: 2, marginLeft: 4 }}>
<span style={{ fontSize: 8, color: C.overlay0 }}>Re:</span>
<SBtn o={() => setRe(Math.max(0,re-1))} l="" d={re===0} />
<span style={{ fontSize: 10, fontWeight: 600, minWidth: 10, textAlign: "center" }}>{re}</span>
<SBtn o={() => setRe(re+1)} l="+" />
</div>
</>}
{status==="day2" && <><AB l="🃏 D2" c={C.green} o={() => ss("playing")} /><AB l="💀" c={C.red} o={() => ss("busted")} /><AB l="💰" c={C.teal} o={() => ss("cashed")} /></>}
{status==="cashed" && !isEd && <AB l="✏️ Edit" c={C.lavender} o={() => setEditId(t.id)} />}
{["busted","cashed","skipped"].includes(status) && <AB l="↩️ Reset" c={C.overlay0} o={() => { ss("upcoming"); setPrize(undefined); setPlace(undefined); setRe(0); }} />}
</div>
</div>
</div>
);
}
function Bdg({ text, color, bg }) {
return <span style={{ fontSize: 8, padding: "1px 4px", borderRadius: 3, background: bg||`${color}20`, color, fontWeight: 600 }}>{text}</span>;
}
function AB({ l, c, o }) {
return <button onClick={o} style={{ padding: "3px 7px", borderRadius: 4, border: `1px solid ${c}40`, background: `${c}15`, color: c, cursor: "pointer", fontFamily: "inherit", fontSize: 9, fontWeight: 600 }} onMouseEnter={e=>e.target.style.background=`${c}30`} onMouseLeave={e=>e.target.style.background=`${c}15`}>{l}</button>;
}
function SBtn({ o, l, d }) {
return <button onClick={o} style={{ width: 16, height: 16, borderRadius: 2, border: `1px solid ${C.surface2}`, background: C.surface1, color: C.text, fontSize: 11, fontWeight: 700, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "inherit", padding: 0, opacity: d?0.3:1 }}>{l}</button>;
}