0
GAMES#f1b0cf1a
TOURNAMENT DIRECTOR (fork)
@GigaChad·deposited 3w ago·updated 2w ago·134 views
GAMES#f1b0cf1a
TOURNAMENT DIRECTOR (fork)
GI
@GigaChad
134Views
0Comments
1Forks
0Saves
SHARE · REMIX
TOURNAMENT DIRECTOR (fork) — a JSX Games widget by @GigaChad.
animationgametimerclock
No comments yet. Be the first!
✦ Remix with AI
SDK in this widgetNo Vibes SDK features detected yet
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "TOURNAMENT DIRECTOR (fork)" by @GigaChad]
Source: https://itjustvibes.com/GigaChad/tournament-director-fork
Type: React/JSX
--- SOURCE CODE ---
```jsx
import { useState, useEffect, useRef } from "react";
const CHIP_COLORS = {
white: { bg: "#F0EDE8", border: "#C8C3BA", text: "#3A3632", label: "White" },
red: { bg: "#C0392B", border: "#922B21", text: "#FFF", label: "Red" },
blue: { bg: "#2471A3", border: "#1A5276", text: "#FFF", label: "Blue" },
green: { bg: "#1E8449", border: "#145A32", text: "#FFF", label: "Green" },
black: { bg: "#2C2C2C", border: "#111", text: "#F0D060", label: "Black" },
};
const DEFAULT_CHIPS = [
{ color: "white", value: 1, count: 100 },
{ color: "red", value: 5, count: 50 },
{ color: "blue", value: 10, count: 50 },
{ color: "green", value: 25, count: 25 },
{ color: "black", value: 100, count: 10 },
];
const SK = "poker-tourney-v2";
const SHARED = true;
function genBlinds(stack) {
const levels = [];
let sb = Math.max(1, Math.round(stack * 0.005));
const mult = [1, 1.5, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 100, 150];
for (let i = 0; i < 15; i++) {
const csb = Math.round(sb * mult[i]);
levels.push({ level: i + 1, sb: csb, bb: csb * 2, ante: i >= 4 ? Math.round(csb * 0.5) : 0, duration: i < 6 ? 15 : i < 10 ? 12 : 10 });
}
return levels;
}
function distChips(chips, players, stack) {
const sorted = [...chips].filter(c => c.count > 0 && c.value > 0).sort((a, b) => a.value - b.value);
if (!sorted.length || !players) return { dist: [], total: 0, left: [] };
let rem = stack; const d = [], used = {};
for (let i = sorted.length - 1; i >= 0; i--) {
const c = sorted[i], give = Math.min(Math.floor(rem / c.value), Math.floor(c.count / players));
if (give > 0) { d.push({ ...c, per: give }); used[c.color] = give * players; rem -= give * c.value; }
}
return { dist: d.reverse(), total: stack - rem, left: sorted.map(c => ({ ...c, count: c.count - (used[c.color] || 0) })).filter(c => c.count > 0) };
}
function ChipIcon({ color, size = 32 }) {
const c = CHIP_COLORS[color] || CHIP_COLORS.white;
return (
<div style={{ width: size, height: size, borderRadius: "50%", background: `radial-gradient(circle at 35% 35%, ${c.bg}, ${c.border})`, border: `2px solid ${c.border}`, display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 2px 4px rgba(0,0,0,0.3), inset 0 1px 2px rgba(255,255,255,0.2)", position: "relative", overflow: "hidden", flexShrink: 0 }}>
<div style={{ position: "absolute", inset: 3, borderRadius: "50%", border: `1.5px dashed ${c.text}40` }} />
</div>
);
}
function Timer({ duration, isRunning, onToggle, onReset, onComplete, savedSec, onTick }) {
const [sec, setSec] = useState(savedSec != null ? savedSec : duration * 60);
const iRef = useRef(null);
useEffect(() => { if (savedSec == null) setSec(duration * 60); }, [duration]);
useEffect(() => { if (savedSec != null) setSec(savedSec); }, [savedSec]);
useEffect(() => {
if (isRunning && sec > 0) {
iRef.current = setInterval(() => setSec(s => {
if (s <= 1) { clearInterval(iRef.current); onComplete?.(); return 0; }
const n = s - 1; onTick?.(n); return n;
}), 1000);
}
return () => clearInterval(iRef.current);
}, [isRunning, sec > 0]);
const reset = () => { setSec(duration * 60); onTick?.(duration * 60); onReset(); };
const m = Math.floor(sec / 60), s = sec % 60, pct = (sec / (duration * 60)) * 100, urg = sec < 60;
return (
<div style={{ textAlign: "center" }}>
<div style={{ fontFamily: "'Courier Prime', monospace", fontSize: 56, fontWeight: 700, letterSpacing: 4, color: urg ? "#E74C3C" : "#F0D060", textShadow: urg ? "0 0 20px #E74C3C80" : "0 0 15px #F0D06040", animation: urg && isRunning ? "pulse 1s infinite" : "none" }}>
{String(m).padStart(2, "0")}:{String(s).padStart(2, "0")}
</div>
<div style={{ height: 6, background: "#1a1a1a", borderRadius: 3, margin: "8px 0 16px", overflow: "hidden", border: "1px solid #333" }}>
<div style={{ height: "100%", borderRadius: 3, transition: "width 1s linear", width: `${pct}%`, background: urg ? "linear-gradient(90deg,#E74C3C,#C0392B)" : "linear-gradient(90deg,#F0D060,#D4A017)" }} />
</div>
<div style={{ display: "flex", gap: 10, justifyContent: "center" }}>
<button onClick={onToggle} style={tBtn}>{isRunning ? "⏸ Pause" : "▶ Start"}</button>
<button onClick={reset} style={tBtn}>↺ Reset</button>
</div>
</div>
);
}
const tBtn = { padding: "8px 20px", borderRadius: 6, background: "linear-gradient(135deg,#2C2C2C,#1a1a1a)", border: "1px solid #F0D06050", color: "#F0D060", fontFamily: "'Courier Prime',monospace", fontSize: 14, cursor: "pointer", fontWeight: 600, letterSpacing: 1 };
export default function App() {
const [loaded, setLoaded] = useState(false);
const [players, setPlayers] = useState(8);
const [chips, setChips] = useState(DEFAULT_CHIPS);
const [tab, setTab] = useState("setup");
const [lvl, setLvl] = useState(0);
const [running, setRunning] = useState(false);
const [pRem, setPRem] = useState(8);
const [cDur, setCDur] = useState({});
const [tSec, setTSec] = useState(null);
const [eLog, setELog] = useState([]);
const [status, setStatus] = useState("");
// ── LOAD from shared storage ──
useEffect(() => {
(async () => {
try {
const r = await window.storage.get(SK, SHARED);
if (r?.value) {
const s = JSON.parse(r.value);
if (s.players) setPlayers(s.players);
if (s.chips) setChips(s.chips);
if (s.tab) setTab(s.tab);
if (s.lvl != null) setLvl(s.lvl);
if (s.pRem != null) setPRem(s.pRem);
if (s.cDur) setCDur(s.cDur);
if (s.tSec != null) setTSec(s.tSec);
if (s.eLog) setELog(s.eLog);
}
} catch (e) { console.log("No saved state."); }
setLoaded(true);
})();
}, []);
// ── AUTO-SAVE to shared storage ──
const saveRef = useRef(null);
useEffect(() => {
if (!loaded) return;
clearTimeout(saveRef.current);
saveRef.current = setTimeout(async () => {
try {
await window.storage.set(SK, JSON.stringify({ players, chips, tab, lvl, pRem, cDur, tSec, eLog, at: Date.now() }), SHARED);
setStatus("Saved ✓"); setTimeout(() => setStatus(""), 2000);
} catch (e) { setStatus("Save failed"); setTimeout(() => setStatus(""), 3000); }
}, 800);
return () => clearTimeout(saveRef.current);
}, [players, chips, tab, lvl, pRem, cDur, tSec, eLog, loaded]);
const pool = chips.reduce((s, c) => s + c.value * c.count, 0);
const perP = Math.floor(pool / players);
const blinds = genBlinds(perP);
const { dist, total, left } = distChips(chips, players, perP);
const cb = blinds[lvl] || blinds[14];
const dur = cDur[lvl] ?? cb.duration;
const avg = Math.round(pool / Math.max(pRem, 1));
const bbs = cb ? Math.round(avg / cb.bb) : 0;
useEffect(() => { if (loaded) setPRem(p => Math.min(p, players)); }, [players]);
const updChip = (i, f, v) => { const n = [...chips]; n[i] = { ...n[i], [f]: Math.max(0, parseInt(v) || 0) }; setChips(n); };
const elim = () => { if (pRem <= 1) return; setPRem(pRem - 1); setELog([...eLog, { place: pRem, lvl: lvl + 1, time: new Date().toLocaleTimeString() }]); };
const reset = async () => {
setPlayers(8); setChips(DEFAULT_CHIPS); setTab("setup"); setLvl(0); setRunning(false); setPRem(8); setCDur({}); setTSec(null); setELog([]);
try { await window.storage.delete(SK, SHARED); } catch (e) {}
setStatus("Reset ✓"); setTimeout(() => setStatus(""), 2000);
};
if (!loaded) return (
<div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", background: "#0B0E11", color: "#F0D060", fontFamily: "'Courier Prime',monospace", fontSize: 16, letterSpacing: 3 }}>
Loading...
</div>
);
return (
<div style={{ minHeight: "100vh", background: "linear-gradient(160deg,#0B0E11 0%,#14191F 40%,#0D1117 100%)", color: "#E8E2D6", fontFamily: "'Libre Baskerville',Georgia,serif" }}>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&display=swap');
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
@keyframes slideIn{from{opacity:0;transform:translateX(-12px)}to{opacity:1;transform:translateX(0)}}
input[type=number]{-moz-appearance:textfield}
input::-webkit-outer-spin-button,input::-webkit-inner-spin-button{-webkit-appearance:none}
*::-webkit-scrollbar{width:6px}*::-webkit-scrollbar-track{background:#111}*::-webkit-scrollbar-thumb{background:#333;border-radius:3px}
`}</style>
{/* Header */}
<div style={{ padding: "28px 24px 20px", textAlign: "center", borderBottom: "1px solid #F0D06020", background: "linear-gradient(180deg,#14191F,transparent)", position: "relative" }}>
<div style={{ fontSize: 11, letterSpacing: 6, color: "#F0D060", marginBottom: 4, fontFamily: "'Courier Prime'" }}>♠ ♥ ♦ ♣</div>
<h1 style={{ fontSize: 26, fontWeight: 700, margin: 0, background: "linear-gradient(135deg,#F0D060,#D4A017,#F0D060)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", letterSpacing: 2 }}>TOURNAMENT DIRECTOR</h1>
<div style={{ fontSize: 11, letterSpacing: 4, color: "#888", marginTop: 4, fontFamily: "'Courier Prime'" }}>TEXAS HOLD'EM · SHARED SESSION</div>
{status && <div style={{ position: "absolute", top: 10, right: 14, fontSize: 10, color: status.includes("fail") ? "#E74C3C" : "#2ECC71", fontFamily: "'Courier Prime'", letterSpacing: 1, animation: "fadeIn 0.2s ease" }}>{status}</div>}
</div>
{/* Tabs */}
<div style={{ display: "flex", borderBottom: "1px solid #F0D06020", background: "#0D1117" }}>
{[["setup","Setup"],["blinds","Blinds"],["timer","Play"],["rules","Rules"]].map(([id,l]) => (
<button key={id} onClick={() => setTab(id)} style={{
flex: 1, padding: "12px 0", border: "none", cursor: "pointer", fontFamily: "'Courier Prime'", fontSize: 12, letterSpacing: 2,
background: tab === id ? "#1a1f28" : "transparent", color: tab === id ? "#F0D060" : "#666",
borderBottom: tab === id ? "2px solid #F0D060" : "2px solid transparent",
}}>{l}</button>
))}
</div>
<div style={{ padding: 20, maxWidth: 600, margin: "0 auto" }}>
{/* ════ SETUP ════ */}
{tab === "setup" && (
<div style={{ animation: "fadeIn 0.3s ease" }}>
<Sec t="Players">
<div style={{ display: "flex", alignItems: "center", gap: 16, justifyContent: "center" }}>
<button onClick={() => setPlayers(Math.max(2, players - 1))} style={sBtn}>-</button>
<span style={{ fontSize: 36, fontWeight: 700, color: "#F0D060", fontFamily: "'Courier Prime'", minWidth: 50, textAlign: "center" }}>{players}</span>
<button onClick={() => setPlayers(Math.min(20, players + 1))} style={sBtn}>+</button>
</div>
</Sec>
<Sec t="Chip Inventory">
{chips.map((c, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", borderRadius: 8, marginBottom: 6, background: "#1a1f2880", border: "1px solid #ffffff08" }}>
<ChipIcon color={c.color} size={28} />
<span style={{ fontSize: 13, color: "#999", width: 50, fontFamily: "'Courier Prime'" }}>{CHIP_COLORS[c.color]?.label}</span>
<div style={{ flex: 1, display: "flex", gap: 8 }}>
<InF l="Value" v={c.value} onChange={v => updChip(i, "value", v)} />
<InF l="Count" v={c.count} onChange={v => updChip(i, "count", v)} />
</div>
</div>
))}
</Sec>
<Sec t="Tournament Summary">
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
<SB l="Total Pool" v={pool.toLocaleString()} />
<SB l="Starting Stack" v={total.toLocaleString()} />
<SB l="Starting BB" v={blinds[0]?.bb || 0} />
<SB l="BBs Deep" v={Math.round(total / (blinds[0]?.bb || 1))} />
</div>
{total < perP && <div style={{ marginTop: 10, padding: "8px 12px", borderRadius: 6, background: "#C0392B20", border: "1px solid #C0392B40", fontSize: 12, color: "#E88", fontFamily: "'Courier Prime'" }}>⚠ Can't evenly distribute {perP}/player. Each gets {total} ({perP - total} short).</div>}
</Sec>
<Sec t="Chip Distribution Per Player">
{dist.map((d, i) => (
<div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 0", borderBottom: "1px solid #ffffff06", animation: `slideIn 0.3s ease ${i * 0.05}s both` }}>
<ChipIcon color={d.color} size={24} />
<span style={{ fontFamily: "'Courier Prime'", fontSize: 13, color: "#ccc", flex: 1 }}>{CHIP_COLORS[d.color]?.label} (${d.value})</span>
<span style={{ fontFamily: "'Courier Prime'", fontSize: 16, color: "#F0D060", fontWeight: 700 }}>*{d.per}</span>
<span style={{ fontFamily: "'Courier Prime'", fontSize: 12, color: "#666" }}>= ${(d.per * d.value).toLocaleString()}</span>
</div>
))}
{left.length > 0 && <div style={{ marginTop: 12, fontSize: 11, color: "#666", fontFamily: "'Courier Prime'" }}>Bank: {left.map(l => `${l.count} ${CHIP_COLORS[l.color]?.label}`).join(", ")} -- use for color-ups.</div>}
</Sec>
<button onClick={reset} style={{ width: "100%", padding: "12px", borderRadius: 8, marginTop: 8, background: "#C0392B15", border: "1px solid #C0392B30", color: "#C0392B", fontFamily: "'Courier Prime'", fontSize: 12, cursor: "pointer", letterSpacing: 2 }}>RESET EVERYTHING</button>
</div>
)}
{/* ════ BLINDS ════ */}
{tab === "blinds" && (
<div style={{ animation: "fadeIn 0.3s ease" }}>
<Sec t="Blind Structure">
<div style={{ fontSize: 12, color: "#888", marginBottom: 14, fontFamily: "'Courier Prime'", lineHeight: 1.6 }}>Antes begin at Level 5. Durations shorten as levels rise.</div>
{blinds.map((b, i) => (
<div key={i} style={{
display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", marginBottom: 6, borderRadius: 8,
background: i === lvl ? "#F0D06015" : "#1a1f2850", border: i === lvl ? "1px solid #F0D06030" : "1px solid transparent",
animation: `slideIn 0.3s ease ${i * 0.03}s both`,
}}>
<span style={{ width: 28, height: 28, borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, fontFamily: "'Courier Prime'", fontWeight: 700, flexShrink: 0, background: i === lvl ? "#F0D060" : "#333", color: i === lvl ? "#111" : "#888" }}>{b.level}</span>
<div style={{ flex: 1 }}>
<div style={{ fontFamily: "'Courier Prime'", fontSize: 14, color: "#E8E2D6" }}>{b.sb.toLocaleString()} / {b.bb.toLocaleString()}</div>
{b.ante > 0 && <div style={{ fontSize: 11, color: "#F0D06090", fontFamily: "'Courier Prime'" }}>Ante: {b.ante.toLocaleString()}</div>}
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: 11, color: "#666", fontFamily: "'Courier Prime'" }}>{cDur[i] ?? b.duration} min</div>
<div style={{ fontSize: 10, color: "#555", fontFamily: "'Courier Prime'" }}>{Math.round(total / b.bb)} BBs</div>
</div>
</div>
))}
</Sec>
</div>
)}
{/* ════ PLAY ════ */}
{tab === "timer" && (
<div style={{ animation: "fadeIn 0.3s ease" }}>
<div style={{ textAlign: "center", padding: "24px 16px", borderRadius: 12, background: "linear-gradient(135deg,#1a1f28,#141820)", border: "1px solid #F0D06018", marginBottom: 16 }}>
<div style={{ fontSize: 10, letterSpacing: 4, color: "#888", fontFamily: "'Courier Prime'", marginBottom: 4 }}>LEVEL {cb.level}</div>
<div style={{ fontSize: 40, fontWeight: 700, color: "#F0D060", fontFamily: "'Courier Prime'", letterSpacing: 2 }}>{cb.sb.toLocaleString()} / {cb.bb.toLocaleString()}</div>
{cb.ante > 0 && <div style={{ fontSize: 16, color: "#F0D06080", fontFamily: "'Courier Prime'", marginTop: 2 }}>Ante: {cb.ante.toLocaleString()}</div>}
<div style={{ marginTop: 16, display: "flex", justifyContent: "center", gap: 24, fontSize: 11, color: "#666", fontFamily: "'Courier Prime'" }}>
<span>Avg Stack: {avg.toLocaleString()}</span><span>{bbs} BBs</span>
</div>
</div>
<div style={{ padding: "20px 16px", borderRadius: 12, background: "#1a1f2860", border: "1px solid #ffffff08", marginBottom: 16 }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }}>
<span style={{ fontSize: 10, letterSpacing: 3, color: "#888", fontFamily: "'Courier Prime'" }}>ROUND TIMER</span>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ fontSize: 10, color: "#666", fontFamily: "'Courier Prime'" }}>MIN:</span>
<input type="number" value={dur} onChange={e => { setCDur({ ...cDur, [lvl]: Math.max(1, parseInt(e.target.value) || 1) }); setTSec(null); }} style={{ ...nIn, width: 40, textAlign: "center" }} />
</div>
</div>
<Timer duration={dur} isRunning={running} savedSec={tSec}
onTick={s => setTSec(s)} onToggle={() => setRunning(!running)}
onReset={() => { setRunning(false); setTSec(null); }}
onComplete={() => { setRunning(false); setTSec(null); if (lvl < 14) setLvl(lvl + 1); }}
/>
</div>
<div style={{ display: "flex", gap: 10, marginBottom: 16 }}>
<button disabled={lvl === 0} onClick={() => { setLvl(lvl - 1); setRunning(false); setTSec(null); }} style={{ ...nBtn, opacity: lvl === 0 ? 0.3 : 1 }}>← Prev</button>
<button disabled={lvl >= 14} onClick={() => { setLvl(lvl + 1); setRunning(false); setTSec(null); }} style={{ ...nBtn, opacity: lvl >= 14 ? 0.3 : 1 }}>Next →</button>
</div>
{lvl < 14 && (
<div style={{ padding: "12px 14px", borderRadius: 8, background: "#1a1f2840", border: "1px solid #ffffff06", display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<span style={{ fontSize: 11, color: "#666", fontFamily: "'Courier Prime'" }}>NEXT:</span>
<span style={{ fontFamily: "'Courier Prime'", fontSize: 14, color: "#ccc" }}>
{blinds[lvl + 1].sb.toLocaleString()} / {blinds[lvl + 1].bb.toLocaleString()}
{blinds[lvl + 1].ante > 0 && ` (ante ${blinds[lvl + 1].ante})`}
</span>
</div>
)}
<Sec t="Players Remaining">
<div style={{ display: "flex", alignItems: "center", gap: 16, justifyContent: "center" }}>
<button onClick={() => setPRem(Math.min(players, pRem + 1))} style={sBtn}>+</button>
<div style={{ textAlign: "center" }}>
<span style={{ fontSize: 32, fontWeight: 700, color: "#F0D060", fontFamily: "'Courier Prime'" }}>{pRem}</span>
<span style={{ fontSize: 14, color: "#666", fontFamily: "'Courier Prime'" }}> / {players}</span>
</div>
<button onClick={elim} style={{ ...sBtn, borderColor: pRem <= 1 ? "#333" : "#C0392B50", color: pRem <= 1 ? "#555" : "#C0392B" }}>✕</button>
</div>
{pRem <= Math.ceil(players / 2) && pRem > 2 && <div style={{ marginTop: 10, padding: "8px 12px", borderRadius: 6, background: "#F0D06010", border: "1px solid #F0D06020", fontSize: 12, color: "#F0D060", fontFamily: "'Courier Prime'", textAlign: "center" }}>Consider a chip color-up -- remove lowest denomination.</div>}
{pRem === 2 && <div style={{ marginTop: 10, padding: "10px 12px", borderRadius: 6, background: "#1E844920", border: "1px solid #1E844940", fontSize: 13, color: "#5DADE2", fontFamily: "'Courier Prime'", textAlign: "center" }}>★ HEADS-UP! Dealer posts the small blind, acts first pre-flop.</div>}
{pRem === 1 && <div style={{ marginTop: 10, padding: "14px", borderRadius: 8, background: "linear-gradient(135deg,#F0D06020,#D4A01720)", border: "1px solid #F0D06040", fontSize: 16, color: "#F0D060", fontFamily: "'Courier Prime'", textAlign: "center", fontWeight: 700, letterSpacing: 2 }}>🏆 WINNER 🏆</div>}
</Sec>
{eLog.length > 0 && (
<Sec t="Elimination Log">
{[...eLog].reverse().map((e, i) => (
<div key={i} style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: "1px solid #ffffff06", fontFamily: "'Courier Prime'", fontSize: 12, color: "#888" }}>
<span>{ord(e.place)} place</span><span>Lvl {e.lvl} · {e.time}</span>
</div>
))}
</Sec>
)}
<Sec t="Payout Structure">
<div style={{ fontSize: 12, color: "#999", fontFamily: "'Courier Prime'", lineHeight: 1.8 }}>
{players >= 7 ? <><PR l="1st" p="50%" c="#F0D060" /><PR l="2nd" p="30%" c="#ccc" /><PR l="3rd" p="20%" c="#A67C52" /></> : players >= 4 ? <><PR l="1st" p="60%" c="#F0D060" /><PR l="2nd" p="40%" c="#ccc" /></> : <PR l="1st" p="Winner Takes All" c="#F0D060" />}
</div>
</Sec>
</div>
)}
{/* ════ RULES ════ */}
{tab === "rules" && (
<div style={{ animation: "fadeIn 0.3s ease" }}>
<Sec t="Tournament Rules">
<RB t="Buy-In & Stacks" d={`All players start with ${total.toLocaleString()} chips. No rebuys unless agreed before the start.`} />
<RB t="Blind Increases" d="Blinds go up on a timer. New blinds take effect at the START of the next hand -- never mid-hand." />
<RB t="Antes" d="Antes kick in at Level 5. Every player posts an ante before each hand to grow pots and force action." />
<RB t="Dealing" d="Dealer button rotates clockwise. Left of dealer posts SB, next posts BB. Burn a card before flop, turn, and river." />
<RB t="Betting" d="Minimum raise = previous raise. All-in for less than a full raise doesn't reopen action. No string bets." />
<RB t="Side Pots" d="When a player is all-in and others keep betting, create a side pot. The all-in player competes only for the main pot." />
<RB t="Color-Ups" d="When small chips fall below the SB, race them off: convert to larger denominations, rounding odd chips up." />
<RB t="Showdown" d="Last aggressor shows first. No final-street bet? First clockwise from dealer shows. Players may muck." />
<RB t="Heads-Up" d="With 2 players, dealer posts SB and acts first pre-flop, but second on all later streets." />
<RB t="Etiquette" d="No slow-rolling. No discussing active hands. Act in turn. Large denominations in front and visible." />
</Sec>
<Sec t="How Saving Works">
<div style={{ padding: "12px 14px", borderRadius: 8, background: "#1E844915", border: "1px solid #1E844930", fontSize: 13, color: "#aaa", fontFamily: "'Courier Prime'", lineHeight: 1.7 }}>
Your tournament auto-saves to <span style={{ color: "#5DADE2" }}>shared storage</span>. Close the app, share the link, or come back later -- your setup, current level, timer, eliminations, and all settings will be right where you left them. Hit <span style={{ color: "#C0392B" }}>RESET EVERYTHING</span> on the Setup tab to start fresh.
</div>
</Sec>
</div>
)}
</div>
</div>
);
}
function ord(n) { const s = ["th","st","nd","rd"], v = n % 100; return n + (s[(v-20)%10]||s[v]||s[0]); }
function Sec({ t, children }) { return <div style={{ marginBottom: 20 }}><div style={{ fontSize: 10, letterSpacing: 4, color: "#F0D06090", fontFamily: "'Courier Prime'", marginBottom: 12, paddingBottom: 6, borderBottom: "1px solid #F0D06010" }}>{t.toUpperCase()}</div>{children}</div>; }
function SB({ l, v }) { return <div style={{ padding: "12px 14px", borderRadius: 8, background: "#1a1f2880", border: "1px solid #ffffff08", textAlign: "center" }}><div style={{ fontSize: 22, fontWeight: 700, color: "#F0D060", fontFamily: "'Courier Prime'" }}>{v}</div><div style={{ fontSize: 10, color: "#888", fontFamily: "'Courier Prime'", letterSpacing: 1, marginTop: 2 }}>{l.toUpperCase()}</div></div>; }
function InF({ l, v, onChange }) { return <div style={{ flex: 1 }}><div style={{ fontSize: 9, color: "#555", fontFamily: "'Courier Prime'", marginBottom: 2, letterSpacing: 1 }}>{l.toUpperCase()}</div><input type="number" value={v} onChange={e => onChange(e.target.value)} style={nIn} /></div>; }
function RB({ t, d }) { return <div style={{ padding: "12px 14px", marginBottom: 8, borderRadius: 8, background: "#1a1f2850", borderLeft: "3px solid #F0D06030" }}><div style={{ fontFamily: "'Courier Prime'", fontSize: 13, color: "#F0D060", marginBottom: 4, fontWeight: 700 }}>{t}</div><div style={{ fontSize: 13, color: "#bbb", lineHeight: 1.6, fontFamily: "'Libre Baskerville',serif" }}>{d}</div></div>; }
function PR({ l, p, c }) { return <div style={{ display: "flex", justifyContent: "space-between", padding: "6px 0", borderBottom: "1px solid #ffffff06" }}><span>{l} Place</span><span style={{ color: c }}>{p}</span></div>; }
const sBtn = { width: 44, height: 44, borderRadius: "50%", background: "linear-gradient(135deg,#2C2C2C,#1a1a1a)", border: "1px solid #F0D06030", color: "#F0D060", fontSize: 22, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'Courier Prime'" };
const nBtn = { flex: 1, padding: "12px", borderRadius: 8, background: "linear-gradient(135deg,#1a1f28,#141820)", border: "1px solid #F0D06025", color: "#F0D060", fontFamily: "'Courier Prime'", fontSize: 12, cursor: "pointer", letterSpacing: 1, fontWeight: 600 };
const nIn = { width: "100%", padding: "6px 8px", borderRadius: 6, background: "#0D1117", border: "1px solid #333", color: "#E8E2D6", fontFamily: "'Courier Prime'", fontSize: 14, outline: "none" };
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **Do NOT import external JS libraries via script tags** — they will be blocked. Include library code inline or use a CDN link in a `<link>` tag for CSS only.
6. **Keep total code under 80KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained JSX file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/GigaChad/tournament-director-fork */