<meta name=’viewport’ content=’width=device-width, initial-scale=1’/><!doctype html>
<html lang=»es»>
<head>
<meta charset=»utf-8″ />
<meta name=»viewport» content=»width=device-width,initial-scale=1″ />
<title>Rifas El Blaky</title>
<style>
:root{–bg:#0b0b0b;–card:#0f0f0f;–accent:#12b24a;–muted:#9aa0a6;–glass:rgba(255,255,255,0.03)}
body{background:linear-gradient(180deg,#030303 0%, #0b0b0b 100%);color:#fff;font-family:Inter,Helvetica,Arial,sans-serif;margin:0;padding:18px;display:flex;justify-content:center}
.wrap{width:100%;max-width:920px}
header{display:flex;align-items:center;gap:12px}
h1{margin:0;font-size:28px;letter-spacing:0.6px}
.top{display:flex;gap:12px;align-items:center;margin-top:12px}
.card{background:var(–card);padding:14px;border-radius:12px;box-shadow:0 6px 18px rgba(0,0,0,0.6)}
.left{flex:1;margin-right:12px}
.right{width:340px}
label{display:block;font-size:13px;color:var(–muted);margin-top:10px}
input[type=»text»],input[type=»tel»],input[type=»number»],input[type=»email»],select{width:100%;padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);background:transparent;color:#fff;box-sizing:border-box}
button{cursor:pointer;padding:10px;border-radius:8px;border:0;background:var(–accent);color:#051006;font-weight:700}
small{color:var(–muted)}
.preview{width:100%;height:220px;background:var(–glass);border-radius:8px;display:flex;align-items:center;justify-content:center;overflow:hidden}
.preview img{width:100%;height:100%;object-fit:cover}
.meta{display:flex;gap:8px;margin-top:10px}
.meta > div{flex:1}
.boletosArea{margin-top:14px}
.registro{margin-top:12px;max-height:220px;overflow:auto;padding:8px;border-radius:8px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.03))}
.ticket{padding:8px;border-bottom:1px dashed rgba(255,255,255,0.03);display:flex;justify-content:space-between;align-items:center}
.ticket strong{color:var(–accent)}
.controls{display:flex;gap:8px;margin-top:12px}
.payBtn{background:#ffd54a;color:#111}
.download{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(–muted)}
/* Slot machine */
.slotWrap{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:14px}
.reel{width:70px;height:70px;border-radius:8px;background:#070707;border:1px solid rgba(255,255,255,0.03);display:flex;align-items:center;justify-content:center;font-size:20px}
.spin{animation:spin 900ms cubic-bezier(.25,.5,.25,1)}
@keyframes spin{
0%{transform:translateY(0);opacity:1}
100%{transform:translateY(-360px);opacity:1}
}
.footer{font-size:12px;color:var(–muted);margin-top:12px}
.topControls{display:flex;gap:8px;margin-top:8px}
.small{padding:6px 8px;font-size:13px}
.info{font-size:13px;color:var(–muted);margin-top:8px}
</style>
</head>
<body>
<div class=»wrap»>
<header>
<h1>Rifas El Blaky</h1>
</header><div class=»top»>
<div class=»left card»>
<div style=»display:flex;justify-content:space-between;align-items:center»>
<div>
<label>Título</label>
<input id=»titulo» type=»text» value=»Rifas El Blaky»>
</div>
<div style=»width:180px»>
<label>Fecha cierre (opcional)</label>
<input id=»fecha» type=»text» placeholder=»31/12/2025″>
</div>
</div><label>Foto del producto</label>
<div class=»preview card preview» id=»previewBox»>Sube imagen del premio</div>
<input id=»file» type=»file» accept=»image/*»><div class=»meta»>
<div>
<label>Cantidad total de boletos (máx)</label>
<input id=»maxBoletos» type=»number» value=»200″ min=»1″>
</div>
<div>
<label>Precio por boleto (MXN)</label>
<input id=»precio» type=»number» value=»50″ min=»1″>
</div>
</div><div class=»boletosArea»>
<label>Datos del comprador</label>
<input id=»nombre» type=»text» placeholder=»Nombre completo»>
<input id=»telefono» type=»tel» placeholder=»Teléfono (WhatsApp preferible)»>
<input id=»email» type=»email» placeholder=»Correo (opcional para recibo)»>
</div><div class=»controls»>
<button id=»spinBtn» class=»small»>🎰 Máquina de la Suerte (Asignar al azar)</button>
<button id=»assignOne» class=»small»>Asignar 1 manual</button>
<button id=»clearStorage» class=»small»>Limpiar todo</button>
</div><div class=»slotWrap» aria-hidden=»true» style=»margin-bottom:6px»>
<div class=»reel» id=»r1″>-</div>
<div class=»reel» id=»r2″>-</div>
<div class=»reel» id=»r3″>-</div>
</div><div style=»display:flex;gap:8px;margin-top:8px»>
<button id=»payBtn» class=»payBtn»>Pagar en PayPal</button>
<button id=»downloadCSV» class=»download»>Descargar CSV</button>
</div><div class=»info»>Al presionar pagar se abrirá PayPal.Me con el monto total calculado (según boletos asignados). Guarda comprobante y confirma pago manualmente.</div>
</div><div class=»right card»>
<label>Lista pública de boletos vendidos</label>
<div class=»registro» id=»registro»>
<!– tickets aquí –>
</div><div style=»margin-top:10px»>
<label>Estadísticas</label>
<div style=»display:flex;gap:8px;margin-top:8px»>
<div class=»card» style=»padding:8px;flex:1;text-align:center»>
<div style=»font-size:22px» id=»vendidos»>0</div>
<div style=»font-size:12px;color:var(–muted)»>Boletos vendidos</div>
</div>
<div class=»card» style=»padding:8px;flex:1;text-align:center»>
<div style=»font-size:22px» id=»disponibles»>0</div>
<div style=»font-size:12px;color:var(–muted)»>Disponibles</div>
</div>
</div>
</div><div style=»margin-top:12px;display:flex;gap:8px»>
<button id=»exportJSON» class=»small»>Exportar JSON</button>
<button id=»importJSON» class=»small»>Importar JSON</button>
</div><textarea id=»jsonArea» placeholder=»Pega JSON para importar…» style=»margin-top:8px;width:100%;height:90px;border-radius:8px;background:transparent;color:#fff;padding:8px;border:1px solid rgba(255,255,255,0.04)»></textarea>
<div class=»footer»>
<div>Tip: Para tener un link público sube este archivo a GitHub Pages o Netlify (arrastrar archivo).</div>
</div>
</div>
</div></div>
<script>
/* — Datos y persistencia — */
const STORAGE_KEY = ‘rifas_el_blaky_v1’;
let state = {
titulo: ‘Rifas El Blaky’,
imagenDataUrl: null,
maxBoletos: 200,
precio: 50,
vendedores: [], // {nombre, telefono, email, boletos:[numbers], total, fecha}
asignados: [] // números ya asignados
};function saveState(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }
function loadState(){
const s = localStorage.getItem(STORAGE_KEY);
if(s){ try{ state = JSON.parse(s); }catch(e){ console.warn(‘JSON parse err’, e); }
}
}
loadState();/* — UI elements — */
const fileIn = document.getElementById(‘file’);
const previewBox = document.getElementById(‘previewBox’);
const maxBoletosIn = document.getElementById(‘maxBoletos’);
const precioIn = document.getElementById(‘precio’);
const nombreIn = document.getElementById(‘nombre’);
const telIn = document.getElementById(‘telefono’);
const emailIn = document.getElementById(‘email’);
const registro = document.getElementById(‘registro’);
const vendidosEl = document.getElementById(‘vendidos’);
const disponiblesEl = document.getElementById(‘disponibles’);
const tituloIn = document.getElementById(‘titulo’);
const fechaIn = document.getElementById(‘fecha’);function refreshUI(){
tituloIn.value = state.titulo;
maxBoletosIn.value = state.maxBoletos;
precioIn.value = state.precio;
if(state.imagenDataUrl){
previewBox.innerHTML = ‘<img src=»‘+state.imagenDataUrl+'»>’;
} else {
previewBox.innerHTML = ‘Sube imagen del premio’;
}
// lista pública
registro.innerHTML = »;
// flatten list of assigned tickets with owner
const flat = [];
state.vendedores.forEach(v=>{
(v.boletos||[]).forEach(b=> flat.push({num:b,nombre:v.nombre,telefono:v.telefono}));
});
// sort by number
flat.sort((a,b)=>a.num – b.num);
flat.forEach(item=>{
const div = document.createElement(‘div’);
div.className = ‘ticket’;
div.innerHTML = ‘<div>#<strong>’+item.num+'</strong> — ‘+escapeHtml(item.nombre)+'</div><div style=»font-size:12px;color:var(–muted)»>’+escapeHtml(item.telefono)+'</div>’;
registro.appendChild(div);
});
vendidosEl.textContent = state.asignados.length;
disponiblesEl.textContent = Math.max(0, state.maxBoletos – state.asignados.length);
saveState();
}
function escapeHtml(s){ if(!s) return »; return s.replaceAll(‘<‘,’<’).replaceAll(‘>’,’>’); }
refreshUI();/* — file upload — */
fileIn.addEventListener(‘change’, e=>{
const f = e.target.files && e.target.files[0];
if(!f) return;
const reader = new FileReader();
reader.onload = ()=> {
state.imagenDataUrl = reader.result;
refreshUI();
};
reader.readAsDataURL(f);
});/* — inputs — */
maxBoletosIn.addEventListener(‘change’, ()=>{ state.maxBoletos = parseInt(maxBoletosIn.value) || 0; refreshUI(); });
precioIn.addEventListener(‘change’, ()=>{ state.precio = parseFloat(precioIn.value) || 0; refreshUI(); });
tituloIn.addEventListener(‘input’, ()=>{ state.titulo = tituloIn.value; refreshUI(); });
fechaIn.addEventListener(‘input’, ()=>{ state.fecha = fechaIn.value; refreshUI(); });/* — helpers — */
function getRandomUnused(){
if(state.asignados.length >= state.maxBoletos) return null;
let n;
do {
n = Math.floor(Math.random() * state.maxBoletos) + 1;
} while(state.asignados.includes(n));
return n;
}
function assignTicketsToBuyer(cantidad){
const nombre = (nombreIn.value || »).trim();
const tel = (telIn.value || »).trim();
const email = (emailIn.value || »).trim();
if(!nombre || !tel){ alert(‘Completa nombre y teléfono’); return null; }
if(state.asignados.length + cantidad > state.maxBoletos){ alert(‘No hay suficientes boletos disponibles’); return null; }
const boletos = [];
for(let i=0;i<cantidad;i++){
const n = getRandomUnused();
if(n===null) break;
boletos.push(n);
state.asignados.push(n);
}
state.vendedores.push({nombre,telefono:tel,email,boletos,total:boletos.length * state.precio, fecha: new Date().toISOString()});
refreshUI();
return boletos;
}/* — máquina de la suerte (animada) — */
const r1 = document.getElementById(‘r1’), r2 = document.getElementById(‘r2’), r3 = document.getElementById(‘r3’);
function animateSlot(numbers, cb){
// show spinning animation (simple)
[r1,r2,r3].forEach(r=> r.classList.remove(‘spin’));
// small timeout to retrigger
setTimeout(()=>{
r1.textContent = numbers[0] || ‘-‘;
r2.textContent = numbers[1] || ‘-‘;
r3.textContent = numbers[2] || ‘-‘;
[r1,r2,r3].forEach(r=> r.classList.add(‘spin’));
setTimeout(()=>{ [r1,r2,r3].forEach(r=> r.classList.remove(‘spin’)); if(cb) cb(); }, 900);
}, 50);
}/* — buttons — */
document.getElementById(‘spinBtn’).addEventListener(‘click’, ()=>{
// Asigna entre 1 y 5 boletos aleatorios y muestra en slots
const cantidad = Math.floor(Math.random()*5) + 1;
const boletos = assignTicketsToBuyer(cantidad);
if(!boletos) return;
// choose 3 numbers to show (pad with ‘-‘)
const pick = [boletos[0]||’-‘, boletos[1]||’-‘, boletos[2]||’-‘];
animateSlot(pick, ()=> {
alert(‘Boletos asignados: ‘ + boletos.join(‘, ‘) + ‘\\nTotal a pagar (MXN): $’ + (boletos.length * state.precio));
});
});document.getElementById(‘assignOne’).addEventListener(‘click’, ()=> {
const boletos = assignTicketsToBuyer(1);
if(boletos) alert(‘Boleto asignado: ‘ + boletos.join(‘, ‘)+ ‘\\nTotal a pagar: $’ + (boletos.length * state.precio));
});document.getElementById(‘clearStorage’).addEventListener(‘click’, ()=> {
if(!confirm(‘¿Eliminar todos los datos guardados localmente? Esto borrará lista de compradores y boletos asignados.’)) return;
state.vendedores = []; state.asignados = []; state.imagenDataUrl = null; refreshUI(); localStorage.removeItem(STORAGE_KEY);
});/* — PayPal — */
document.getElementById(‘payBtn’).addEventListener(‘click’, ()=>{
// calcula total por comprador (último vendedor)
if(state.vendedores.length === 0){ alert(‘Aún no hay pedidos.’); return; }
const last = state.vendedores[state.vendedores.length – 1];
const amount = (last.total || 0).toFixed(2);
if(amount <= 0){ alert(‘El monto es $0. Asegúrate de asignar boletos.’); return; }
// Cambia el usuario PayPal aquí si quieres
const PAYPAL_ME = ‘https://www.paypal.me/TU_USUARIO/’; // <– edita tu PayPal.Me
const link = PAYPAL_ME + encodeURIComponent(amount);
window.open(link, ‘_blank’);
});/* — CSV export — */
function exportCSV(){
let rows = [[‘Nombre’,’Teléfono’,’Email’,’Boletos’,’Total’,’Fecha’]];
state.vendedores.forEach(v=>{
rows.push([v.nombre, v.telefono || », v.email || », (v.boletos||[]).join(‘ ‘), (v.total||0), v.fecha || »]);
});
const csv = rows.map(r=> r.map(c=> ‘»‘+String(c).replace(/»/g,'»»‘)+'»‘).join(‘,’)).join(‘\\n’);
const blob = new Blob([csv], {type:’text/csv;charset=utf-8;’});
const url = URL.createObjectURL(blob);
const a = document.createElement(‘a’);
a.href = url; a.download = ‘rifas_el_blaky_compradores.csv’; document.body.appendChild(a); a.click(); a.remove();
}
document.getElementById(‘downloadCSV’).addEventListener(‘click’, exportCSV);/* — export/import JSON — */
document.getElementById(‘exportJSON’).addEventListener(‘click’, ()=> {
const a = document.createElement(‘a’);
const data = JSON.stringify(state, null, 2);
const blob = new Blob([data], {type:’application/json’});
a.href = URL.createObjectURL(blob); a.download = ‘rifas_el_blaky_data.json’; document.body.appendChild(a); a.click(); a.remove();
});
document.getElementById(‘importJSON’).addEventListener(‘click’, ()=> {
const text = document.getElementById(‘jsonArea’).value.trim();
if(!text) return alert(‘Pega JSON en el recuadro para importar.’);
try{
const obj = JSON.parse(text);
if(confirm(‘Importar datos reemplazará el estado actual. Continuar?’)){
state = obj; saveState(); refreshUI(); alert(‘Importado.’);
}
}catch(e){ alert(‘JSON inválido: ‘+e.message); }
});/* — init assigned array from vendedores — */
(function rebuildAssigned(){
const arr = [];
state.vendedores.forEach(v => (v.boletos||[]).forEach(n=> arr.push(n)));
// unique
state.asignados = Array.from(new Set(arr));
// ensure maxBoletos defined
state.maxBoletos = state.maxBoletos || 200;
state.precio = state.precio || 50;
refreshUI();
})();
</script>
</body>
</html>