# 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
MÉTODOS DE PAGO
BOLETOS DISPONIBLES
$200,000 MXN
1 DE JULIO 2025
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