bingo-card-generator / index.html
bingwow's picture
Initial: bingo card generator with BingWow CTA
7b30f44
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bingo Card Generator — by BingWow</title>
<meta name="description" content="Generate shuffled bingo cards (3×3, 4×4, 5×5) from any list of items. Free, in-browser, no signup.">
<link rel="canonical" href="https://huggingface.co/spaces/bingwow/bingo-card-generator">
<style>
:root {
--bg: #fff7ed;
--surface: #ffffff;
--ink: #1f2937;
--muted: #6b7280;
--accent: #db2777;
--accent-hover: #be185d;
--border: #fde6c8;
--claimed: linear-gradient(135deg, #fbbf24, #f59e0b);
--shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.04);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
}
.container {
max-width: 720px;
margin: 0 auto;
padding: 32px 20px 60px;
}
header { text-align: center; margin-bottom: 28px; }
h1 {
margin: 0 0 8px;
font-size: 2rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.subtitle { color: var(--muted); margin: 0; font-size: 1rem; }
.subtitle a { color: var(--accent); text-decoration: none; font-weight: 600; }
.subtitle a:hover { text-decoration: underline; }
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
box-shadow: var(--shadow);
margin-bottom: 20px;
}
label { display: block; font-weight: 600; margin-bottom: 8px; font-size: 0.9rem; }
textarea, select, input[type="text"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-family: inherit;
font-size: 0.95rem;
background: #fff;
color: var(--ink);
}
textarea { min-height: 120px; resize: vertical; }
textarea:focus, select:focus, input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(219, 39, 119, 0.15);
}
.row { display: flex; gap: 16px; align-items: flex-end; flex-wrap: wrap; }
.row > div { flex: 1; min-width: 160px; }
.pills { display: flex; gap: 8px; flex-wrap: wrap; }
.pill {
padding: 6px 12px;
border: 1px solid var(--border);
background: #fff;
border-radius: 999px;
cursor: pointer;
font-size: 0.85rem;
color: var(--muted);
transition: all 0.15s;
}
.pill:hover { border-color: var(--accent); color: var(--accent); }
button.primary {
background: var(--accent);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
transition: background 0.15s;
width: 100%;
}
button.primary:hover { background: var(--accent-hover); }
button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
.grid-controls { display: flex; gap: 8px; }
.grid-btn {
flex: 1;
padding: 8px 12px;
border: 2px solid #e5e7eb;
background: #fff;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
color: var(--muted);
font-size: 0.9rem;
}
.grid-btn.active {
border-color: var(--accent);
color: var(--accent);
background: rgba(219, 39, 119, 0.05);
}
.error { color: #dc2626; font-size: 0.85rem; margin-top: 8px; min-height: 1.2em; }
.card {
display: grid;
gap: 6px;
background: #fff;
padding: 16px;
border-radius: 12px;
border: 1px solid var(--border);
box-shadow: var(--shadow);
margin-bottom: 20px;
aspect-ratio: 1;
}
.cell {
background: #fff5ec;
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 8px;
font-size: 0.85rem;
line-height: 1.2;
cursor: pointer;
user-select: none;
word-break: break-word;
transition: transform 0.1s;
}
.cell:hover { transform: scale(1.02); }
.cell.claimed {
background: var(--claimed);
color: white;
font-weight: 700;
border-color: transparent;
}
.cell.free {
background: linear-gradient(135deg, #ec4899, #f43f5e);
color: white;
font-weight: 800;
}
.cta {
background: linear-gradient(135deg, #ec4899, #f43f5e);
color: white;
padding: 24px;
border-radius: 12px;
text-align: center;
margin-top: 24px;
}
.cta h2 { margin: 0 0 8px; font-size: 1.4rem; }
.cta p { margin: 0 0 16px; opacity: 0.95; }
.cta a.btn {
display: inline-block;
background: white;
color: var(--accent);
padding: 12px 28px;
border-radius: 8px;
text-decoration: none;
font-weight: 700;
font-size: 1rem;
transition: transform 0.1s;
}
.cta a.btn:hover { transform: translateY(-1px); }
footer {
text-align: center;
color: var(--muted);
font-size: 0.85rem;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
footer a { color: var(--accent); text-decoration: none; }
footer a:hover { text-decoration: underline; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>🎲 Bingo Card Generator</h1>
<p class="subtitle">Free in-browser card maker. Built by <a href="https://bingwow.com" rel="noopener">BingWow</a>.</p>
</header>
<div class="panel">
<label for="preset">Pick a preset (or type your own list below)</label>
<div class="pills" id="presets"></div>
</div>
<div class="panel">
<label for="items">Your items (one per line, need at least 8 for 3×3, 15 for 4×4, or 24 for 5×5)</label>
<textarea id="items" placeholder="cat&#10;dog&#10;fish&#10;bird&#10;hamster&#10;..."></textarea>
<div class="row" style="margin-top:16px;">
<div>
<label>Grid size</label>
<div class="grid-controls">
<button class="grid-btn" data-size="3">3×3</button>
<button class="grid-btn active" data-size="4">4×4</button>
<button class="grid-btn" data-size="5">5×5</button>
</div>
</div>
<div>
<label for="freeText">Free space (5×5 / 3×3 only)</label>
<input type="text" id="freeText" value="FREE" maxlength="12">
</div>
</div>
<div class="error" id="error"></div>
<button class="primary" id="generate" style="margin-top:16px;">Generate Card</button>
</div>
<div id="cardWrap" class="hidden">
<div class="card" id="card"></div>
<p style="text-align:center; color: var(--muted); font-size:0.85rem; margin: -8px 0 0;">Tap cells to mark them. Refresh to shuffle.</p>
</div>
<div class="cta">
<h2>Want real multiplayer?</h2>
<p>BingWow lets multiple players each get their own board, calls out clues live, and works on any device — no signup.</p>
<a class="btn" href="https://bingwow.com" rel="noopener">Try BingWow →</a>
</div>
<footer>
Powered by the open-source <a href="https://www.npmjs.com/package/bingwow" rel="noopener">bingwow</a> npm package
· <a href="https://bingwow.com" rel="noopener">bingwow.com</a>
</footer>
</div>
<script>
const PRESETS = {
"Animals": ["cat","dog","fish","bird","hamster","rabbit","turtle","lizard","snake","frog","parrot","gerbil","pony","goat","duck","pig","cow","sheep","goose","chicken","horse","donkey","llama","alpaca","penguin"],
"Office Words": ["meeting","deadline","budget","spreadsheet","email","Slack","calendar","KPI","quarterly","sync","circle back","stakeholder","bandwidth","pivot","roadmap","ASAP","standup","retro","OKR","one-pager","action item","kickoff","status","milestone","blocker"],
"Halloween": ["pumpkin","ghost","witch","bat","spider","candy","costume","skeleton","vampire","mummy","zombie","haunted","jack-o-lantern","cauldron","broomstick","black cat","trick or treat","spooky","graveyard","full moon","cobweb","monster","werewolf","fog","creepy"],
"Holiday Party": ["ugly sweater","mistletoe","eggnog","secret santa","reindeer","ornament","fireplace","caroling","gingerbread","snowflake","candy cane","wreath","Christmas tree","tinsel","stocking","sleigh","frosty","yule log","hot cocoa","glitter","gift wrap","fairy lights","feast","family photo","awkward toast"],
"Road Trip": ["billboard","rest stop","cow field","gas station","truck stop","construction","detour","license plate game","road sign","mountain","river","tunnel","toll booth","scenic overlook","rest area","motel","sunrise","sunset","podcast","snack","fast food","windshield bug","map","traffic jam","state line"],
"Wedding": ["white dress","first kiss","ring exchange","father-daughter dance","cake cutting","bouquet toss","garter toss","speech","DJ","open bar","photo booth","champagne","centerpiece","aisle","veil","vows","groomsmen","bridesmaids","flower girl","ring bearer","officiant","reception","tuxedo","cocktail hour","getaway car"]
};
// Default
document.getElementById('items').value = PRESETS["Animals"].join('\n');
// Render presets
const presetsEl = document.getElementById('presets');
Object.keys(PRESETS).forEach(name => {
const btn = document.createElement('button');
btn.className = 'pill';
btn.textContent = name;
btn.onclick = () => {
document.getElementById('items').value = PRESETS[name].join('\n');
};
presetsEl.appendChild(btn);
});
// Grid size state
let gridSize = 4;
document.querySelectorAll('.grid-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('.grid-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
gridSize = parseInt(btn.dataset.size, 10);
};
});
// Mirror of the `bingwow` npm package logic
function generateCard({ items, gridSize, freeSpace, freeSpaceText }) {
const totalCells = gridSize * gridSize;
const hasFreeSpace = freeSpace && gridSize % 2 === 1;
const needed = hasFreeSpace ? totalCells - 1 : totalCells;
const pool = items.slice();
for (let i = pool.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[pool[i], pool[j]] = [pool[j], pool[i]];
}
const selected = [];
for (let i = 0; i < needed; i++) selected.push(pool[i % pool.length]);
const grid = [];
let idx = 0;
const center = Math.floor(gridSize / 2);
for (let row = 0; row < gridSize; row++) {
const r = [];
for (let col = 0; col < gridSize; col++) {
if (hasFreeSpace && row === center && col === center) {
r.push({ text: freeSpaceText, free: true });
} else {
r.push({ text: selected[idx++], free: false });
}
}
grid.push(r);
}
return grid;
}
document.getElementById('generate').onclick = () => {
const errEl = document.getElementById('error');
errEl.textContent = '';
const raw = document.getElementById('items').value.trim();
const items = raw.split('\n').map(s => s.trim()).filter(Boolean);
const freeText = document.getElementById('freeText').value.trim() || 'FREE';
if (items.length === 0) {
errEl.textContent = 'Add at least one item.';
return;
}
const needed = gridSize === 3 ? 8 : gridSize === 4 ? 16 : 24;
if (items.length < needed) {
errEl.textContent = `Need at least ${needed} unique items for a ${gridSize}×${gridSize} card (you have ${items.length}). Items will be repeated.`;
}
const grid = generateCard({ items, gridSize, freeSpace: true, freeSpaceText: freeText });
const cardEl = document.getElementById('card');
cardEl.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`;
cardEl.innerHTML = '';
grid.flat().forEach(cell => {
const div = document.createElement('div');
div.className = 'cell' + (cell.free ? ' free claimed' : '');
div.textContent = cell.text;
if (!cell.free) {
div.onclick = () => div.classList.toggle('claimed');
}
cardEl.appendChild(div);
});
document.getElementById('cardWrap').classList.remove('hidden');
};
</script>
</body>
</html>