Spaces:
Running
Running
| <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 dog fish bird hamster ..."></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> | |