# Create an improved, organized project structure with public index, CSS, and JS. import os, textwrap, zipfile, json base = "/mnt/data/sorteos_krisb3_public" css_dir = os.path.join(base, "css") js_dir = os.path.join(base, "js") img_dir = os.path.join(base, "imagenes") os.makedirs(css_dir, exist_ok=True) os.makedirs(js_dir, exist_ok=True) os.makedirs(img_dir, exist_ok=True) index_html = """ Sorteos KrisB3

$200,000 MXN
1 DE JULIO 2025

LISTA DE BOLETOS ABAJO
Premio de $200,000 MXN

1 BOLETO POR $4

2 BOLETOS POR $8

3 BOLETOS POR $12

5 BOLETOS POR $20

10 BOLETOS POR $40

Con tu boleto liquidado participas por: $200,000 MXN – 1 JULIO

Se Juega En Base LOTERÍA NACIONAL TRIS CLÁSICO

HAZ CLICK ABAJO EN TU NÚMERO DE LA SUERTE
Para eliminar un boleto seleccionado, haz click sobre él.
""" main_css = """/* ===== Variables y base ===== */ :root { --verde: #00C853; --dark:#1f1f1f; --danger:#d32f2f; --accent:#00C853; } * { box-sizing: border-box; } body { margin: 0; padding: 0; font-family: Arial, sans-serif; background: #fff; color: #000; } /* Barras decorativas */ .bar-green, .bar-green2 { background: var(--verde); height: 10px; } /* Cabecera */ .bar-dark { background: var(--dark); color: #fff; display: flex; justify-content: space-between; align-items: center; padding: 0 15px; height: 30px; font-weight: bold; font-size: 15px; white-space: nowrap; } .bar-dark .left, .bar-dark .right { flex: 0 0 auto; } @media (max-width: 480px) { .bar-dark { font-size: 14px; padding: 0 10px; } } /* Premio */ .bar-black { background: var(--dark); display: flex; justify-content: center; align-items: center; min-height: 90px; } .bar-black .prize-text { color: #fff; font-size: 1.5em; font-weight: bold; text-align: center; line-height: 1.5; margin: .75rem 0; } /* Flechas */ .list-heading { display: flex; justify-content: center; align-items: center; margin: 20px 0; } .list-heading .triangle { width: 0; height: 0; border-left: 13px solid transparent; border-right: 13px solid transparent; border-top: 20px solid var(--verde); } .list-heading .title { margin: 0 6px; font-size: 1.5em; font-weight: bold; color: #000; } /* Imagen */ .prize-image { text-align: center; margin: 5px 3px 20px; } .prize-image img { width: 97%; max-width: 400px; border: 4px solid var(--verde); border-radius: 8px; } /* Tarifas */ .bar-black2 { background: var(--dark); text-align: center; padding: 20px 0; margin: 0; } .bar-black2 p { margin: 8px 0; color: #fff; font-size: 1.2em; font-weight: bold; } /* Info */ .info-section { text-align: center; margin: 20px 0 40px; } .info-section p { margin: 16px 0; font-size: 1.4em; font-weight: bold; line-height: 1.3; } .info-section .highlight { color: var(--verde); } .info-section .uppercase { text-transform: uppercase; } /* Instrucción */ .bar-black3 { background: var(--dark); text-align: center; padding: 20px 0; margin: 0; } .bar-black3 .text3 { color: #fff; font-size: 1.3em; font-weight: bold; line-height: 1.2; } /* Apartado */ .bar-black4 { background: var(--dark); display: none; flex-direction: column; align-items: center; padding: 20px 15px; margin: 0; } .bar-black4 .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-bottom: 12px; } .bar-black4 .selected-list { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-bottom: 8px; } .bar-black4 .selected-list span { background: var(--verde); color: #fff; padding: 8px 12px; border-radius: 4px; cursor: pointer; } .bar-black4 .info-apartar { color: #ffeb3b; text-align: center; font-size: 1.05em; font-weight: bold; margin-top: 8px; } /* Botones */ button { border-radius: 6px; cursor: pointer; border: 2px solid var(--verde); font-weight: 700; } .btn-primary { background: var(--verde); color: #fff; padding: 10px 16px; border: none; } .btn-secondary { background: #fff; color: #000; padding: 10px 16px; } .btn-danger { background: #d32f2f; color: #fff; border-color: #d32f2f; padding: 12px 24px; } .btn-accent { background: var(--accent); color: #fff; padding: 12px 18px; border: none; } /* Búsqueda */ .search-section { text-align: center; margin: 40px 0 60px; } .search-row { display: inline-flex; gap: 8px; align-items: center; } .search-section input { width: 80%; max-width: 300px; padding: 12px; font-size: 1.05em; text-align: center; border: 2px solid var(--verde); border-radius: 6px; } #luckyPrompt { display: none; margin: 20px auto; padding: 16px; width: 80%; max-width: 300px; border: 2px solid var(--verde); border-radius: 6px; text-align: center; font-size: 1.1em; font-weight: bold; color: var(--verde); cursor: pointer; } #slotAnim { display: none; margin: 20px auto; width: 80px; height: 80px; } #slotAnim img { width: 100%; height: auto; } #searchResult, #luckyResult { margin: 16px auto; font-size: 1.2em; font-weight: bold; width: 80%; max-width: 420px; text-align: center; } .available { color: var(--verde); } .unavailable { color: #e53935; } #chooseBtn, #luckyChoose { display: none; margin: 0 auto 20px; } /* Grid de boletos */ .ticket-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(56px, 1fr)); gap: 8px; margin: 20px auto; padding: 0 20px; max-width: 980px; } .ticket { background: #fff; border: 2px solid var(--verde); border-radius: 6px; padding: 10px 0; text-align: center; color: #000; cursor: pointer; user-select: none; outline: none; } .ticket:hover { filter: brightness(0.98); } .ticket:focus { box-shadow: 0 0 0 3px rgba(0,200,83,.35); } .ticket.selected { background: #000; color: #fff; } .ticket.occupied { background: #000; color: #fff; cursor: not-allowed; opacity: .85; } /* Overlay Sub-Menú */ #luckyMenuOverlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); justify-content: center; align-items: center; z-index: 1000; } #luckyMenu { background: #fff; border: 3px solid var(--verde); border-radius: 12px; width: 90%; max-width: 320px; max-height: 80%; padding: 16px; box-sizing: border-box; position: relative; display: flex; flex-direction: column; } #luckyMenu .closeBtn { position: absolute; top: 8px; right: 8px; background: #e53935; color: #fff; border: none; width: 32px; height: 32px; border-radius: 50%; font-size: 1.2em; line-height: 32px; cursor: pointer; } .select-header { display: flex; align-items: center; border: 2px solid var(--verde); border-radius: 8px; margin: 0 0 16px; padding: 8px; gap: 8px; } .select-label { font-weight: bold; } #openSelect { flex: 1; text-align: left; } #luckyMenuList { list-style: none; margin: 0; padding: 0; overflow-y: auto; flex: 1; } #luckyMenuList.hidden { display: none; } #luckyMenuList li { padding: 10px 14px; border-bottom: 1px solid #ddd; cursor: pointer; display: flex; justify-content: space-between; align-items: center; } #luckyMenuList li:last-child { border-bottom: none; } #luckyMenuList li:hover { background: #f5f5f5; } #luckyMenuList li .value { font-weight: bold; } /* Utilidades */ .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } """ app_js = """'use strict'; /* ====== Configuración ====== */ const TOTAL_TICKETS = 500; /** Cambia este array por una llamada a tu backend si quieres hacerlo dinámico */ const boletosOcupados = new Set([2, 5, 10, 25, 48, 101]); /* ====== Referencias DOM ====== */ const input = document.getElementById('searchInput'); const searchRes = document.getElementById('searchResult'); const chooseBtn = document.getElementById('chooseBtn'); const luckyBtn = document.getElementById('luckyBtn'); const luckyPrompt = document.getElementById('luckyPrompt'); const slotAnim = document.getElementById('slotAnim'); const luckyRes = document.getElementById('luckyResult'); const luckyChoose = document.getElementById('luckyChoose'); const apartSect = document.getElementById('apartSection'); const apartBtn = document.getElementById('apartBtn'); const clearBtn = document.getElementById('clearBtn'); const copyBtn = document.getElementById('copyBtn'); const apartList = document.getElementById('apartList'); const menuOverlay = document.getElementById('luckyMenuOverlay'); const menuList = document.getElementById('luckyMenuList'); const openSelect = document.getElementById('openSelect'); const closeBtn = document.querySelector('#luckyMenu .closeBtn'); const ticketGrid = document.getElementById('ticketGrid'); /* Estado */ let currentCount = 0; const selected = new Set(); let lastLucky = []; /* Util */ const numberInRange = (n) => Number.isInteger(n) && n >= 1 && n <= TOTAL_TICKETS; function init() { // Renderiza la grilla renderTickets(); // Limpia estados iniciales chooseBtn.style.display = 'none'; luckyPrompt.style.display = 'none'; slotAnim.style.display = 'none'; luckyChoose.style.display = 'none'; apartSect.style.display = 'none'; luckyRes.textContent = ''; // Poblar sub-menú al primer uso if (!menuList.childElementCount) { const opts = Array.from({length:10}, (_,i)=> i+1).concat([20,30,40,50,60,70,80,90,100]); for (const n of opts) { const li = document.createElement('li'); li.innerHTML = `${n} Boleto${n>1?'s':''}`; li.addEventListener('click', () => { currentCount = n; menuOverlay.style.display = 'none'; showLuckyPrompt(); }); menuList.append(li); } } // Delegación de eventos para la grilla ticketGrid.addEventListener('click', onTicketClick); ticketGrid.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { const t = e.target; if (t.classList.contains('ticket')) { e.preventDefault(); t.click(); } } }); // Buscar con Enter input.addEventListener('input', resetSearch); input.addEventListener('keydown', (e) => { if (e.key !== 'Enter') return; handleSearch(); }); chooseBtn.addEventListener('click', addSearchToSelection); // Maquinita luckyBtn.addEventListener('click', openLuckyMenu); closeBtn.addEventListener('click', () => menuOverlay.style.display = 'none'); menuOverlay.addEventListener('click', (e) => { if (e.target === menuOverlay) menuOverlay.style.display = 'none'; }); openSelect.addEventListener('click', () => { const hidden = menuList.classList.toggle('hidden'); openSelect.setAttribute('aria-expanded', (!hidden).toString()); }); luckyPrompt.addEventListener('click', runLucky); luckyPrompt.addEventListener('keydown', (e)=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); runLucky(); }}); luckyChoose.addEventListener('click', applyLuckySelection); // Apartar / limpiar / copiar apartBtn.addEventListener('click', sendWhatsApp); clearBtn.addEventListener('click', clearSelection); copyBtn.addEventListener('click', copySelection); } /* Renderiza los 500 boletos */ function renderTickets() { const frag = document.createDocumentFragment(); for (let i=1;i<=TOTAL_TICKETS;i++) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ticket'; btn.textContent = i; btn.dataset.num = i; btn.setAttribute('role','gridcell'); btn.setAttribute('aria-label', `Boleto ${i}`); if (boletosOcupados.has(i)) { btn.classList.add('occupied'); btn.disabled = true; btn.setAttribute('aria-disabled','true'); } frag.appendChild(btn); } ticketGrid.innerHTML = ''; ticketGrid.appendChild(frag); } /* Click en boleto */ function onTicketClick(e) { const t = e.target; if (!t.classList.contains('ticket') || t.classList.contains('occupied')) return; const n = parseInt(t.dataset.num, 10); if (selected.has(n)) { selected.delete(n); t.classList.remove('selected'); removePill(n); } else { selected.add(n); t.classList.add('selected'); addPill(n); } toggleApartSection(); } /* Píldoras de selección */ function addPill(n) { if ([...apartList.children].some(sp => parseInt(sp.dataset.num,10) === n)) return; const sp = document.createElement('span'); sp.dataset.num = String(n); sp.textContent = n; sp.title = 'Quitar'; sp.addEventListener('click', () => { selected.delete(n); const td = ticketGrid.querySelector(`[data-num="${n}"]`); if (td) td.classList.remove('selected'); sp.remove(); toggleApartSection(); }); apartList.append(sp); } function removePill(n) { const pill = [...apartList.children].find(sp => parseInt(sp.dataset.num,10) === n); if (pill) pill.remove(); } function toggleApartSection() { apartSect.style.display = selected.size ? 'flex' : 'none'; } /* Buscar */ function resetSearch() { searchRes.innerHTML = ''; chooseBtn.style.display = 'none'; } function handleSearch() { resetSearch(); const n = parseInt(input.value, 10); if (!numberInRange(n)) { searchRes.innerHTML = '❌ Número inválido'; return; } if (boletosOcupados.has(n)) { searchRes.innerHTML = '❌ Número no disponible'; return; } searchRes.innerHTML = '✅ Número disponible'; chooseBtn.style.display = 'inline-block'; } function addSearchToSelection() { const n = parseInt(input.value, 10); if (!numberInRange(n) || boletosOcupados.has(n)) return; const btn = ticketGrid.querySelector(`[data-num="${n}"]`); if (btn && !btn.classList.contains('selected')) btn.click(); input.value = ''; resetSearch(); } /* Maquinita */ function openLuckyMenu() { resetSearch(); luckyPrompt.style.display = 'none'; slotAnim.style.display = 'none'; luckyRes.textContent = ''; luckyChoose.style.display = 'none'; menuOverlay.style.display = 'flex'; } function showLuckyPrompt() { luckyPrompt.textContent = `HAZ CLICK AQUÍ PARA GENERAR ${currentCount} BOLETO${currentCount>1?'S':''} AL AZAR!`; luckyPrompt.style.display = 'block'; luckyPrompt.setAttribute('aria-hidden','false'); luckyPrompt.focus({preventScroll:true}); } function runLucky() { luckyPrompt.style.display = 'none'; slotAnim.style.display = 'block'; const available = []; for (let i=1;i<=TOTAL_TICKETS;i++) if (!boletosOcupados.has(i)) available.push(i); // Evita pedir más de los disponibles const toPick = Math.min(currentCount, available.length); setTimeout(() => { slotAnim.style.display = 'none'; // Copia del pool para no repetir const pool = available.slice(); const sel = []; while (sel.length < toPick && pool.length) { const idx = Math.floor(Math.random() * pool.length); sel.push(pool.splice(idx, 1)[0]); } sel.sort((a,b)=>a-b); lastLucky = sel; luckyRes.innerHTML = `🎲 ${sel.join(', ')}`; luckyChoose.style.display = 'inline-block'; }, 1200); } function applyLuckySelection() { for (const n of lastLucky) { const btn = ticketGrid.querySelector(`[data-num="${n}"]`); if (btn && !btn.classList.contains('selected')) btn.click(); } luckyRes.textContent = ''; luckyChoose.style.display = 'none'; } /* WhatsApp */ function sendWhatsApp() { const nums = [...apartList.children].map(sp => sp.textContent.trim()); if (!nums.length) return; const waNumber = apartSect.dataset.wa || ''; const msg = `Hola! Quiero apartar: ${nums.join(', ')}`; const url = `https://wa.me/${encodeURIComponent(waNumber)}?text=${encodeURIComponent(msg)}`; window.open(url, '_blank', 'noopener'); } /* Auxiliares */ function clearSelection() { selected.clear(); apartList.innerHTML = ''; ticketGrid.querySelectorAll('.ticket.selected').forEach(el => el.classList.remove('selected')); toggleApartSection(); } async function copySelection() { const nums = [...apartList.children].map(sp => sp.textContent.trim()); if (!nums.length) return; try { await navigator.clipboard.writeText(nums.join(', ')); copyBtn.textContent = '¡Copiado!'; setTimeout(()=> copyBtn.textContent = 'COPIAR', 1000); } catch(e) { alert('Copia manual: ' + nums.join(', ')); } } /* Iniciar */ document.addEventListener('DOMContentLoaded', init); """ readme = """# Sorteos KrisB3 (público) Este paquete contiene la versión pública optimizada de tu sitio. ## Estructura - `index.html` — interfaz pública (sin login) - `css/main.css` — estilos - `js/app.js` — lógica del cliente - `imagenes/` — coloca aquí `premio.jpeg` y `slot.gif` ## Personalización rápida - Cambia el número de WhatsApp en el atributo `data-wa` del elemento `#apartSection` en `index.html`. - Edita el array de `boletosOcupados` en `js/app.js` o reemplázalo por una llamada a tu backend. ## Admin (recomendado) Crea una carpeta `admin/` aparte, protégela con contraseña (htaccess) y gestiona ahí lo privado. """ # Write files with open(os.path.join(base, "index.html"), "w", encoding="utf-8") as f: f.write(index_html) with open(os.path.join(css_dir, "main.css"), "w", encoding="utf-8") as f: f.write(main_css) with open(os.path.join(js_dir, "app.js"), "w", encoding="utf-8") as f: f.write(app_js) with open(os.path.join(base, "README.md"), "w", encoding="utf-8") as f: f.write(readme) # Create a zip for download zip_path = "/mnt/data/sorteos_krisb3_public.zip" with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z: for root, dirs, files in os.walk(base): for name in files: full = os.path.join(root, name) arc = os.path.relpath(full, start=os.path.dirname(base)) z.write(full, arcname=arc) zip_path