<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mi Torneo Escolar - Gestor Moderno</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- Librerías para QR y PDF -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet">
<style>
/* Estilo base y fondo animado */
body {
font-family: 'Inter', sans-serif;
background-color: #0f0c29;
background: linear-gradient(45deg, #0f0c29, #302b63, #24243e);
background-size: 400% 400%;
animation: gradientBG 15s ease infinite;
color: #e0e0e0;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Clases para gestionar vistas y animaciones */
.view { display: none; }
.view.active {
display: block;
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Estilo mejorado para modales */
.modal-backdrop {
background-color: rgba(10, 10, 20, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: none;
}
.modal-backdrop.flex {
display: flex;
animation: fadeIn 0.3s ease;
}
.modal-content {
animation: slideUp 0.4s ease-out;
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* Efecto "Glassmorphism" refinado */
.glass-effect {
background: rgba(36, 36, 62, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
/* Estilo para pestañas activas */
.tab-btn.active {
border-color: #8B5CF6; /* violet-500 */
color: #8B5CF6;
background-color: rgba(139, 92, 246, 0.1);
}
/* Estilos para inputs y botones */
.form-input {
background-color: rgba(0,0,0,0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: #8B5CF6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.4);
}
.btn-primary {
background: linear-gradient(45deg, #8B5CF6, #EC4899);
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.btn-primary:active {
transform: translateY(0px) scale(0.98);
}
/* Estilos de impresión */
@media print {
body * { visibility: hidden; }
#pdf-export-area, #pdf-export-area * { visibility: visible; }
#pdf-export-area { position: absolute; left: 0; top: 0; width: 100%; }
.no-print { display: none; }
}
</style>
</head>
<body>
<div id="app" class="max-w-7xl mx-auto p-4 sm:p-6 lg:p-8 min-h-screen">
<!-- Vista de Login / Bienvenida -->
<div id="login-view" class="view active">
<div class="max-w-md mx-auto mt-10 sm:mt-20 text-center">
<h1 class="text-5xl font-black text-white uppercase tracking-wider" style="text-shadow: 0 0 15px rgba(236, 72, 153, 0.5);">Mi Torneo Escolar</h1>
<p class="text-gray-400 mt-2 text-lg">La forma más fácil de gestionar tus torneos.</p>
<div class="glass-effect p-8 rounded-2xl mt-8">
<div id="login-form-container">
<h2 class="text-2xl font-bold mb-6 text-white">Iniciar Sesión</h2>
<input type="text" id="login-username" placeholder="Nombre de Usuario" class="w-full form-input text-white rounded-md p-3 mb-4 placeholder-gray-400">
<input type="password" id="login-password" placeholder="Contraseña" class="w-full form-input text-white rounded-md p-3 mb-4 placeholder-gray-400">
<button id="login-btn" class="w-full btn-primary text-white font-bold py-3 rounded-lg">Ingresar</button>
<p class="text-sm text-gray-400 mt-4">¿No tienes cuenta? <a href="#" id="show-register-link" class="text-violet-400 hover:underline">Regístrate aquí</a></p>
</div>
<div id="register-form-container" class="hidden">
<h2 class="text-2xl font-bold mb-6 text-white">Crear Cuenta</h2>
<input type="text" id="register-username" placeholder="Nombre de Usuario (Nick Name)" class="w-full form-input text-white rounded-md p-3 mb-4 placeholder-gray-400">
<input type="password" id="register-password" placeholder="Contraseña" class="w-full form-input text-white rounded-md p-3 mb-4 placeholder-gray-400">
<button id="register-btn" class="w-full btn-primary text-white font-bold py-3 rounded-lg">Registrarse</button>
<p class="text-sm text-gray-400 mt-4">¿Ya tienes cuenta? <a href="#" id="show-login-link" class="text-violet-400 hover:underline">Inicia sesión</a></p>
</div>
<div class="mt-6 border-t border-gray-700 pt-6">
<p class="text-gray-400 mb-4">O puedes probar la aplicación sin registrarte:</p>
<button id="guest-btn" class="w-full bg-gray-600 text-white font-bold py-3 rounded-lg hover:bg-gray-700 transition-all duration-300">Entrar como Invitado</button>
</div>
</div>
</div>
</div>
<!-- Vista del Dashboard -->
<div id="dashboard-view" class="view">
<header class="mb-8 flex justify-between items-center">
<div>
<h1 class="text-4xl font-black text-white">Mis Torneos</h1>
<p id="welcome-user" class="text-gray-400 mt-1"></p>
</div>
<button id="logout-btn" class="bg-red-600 text-white font-semibold py-2 px-4 rounded-lg hover:bg-red-700 transition-colors flex items-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clip-rule="evenodd" /></svg>
<span>Cerrar Sesión</span>
</button>
</header>
<div id="tournament-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"></div>
<div class="mt-8">
<button id="go-to-create-btn" class="w-full sm:w-auto btn-primary text-white font-bold py-3 px-6 rounded-lg shadow-lg flex items-center justify-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
Crear Nuevo Torneo
</button>
</div>
</div>
<!-- Vista de Creación de Torneo -->
<div id="create-tournament-view" class="view">
<header class="mb-8">
<button class="back-to-dashboard text-violet-400 hover:underline mb-4 no-print">← Volver al Dashboard</button>
<h1 class="text-4xl font-black text-white">Nuevo Torneo</h1>
</header>
<div class="glass-effect p-8 rounded-2xl">
<!-- Wizard -->
<div id="step-1">
<h2 class="text-2xl font-bold mb-2 text-white">Paso 1: Información</h2>
<p class="text-gray-400 mb-4">¡Vamos a empezar! Primero, dale un nombre a tu torneo y dinos qué deporte se jugará.</p>
<input type="text" id="tournament-name" placeholder="Nombre del Torneo" class="w-full form-input text-white rounded-md p-3 mb-4 placeholder-gray-400">
<input type="text" id="tournament-sport" placeholder="Deporte (ej. Fútbol, Voley, Basquet)" class="w-full form-input text-white rounded-md p-3 mb-4 placeholder-gray-400">
<button id="next-step-2-btn" class="w-full btn-primary text-white font-bold py-3 rounded-lg">Siguiente</button>
</div>
<div id="step-2" class="hidden">
<h2 class="text-2xl font-bold mb-2 text-white">Paso 2: Equipos</h2>
<p class="text-gray-400 mb-4">Ahora, añade todos los equipos que participarán. Escribe el nombre y pulsa "Añadir".</p>
<div class="flex mb-4"><input type="text" id="team-name-input" placeholder="Nombre del equipo" class="flex-grow form-input text-white rounded-l-md p-3 placeholder-gray-400"><button id="add-team-btn" class="bg-gray-600 px-4 rounded-r-md hover:bg-gray-500 text-white font-bold">Añadir</button></div>
<ul id="teams-list" class="space-y-2 mb-4 max-h-60 overflow-y-auto pr-2"></ul>
<div class="flex justify-between"><button id="back-step-1-btn" class="bg-gray-600 text-white py-2 px-4 rounded-lg hover:bg-gray-700">Anterior</button><button id="next-step-3-btn" class="btn-primary text-white py-2 px-4 rounded-lg">Siguiente</button></div>
</div>
<div id="step-3" class="hidden">
<h2 class="text-2xl font-bold mb-4 text-white">Paso 3: Formato</h2>
<select id="tournament-format" class="w-full form-input text-white rounded-md p-3 mb-4">
<option value="liga">Liga (Todos contra todos)</option>
<option value="eliminatoria">Eliminación Directa</option>
</select>
<div class="flex justify-between"><button id="back-step-2-btn" class="bg-gray-600 text-white py-2 px-4 rounded-lg hover:bg-gray-700">Anterior</button><button id="generate-fixture-btn" class="bg-blue-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-blue-700">Generar y Guardar</button></div>
</div>
</div>
</div>
<!-- Vista de Gestión de Torneo -->
<div id="manage-tournament-view" class="view">
<header class="mb-6 flex justify-between items-center">
<div>
<button class="back-to-dashboard text-violet-400 hover:underline mb-4 no-print">← Volver al Dashboard</button>
<h1 id="manage-tournament-title" class="text-4xl font-black text-white"></h1>
</div>
<button id="show-podium-btn" class="hidden bg-yellow-400 text-yellow-900 font-bold py-2 px-4 rounded-lg shadow-md hover:bg-yellow-500 no-print flex items-center space-x-2 transition-transform hover:scale-105">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.783-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" /></svg>
<span>Ver Podio</span>
</button>
</header>
<div class="border-b border-gray-700 mb-6 no-print"><nav class="-mb-px flex space-x-4"><button class="tab-btn whitespace-nowrap py-3 px-4 rounded-t-lg border-b-2 font-medium text-lg text-gray-400 hover:text-white border-transparent transition-colors" data-tab="partidos">Partidos</button><button class="tab-btn whitespace-nowrap py-3 px-4 rounded-t-lg border-b-2 font-medium text-lg text-gray-400 hover:text-white border-transparent transition-colors" data-tab="clasificacion">Clasificación</button><button class="tab-btn whitespace-nowrap py-3 px-4 rounded-t-lg border-b-2 font-medium text-lg text-gray-400 hover:text-white border-transparent transition-colors" data-tab="compartir">Compartir</button></nav></div>
<div id="pdf-export-area" class="bg-transparent">
<div id="tab-content-partidos" class="tab-content"></div>
<div id="tab-content-clasificacion" class="tab-content hidden"></div>
</div>
<div id="tab-content-compartir" class="tab-content hidden">
<div class="glass-effect p-8 rounded-2xl text-center max-w-lg mx-auto">
<h2 class="text-2xl font-bold mb-4 text-white">Compartir con Código QR</h2>
<p class="text-gray-400 mb-4">Los alumnos pueden escanear este código para ver el torneo en tiempo real.</p>
<div id="qrcode-container" class="flex justify-center mb-6 bg-white p-4 rounded-lg"></div>
<h2 class="text-2xl font-bold mt-8 mb-4 text-white">Exportar a PDF</h2>
<p class="text-gray-400 mb-4">Imprime el fixture o la clasificación en formato A4 para pegar en el colegio.</p>
<button id="export-pdf-btn" class="bg-red-600 text-white font-bold py-3 px-5 rounded-lg hover:bg-red-700 transition-colors">Imprimir PDF</button>
</div>
</div>
</div>
<!-- Vista Pública -->
<div id="public-view" class="view"></div>
<!-- Modal de Partido en Vivo (Standard) -->
<div id="live-match-modal" class="modal-backdrop fixed inset-0 items-center justify-center z-50">
<div class="glass-effect modal-content rounded-2xl p-8 w-full max-w-lg mx-4">
<div id="standard-match-main-controls">
<div class="flex justify-between items-start">
<h2 class="text-3xl font-bold mb-6 text-white">Partido en Vivo</h2>
<div id="live-match-period" class="text-lg font-semibold bg-gray-900 px-3 py-1 rounded-md text-white">Tiempo: 1</div>
</div>
<div id="live-match-timer" class="text-7xl font-black font-mono text-center mb-6 text-white">00:00</div>
<div class="flex justify-center space-x-2 mb-8">
<button id="timer-start" class="bg-emerald-600 text-white px-4 py-2 rounded-lg flex items-center space-x-2 hover:bg-emerald-700"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" /></svg><span>Iniciar</span></button>
<button id="timer-pause" class="bg-yellow-500 text-white px-4 py-2 rounded-lg flex items-center space-x-2 hover:bg-yellow-600"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /></svg><span>Pausar</span></button>
<button id="timer-reset" class="bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center space-x-2 hover:bg-gray-700"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm10 10a1 1 0 011-1h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 111.885-.666A5.002 5.002 0 0014.001 13H11a1 1 0 01-1-1z" clip-rule="evenodd" /></svg><span>Reiniciar</span></button>
<button id="timer-new-period" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">Nuevo Tiempo</button>
</div>
<div class="grid grid-cols-2 gap-6 text-center">
<div>
<h3 id="live-team1-name" class="text-xl font-bold mb-2 truncate text-white"></h3>
<div id="live-team1-score" class="text-8xl font-black mb-4 text-white">0</div>
<div class="flex justify-center space-x-3"><button class="score-btn bg-emerald-600 text-white w-14 h-14 rounded-full text-3xl font-bold hover:bg-emerald-700 transform hover:scale-110 transition-transform" data-team="1" data-op="add">+</button><button class="score-btn bg-red-600 text-white w-14 h-14 rounded-full text-3xl font-bold hover:bg-red-700 transform hover:scale-110 transition-transform" data-team="1" data-op="sub">-</button></div>
</div>
<div>
<h3 id="live-team2-name" class="text-xl font-bold mb-2 truncate text-white"></h3>
<div id="live-team2-score" class="text-8xl font-black mb-4 text-white">0</div>
<div class="flex justify-center space-x-3"><button class="score-btn bg-emerald-600 text-white w-14 h-14 rounded-full text-3xl font-bold hover:bg-emerald-700 transform hover:scale-110 transition-transform" data-team="2" data-op="add">+</button><button class="score-btn bg-red-600 text-white w-14 h-14 rounded-full text-3xl font-bold hover:bg-red-700 transform hover:scale-110 transition-transform" data-team="2" data-op="sub">-</button></div>
</div>
</div>
</div>
<!-- Sección de Penales / Opciones de Empate -->
<div id="tiebreaker-options" class="hidden text-center">
<h3 class="text-xl font-bold mb-4 text-white">El partido ha terminado en empate</h3>
<p class="text-gray-400 mb-6">¿Cómo deseas definir el resultado?</p>
<div class="flex flex-col space-y-3">
<button id="start-penalty-btn" class="w-full bg-purple-600 text-white font-bold py-3 rounded-lg hover:bg-purple-700">Iniciar Penales</button>
<button id="finish-as-draw-btn" class="w-full bg-gray-600 text-white font-bold py-3 rounded-lg hover:bg-gray-700">Finalizar como Empate</button>
</div>
</div>
<div id="penalty-shootout-section" class="hidden mt-6 border-t border-gray-700 pt-6">
<h3 class="text-2xl font-bold text-center mb-4 text-white">Tanda de Penales</h3>
<div class="grid grid-cols-2 gap-6 text-center">
<div>
<div id="penalty-team1-score" class="text-5xl font-bold mb-4 text-white">0</div>
<div class="flex justify-center space-x-3"><button class="penalty-score-btn bg-emerald-600 text-white w-10 h-10 rounded-full text-xl hover:bg-emerald-700" data-team="1" data-op="add">+</button><button class="penalty-score-btn bg-red-600 text-white w-10 h-10 rounded-full text-xl hover:bg-red-700" data-team="1" data-op="sub">-</button></div>
</div>
<div>
<div id="penalty-team2-score" class="text-5xl font-bold mb-4 text-white">0</div>
<div class="flex justify-center space-x-3"><button class="penalty-score-btn bg-emerald-600 text-white w-10 h-10 rounded-full text-xl hover:bg-emerald-700" data-team="2" data-op="add">+</button><button class="penalty-score-btn bg-red-600 text-white w-10 h-10 rounded-full text-xl hover:bg-red-700" data-team="2" data-op="sub">-</button></div>
</div>
</div>
</div>
<div class="mt-8 text-center">
<button id="finish-match-btn" class="w-full bg-green-600 text-white font-bold py-3 rounded-lg hover:bg-green-700">Finalizar Partido</button>
<button id="cancel-match-btn" class="w-full mt-2 text-gray-400 hover:underline">Cancelar</button>
</div>
</div>
</div>
<!-- Modal de Partido de Voley -->
<div id="volleyball-match-modal" class="modal-backdrop fixed inset-0 items-center justify-center z-50">
<div class="glass-effect modal-content rounded-2xl p-8 w-full max-w-2xl mx-4">
<div class="flex justify-between items-start mb-4">
<h2 class="text-3xl font-bold text-white">Partido de Voley</h2>
<div id="voley-sets-won" class="text-lg font-semibold text-white">Sets: 0 - 0</div>
</div>
<div id="voley-previous-sets" class="text-sm text-gray-400 mb-4 h-12 overflow-y-auto"></div>
<div class="bg-gray-900 bg-opacity-50 p-4 rounded-lg">
<h3 id="voley-current-set-label" class="text-center font-bold mb-4 text-xl text-white">Set Actual: 1</h3>
<div class="grid grid-cols-2 gap-6 text-center">
<div>
<h3 id="voley-team1-name" class="text-xl font-bold mb-2 truncate text-white"></h3>
<div id="voley-team1-score" class="text-8xl font-black mb-4 text-white">0</div>
<div class="flex justify-center space-x-3"><button class="voley-score-btn bg-emerald-600 text-white w-14 h-14 rounded-full text-3xl hover:bg-emerald-700 transform hover:scale-110 transition-transform" data-team="1" data-op="add">+</button><button class="voley-score-btn bg-red-600 text-white w-14 h-14 rounded-full text-3xl hover:bg-red-700 transform hover:scale-110 transition-transform" data-team="1" data-op="sub">-</button></div>
</div>
<div>
<h3 id="voley-team2-name" class="text-xl font-bold mb-2 truncate text-white"></h3>
<div id="voley-team2-score" class="text-8xl font-black mb-4 text-white">0</div>
<div class="flex justify-center space-x-3"><button class="voley-score-btn bg-emerald-600 text-white w-14 h-14 rounded-full text-3xl hover:bg-emerald-700 transform hover:scale-110 transition-transform" data-team="2" data-op="add">+</button><button class="voley-score-btn bg-red-600 text-white w-14 h-14 rounded-full text-3xl hover:bg-red-700 transform hover:scale-110 transition-transform" data-team="2" data-op="sub">-</button></div>
</div>
</div>
</div>
<div class="mt-8 flex space-x-4">
<button id="voley-new-set-btn" class="w-full bg-blue-600 text-white font-bold py-3 rounded-lg hover:bg-blue-700">Finalizar Set y Empezar Nuevo</button>
<button id="voley-finish-match-btn" class="w-full bg-green-600 text-white font-bold py-3 rounded-lg hover:bg-green-700">Finalizar Partido</button>
</div>
<button id="voley-cancel-match-btn" class="w-full mt-2 text-gray-400 hover:underline">Cancelar</button>
</div>
</div>
<!-- Modal de Podio -->
<div id="podium-modal" class="modal-backdrop fixed inset-0 items-center justify-center z-50">
<div class="glass-effect modal-content rounded-2xl p-8 w-full max-w-md mx-4 text-center">
<h2 class="text-3xl font-bold text-white mb-6">🏆 Podio del Torneo 🏆</h2>
<div class="space-y-4">
<div class="bg-yellow-400 p-4 rounded-lg border-2 border-yellow-500">
<p class="text-lg font-medium text-yellow-900">🥇 1er Puesto</p>
<p id="podium-first" class="text-2xl font-bold text-yellow-900"></p>
</div>
<div class="bg-gray-300 p-4 rounded-lg border-2 border-gray-400">
<p class="text-lg font-medium text-gray-800">🥈 2do Puesto</p>
<p id="podium-second" class="text-2xl font-bold text-gray-900"></p>
</div>
<div class="bg-yellow-600 p-4 rounded-lg border-2 border-yellow-700">
<p class="text-lg font-medium text-yellow-900">🥉 3er Puesto</p>
<p id="podium-third" class="text-2xl font-bold text-yellow-900"></p>
</div>
</div>
<button id="close-podium-btn" class="w-full mt-8 bg-gray-600 text-white font-bold py-2 rounded-lg hover:bg-gray-700">Cerrar</button>
</div>
</div>
<!-- Modal Genérico para Alertas y Confirmaciones -->
<div id="generic-modal" class="modal-backdrop fixed inset-0 items-center justify-center z-50">
<div class="glass-effect modal-content rounded-2xl p-8 w-full max-w-sm mx-4 text-center">
<h3 id="generic-modal-title" class="text-xl font-bold mb-4 text-white"></h3>
<p id="generic-modal-message" class="text-gray-300 mb-6"></p>
<div id="generic-modal-buttons" class="flex justify-center space-x-4">
<!-- Los botones se inyectan dinámicamente -->
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const App = {
state: {
tournaments: [],
activeTournamentId: null,
currentUser: null,
liveMatch: {
tournamentId: null, matchId: null, roundIndex: null,
score1: 0, score2: 0,
penaltyScore1: 0, penaltyScore2: 0,
isPenaltyShootout: false,
timerInterval: null, timeSeconds: 0, period: 1,
sets: [], currentSet: { score1: 0, score2: 0 }
},
},
ui: {
views: { login: document.getElementById('login-view'), dashboard: document.getElementById('dashboard-view'), create: document.getElementById('create-tournament-view'), manage: document.getElementById('manage-tournament-view'), public: document.getElementById('public-view') },
liveMatchModal: document.getElementById('live-match-modal'),
volleyballMatchModal: document.getElementById('volleyball-match-modal'),
podiumModal: document.getElementById('podium-modal'),
genericModal: document.getElementById('generic-modal'),
},
init() {
this.attachEventListeners();
const params = new URLSearchParams(window.location.search);
if (params.get('user') && params.get('torneo')) {
this.handleRouting();
} else {
this.state.currentUser = sessionStorage.getItem('currentUser');
if (this.state.currentUser) {
this.loadTournaments();
this.renderDashboard();
this.showView('dashboard');
} else {
this.showView('login');
}
}
window.jsPDF = window.jspdf.jsPDF;
},
// --- MODAL SYSTEM ---
showModal(title, message, buttons) {
document.getElementById('generic-modal-title').textContent = title;
document.getElementById('generic-modal-message').textContent = message;
const buttonsContainer = document.getElementById('generic-modal-buttons');
buttonsContainer.innerHTML = '';
buttons.forEach(btnConfig => {
const button = document.createElement('button');
button.textContent = btnConfig.text;
button.className = btnConfig.classes;
button.addEventListener('click', () => {
this.ui.genericModal.classList.remove('flex');
if (btnConfig.callback) {
btnConfig.callback();
}
});
buttonsContainer.appendChild(button);
});
this.ui.genericModal.classList.add('flex');
},
showAlert(message, title = 'Aviso') {
this.showModal(title, message, [
{ text: 'Aceptar', classes: 'w-full btn-primary text-white font-bold py-2 px-4 rounded-lg' }
]);
},
showConfirm(message, onConfirm, title = 'Confirmación') {
this.showModal(title, message, [
{ text: 'Cancelar', classes: 'bg-gray-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-gray-700' },
{ text: 'Confirmar', classes: 'bg-red-600 text-white font-bold py-2 px-4 rounded-lg hover:bg-red-700', callback: onConfirm }
]);
},
// --- VIEW MANAGEMENT ---
showView(viewName) {
Object.values(this.ui.views).forEach(v => v.classList.remove('active'));
this.ui.views[viewName].classList.add('active');
window.scrollTo(0, 0);
},
handleRouting() {
const params = new URLSearchParams(window.location.search);
const username = params.get('user');
const tournamentId = params.get('torneo');
if (username && tournamentId) {
this.renderPublicView(username, tournamentId);
} else {
this.showView('login');
}
},
// --- DATA MANAGEMENT (LOCALSTORAGE) ---
loadTournaments() {
if (!this.state.currentUser || this.state.currentUser === 'Invitado') {
this.state.tournaments = [];
return;
}
const data = localStorage.getItem(`tournaments_${this.state.currentUser}`);
this.state.tournaments = data ? JSON.parse(data) : [];
},
saveTournaments() {
if (!this.state.currentUser || this.state.currentUser === 'Invitado') return;
localStorage.setItem(`tournaments_${this.state.currentUser}`, JSON.stringify(this.state.tournaments));
},
// --- RENDERING ---
renderDashboard() {
document.getElementById('welcome-user').textContent = `Bienvenido, ${this.state.currentUser}`;
const list = document.getElementById('tournament-list');
list.innerHTML = '';
if (this.state.tournaments.length === 0) {
list.innerHTML = `<div class="col-span-full text-center text-gray-400 p-10 glass-effect rounded-2xl">
<h3 class="text-xl font-bold text-white">¡Es hora de empezar!</h3>
<p>Aún no has creado ningún torneo. Haz clic en el botón de abajo para crear el primero.</p>
</div>`;
return;
}
this.state.tournaments.forEach(t => {
const card = document.createElement('div');
card.className = 'glass-effect p-6 rounded-2xl flex flex-col justify-between transition-all duration-300 hover:border-violet-500 border-t-4 border-t-violet-500/50';
card.dataset.id = t.id;
let formatText = t.format === 'liga' ? 'Liga' : 'Eliminación Directa';
card.innerHTML = `
<div>
<div class="flex justify-between items-start">
<h3 class="font-bold text-lg text-white flex-grow pr-2">${t.name}</h3>
<button class="edit-tournament-btn p-1 text-gray-400 hover:text-violet-400 flex-shrink-0" data-action="edit-name">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" /><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" /></svg>
</button>
</div>
<p class="text-sm text-gray-400">${t.sport}</p>
<div class="mt-4 flex justify-between items-center text-xs text-gray-500">
<span>👥 ${t.teams.length} Equipos</span>
<span>📅 ${t.creationDate}</span>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-700 flex flex-col space-y-2">
<button class="w-full btn-primary text-white font-semibold py-2 px-4 rounded-lg" data-action="view">Iniciar Torneo</button>
<div class="flex justify-between items-center">
<span class="inline-block bg-gray-700 text-gray-300 text-xs font-semibold px-2.5 py-0.5 rounded-full">${formatText}</span>
<button class="delete-tournament-btn text-red-500 hover:text-red-400 text-sm font-semibold p-1 rounded-md" data-action="delete">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /></svg>
</button>
</div>
</div>`;
list.appendChild(card);
});
},
renderManageView() {
const tournament = this.state.tournaments.find(t => t.id === this.state.activeTournamentId);
if (!tournament) return;
document.getElementById('manage-tournament-title').textContent = tournament.name;
const podiumBtn = document.getElementById('show-podium-btn');
let isFinished = false;
if (tournament.format === 'liga') {
isFinished = tournament.rounds.every(round => round.matches.every(match => match.score1 !== null || (match.sets && match.sets.length > 0)));
} else {
isFinished = tournament.isFinished || false;
}
podiumBtn.classList.toggle('hidden', !isFinished);
const initialTab = 'partidos';
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector(`.tab-btn[data-tab="${initialTab}"]`).classList.add('active');
document.querySelectorAll('.tab-content').forEach(content => content.classList.add('hidden'));
document.getElementById(`tab-content-${initialTab}`).classList.remove('hidden');
document.querySelector('.tab-btn[data-tab="clasificacion"]').style.display = tournament.format === 'liga' ? 'inline-block' : 'none';
this.renderMatches(tournament);
this.renderClassification(tournament);
this.renderShareTab(tournament);
},
renderMatches(tournament) {
const container = document.getElementById('tab-content-partidos');
if (tournament.format === 'eliminatoria') {
container.innerHTML = this.renderBracketHTML(tournament, "Cuadro Principal");
} else {
let html = '';
tournament.rounds.forEach((round, index) => {
html += `<h3 class="text-2xl font-bold mt-6 mb-3 text-white">Jornada ${index + 1}</h3><div class="space-y-3">`;
round.matches.forEach(match => {
const isVoley = tournament.sport.toLowerCase().includes('voley');
const isPlayed = isVoley ? (match.sets && match.sets.length > 0) : match.score1 !== null;
let centerContent = '';
let team1Display = `<span class="font-bold text-right w-2/5 text-lg">${match.team1}</span>`;
let team2Display = `<span class="font-bold text-left w-2/5 text-lg">${match.team2}</span>`;
if (isPlayed) {
let resultText = '';
let winner = null;
if(isVoley){
let setsWon1 = 0, setsWon2 = 0;
match.sets.forEach(set => {
if (set.score1 > set.score2) setsWon1++; else setsWon2++;
});
resultText = `<span class="text-sm">Sets: ${match.sets.map(s => `${s.score1}-${s.score2}`).join(', ')}</span>`;
if (setsWon1 > setsWon2) winner = match.team1;
if (setsWon2 > setsWon1) winner = match.team2;
} else {
resultText = `<span class="text-xl font-bold">${match.score1} - ${match.score2}</span>`;
if (match.penaltyScore1 !== null && match.penaltyScore1 !== undefined) {
resultText += ` <span class="text-sm text-gray-400">(Pen: ${match.penaltyScore1}-${match.penaltyScore2})</span>`;
if(match.penaltyScore1 > match.penaltyScore2) winner = match.team1;
if(match.penaltyScore2 > match.penaltyScore1) winner = match.team2;
} else {
if (match.score1 > match.score2) winner = match.team1;
if (match.score2 > match.score1) winner = match.team2;
}
}
if (winner) {
if (winner === match.team1) {
team1Display = `<span class="font-bold text-right w-2/5 text-lg text-emerald-400">${match.team1} (G)</span>`;
team2Display = `<span class="font-bold text-left w-2/5 text-lg text-gray-400">${match.team2} (P)</span>`;
} else {
team1Display = `<span class="font-bold text-right w-2/5 text-lg text-gray-400">${match.team1} (P)</span>`;
team2Display = `<span class="font-bold text-left w-2/5 text-lg text-emerald-400">${match.team2} (G)</span>`;
}
}
centerContent = `<div class="text-center">
<div>${resultText}</div>
<div class="text-xs text-emerald-400 font-semibold mt-1">Finalizado</div>
</div>`;
} else {
centerContent = `<button class="bg-violet-600 text-white px-4 py-1 rounded-md text-sm font-semibold hover:bg-violet-700">Jugar</button>`;
}
html += `<div class="glass-effect p-4 rounded-lg flex items-center justify-between cursor-pointer hover:border-violet-500" data-match-id="${match.id}" data-round-index="${index}">
${team1Display}
<div class="text-center w-1/5 text-white">${centerContent}</div>
${team2Display}
</div>`;
});
html += `</div>`;
});
container.innerHTML = html;
}
},
renderBracketHTML(tournament, title = "Cuadro Principal") {
let html = `<h2 class="text-3xl font-black text-white text-center mb-4">${title}</h2>`;
html += '<div class="flex space-x-4 overflow-x-auto p-4">';
const isVoley = tournament.sport.toLowerCase().includes('voley');
const roundsData = tournament.rounds;
roundsData.forEach((round, roundIndex) => {
html += `<div class="flex flex-col justify-around min-w-[240px]">
<h3 class="text-center font-bold mb-4 text-white text-xl">Ronda ${roundIndex + 1}</h3>`;
if (tournament.roundByes && tournament.roundByes[roundIndex]) {
html += `<div class="text-center text-sm text-gray-400 mb-4 p-2 glass-effect rounded-md">Descansa: <span class="font-semibold text-white">${tournament.roundByes[roundIndex]}</span></div>`;
}
html += `<div class="space-y-8">`;
round.matches.forEach(match => {
const isPlayed = isVoley ? (match.sets && match.sets.length > 0) : match.score1 !== null;
const canPlay = match.team1 && match.team2;
let score1Display = '', score2Display = '';
let winnerTeam = null;
if (isPlayed) {
if (isVoley) {
let setsWon1 = 0, setsWon2 = 0;
match.sets.forEach(set => {
if (set.score1 > set.score2) setsWon1++; else setsWon2++;
});
score1Display = setsWon1;
score2Display = setsWon2;
if (setsWon1 > setsWon2) winnerTeam = match.team1;
if (setsWon2 > setsWon1) winnerTeam = match.team2;
} else {
score1Display = match.score1;
score2Display = match.score2;
if (match.penaltyScore1 !== null && match.penaltyScore1 !== undefined) {
if (match.penaltyScore1 > match.penaltyScore2) winnerTeam = match.team1;
if (match.penaltyScore2 > match.penaltyScore1) winnerTeam = match.team2;
} else {
if (match.score1 > match.score2) winnerTeam = match.team1;
if (match.score2 > match.score1) winnerTeam = match.team2;
}
}
}
let team1Text = match.team1 || '<i>Por definir</i>';
let team2Text = match.team2 || '<i>Por definir</i>';
if (winnerTeam) {
if (winnerTeam === match.team1) {
team1Text += ' (G)';
team2Text += ' (P)';
} else {
team1Text += ' (P)';
team2Text += ' (G)';
}
}
const team1HTML = `<div class="flex justify-between items-center ${winnerTeam === match.team1 ? 'font-bold text-white' : 'text-gray-300'}"><span>${team1Text}</span><span class="font-black text-lg">${isPlayed ? score1Display : ''}</span></div>`;
const team2HTML = `<div class="flex justify-between items-center ${winnerTeam === match.team2 ? 'font-bold text-white' : 'text-gray-300'}"><span>${team2Text}</span><span class="font-black text-lg">${isPlayed ? score2Display : ''}</span></div>`;
let separatorHTML;
if (isPlayed) {
let details = '';
if (isVoley) {
details = `Sets: ${match.sets.map(s => `${s.score1}-${s.score2}`).join(', ')}`;
} else if (match.penaltyScore1 !== null && match.penaltyScore1 !== undefined) {
details = `(Pen: ${match.penaltyScore1}-${match.penaltyScore2})`;
}
separatorHTML = `<div class="text-center text-xs text-emerald-400 font-semibold my-1">Finalizado <span class="block text-gray-400 font-normal">${details}</span></div>`;
} else if (canPlay) {
separatorHTML = `<div class="text-center my-1"><button class="bg-violet-600 text-white px-3 py-0.5 rounded text-xs font-semibold hover:bg-violet-700">Jugar</button></div>`;
} else {
separatorHTML = `<hr class="my-1 border-gray-700">`;
}
html += `<div class="glass-effect p-3 rounded-lg cursor-pointer hover:border-violet-500" data-match-id="${match.id}" data-round-index="${roundIndex}">
${team1HTML}
${separatorHTML}
${team2HTML}
</div>`;
});
html += `</div></div>`;
});
html += '</div>';
if (tournament.thirdPlaceMatch) {
const match = tournament.thirdPlaceMatch;
const isPlayed = isVoley ? (match.sets && match.sets.length > 0) : match.score1 !== null;
const canPlay = match.team1 && match.team2;
let score1Display = '', score2Display = '';
let winnerTeam = null;
if (isPlayed) {
if (isVoley) {
let setsWon1 = 0, setsWon2 = 0;
match.sets.forEach(set => {
if (set.score1 > set.score2) setsWon1++; else setsWon2++;
});
score1Display = setsWon1;
score2Display = setsWon2;
if (setsWon1 > setsWon2) winnerTeam = match.team1;
if (setsWon2 > setsWon1) winnerTeam = match.team2;
} else {
score1Display = match.score1;
score2Display = match.score2;
if (match.penaltyScore1 !== null) {
if (match.penaltyScore1 > match.penaltyScore2) winnerTeam = match.team1;
if (match.penaltyScore2 > match.penaltyScore1) winnerTeam = match.team2;
} else {
if (match.score1 > match.score2) winnerTeam = match.team1;
if (match.score2 > match.score1) winnerTeam = match.team2;
}
}
}
let team1Text = match.team1 || '<i>Por definir</i>';
let team2Text = match.team2 || '<i>Por definir</i>';
if (winnerTeam) {
if (winnerTeam === match.team1) {
team1Text += ' (G)';
team2Text += ' (P)';
} else {
team1Text += ' (P)';
team2Text += ' (G)';
}
}
const team1HTML = `<div class="flex justify-between items-center ${winnerTeam === match.team1 ? 'font-bold text-white' : 'text-gray-300'}"><span>${team1Text}</span><span class="font-black text-lg">${isPlayed ? score1Display : ''}</span></div>`;
const team2HTML = `<div class="flex justify-between items-center ${winnerTeam === match.team2 ? 'font-bold text-white' : 'text-gray-300'}"><span>${team2Text}</span><span class="font-black text-lg">${isPlayed ? score2Display : ''}</span></div>`;
let separatorHTML;
if (isPlayed) {
let details = '';
if (isVoley) {
details = `Sets: ${match.sets.map(s => `${s.score1}-${s.score2}`).join(', ')}`;
} else if (match.penaltyScore1 !== null) {
details = `(Pen: ${match.penaltyScore1}-${match.penaltyScore2})`;
}
separatorHTML = `<div class="text-center text-xs text-emerald-400 font-semibold my-1">Finalizado <span class="block text-gray-400 font-normal">${details}</span></div>`;
} else if (canPlay) {
separatorHTML = `<div class="text-center my-1"><button class="bg-violet-600 text-white px-3 py-0.5 rounded text-xs font-semibold hover:bg-violet-700">Jugar</button></div>`;
} else {
separatorHTML = `<hr class="my-1 border-gray-700">`;
}
html += `<div class="mt-8 p-4">
<h3 class="text-center font-bold mb-4 text-white text-xl">Partido por el 3er Puesto</h3>
<div class="glass-effect p-3 rounded-lg cursor-pointer hover:border-violet-500 max-w-xs mx-auto" data-match-id="${match.id}" data-round-index="${match.roundIndex}">
${team1HTML}
${separatorHTML}
${team2HTML}
</div>
</div>`;
}
return html;
},
getLeagueStandingsHTML(tournament) {
const stats = {};
tournament.teams.forEach(team => { stats[team] = { pj: 0, pg: 0, pe: 0, pp: 0, gf: 0, gc: 0, dg: 0, pts: 0 }; });
tournament.rounds.forEach(round => {
round.matches.forEach(match => {
const isVoley = tournament.sport.toLowerCase().includes('voley');
const isPlayed = isVoley ? (match.sets && match.sets.length > 0) : match.score1 !== null;
if (isPlayed) {
const t1 = match.team1, t2 = match.team2;
stats[t1].pj++; stats[t2].pj++;
if (isVoley) {
let setsWon1 = 0, setsWon2 = 0;
match.sets.forEach(set => {
stats[t1].gf += set.score1;
stats[t2].gf += set.score2;
stats[t1].gc += set.score2;
stats[t2].gc += set.score1;
if (set.score1 > set.score2) setsWon1++; else setsWon2++;
});
if (setsWon1 > setsWon2) {
stats[t1].pg++;
stats[t2].pp++;
stats[t1].pts += 3;
} else if (setsWon2 > setsWon1) {
stats[t2].pg++;
stats[t1].pp++;
stats[t2].pts += 3;
} else {
stats[t1].pe++;
stats[t2].pe++;
stats[t1].pts += 1;
stats[t2].pts += 1;
}
} else {
const s1 = match.score1, s2 = match.score2;
stats[t1].gf += s1; stats[t2].gf += s2;
stats[t1].gc += s2; stats[t2].gc += s1;
if (match.penaltyScore1 !== null && match.penaltyScore1 !== undefined) {
const winner = match.penaltyScore1 > match.penaltyScore2 ? t1 : t2;
const loser = winner === t1 ? t2 : t1;
stats[winner].pg++;
stats[loser].pp++;
stats[winner].pts += 3;
} else if (s1 > s2) {
stats[t1].pg++; stats[t2].pp++; stats[t1].pts += 3;
} else if (s2 > s1) {
stats[t2].pg++; stats[t1].pp++; stats[t2].pts += 3;
} else {
stats[t1].pe++; stats[t2].pe++; stats[t1].pts++; stats[t2].pts++;
}
}
}
});
});
Object.keys(stats).forEach(t => { stats[t].dg = stats[t].gf - stats[t].gc; });
const sorted = Object.entries(stats).sort(([,a], [,b]) => b.pts - a.pts || b.dg - a.dg || b.gf - a.gf);
let table = `<div class="overflow-x-auto glass-effect rounded-xl"><table class="min-w-full">
<thead class="bg-gray-900 bg-opacity-50"><tr>
<th class="p-3 text-left text-xs font-medium uppercase text-gray-300">Equipo</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">PTS</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">PJ</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">PG</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">PE</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">PP</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">GF</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">GC</th><th class="p-3 text-center text-xs font-medium uppercase text-gray-300">DG</th>
</tr></thead><tbody>`;
sorted.forEach(([team, data]) => {
table += `<tr class="border-t border-gray-700">
<td class="p-3 font-medium text-white">${team}</td><td class="p-3 text-center font-bold text-violet-400">${data.pts}</td><td class="p-3 text-center">${data.pj}</td><td class="p-3 text-center">${data.pg}</td><td class="p-3 text-center">${data.pe}</td><td class="p-3 text-center">${data.pp}</td><td class="p-3 text-center">${data.gf}</td><td class="p-3 text-center">${data.gc}</td><td class="p-3 text-center">${data.dg}</td>
</tr>`;
});
table += `</tbody></table></div>`;
return table;
},
renderClassification(tournament) {
const container = document.getElementById('tab-content-clasificacion');
if (tournament.format !== 'liga') {
container.innerHTML = '';
return;
}
container.innerHTML = this.getLeagueStandingsHTML(tournament);
},
renderShareTab(tournament) {
const qrContainer = document.getElementById('qrcode-container');
qrContainer.innerHTML = '';
const publicUrl = `${window.location.origin}${window.location.pathname}?user=${this.state.currentUser}&torneo=${tournament.id}`;
new QRCode(qrContainer, { text: publicUrl, width: 128, height: 128 });
},
renderPublicView(username, tournamentId) {
const allTournaments = [];
const users = JSON.parse(localStorage.getItem('users_v2')) || [];
const targetUser = users.find(u => u.username === username);
if (!targetUser) {
this.ui.views.public.innerHTML = `<p class="text-center text-red-500">Usuario no encontrado.</p>`;
this.showView('public');
return;
}
const userTournaments = JSON.parse(localStorage.getItem(`tournaments_${username}`)) || [];
const tournament = userTournaments.find(t => t.id === tournamentId);
if (!tournament) {
this.ui.views.public.innerHTML = `<p class="text-center text-red-500">Torneo no encontrado.</p>`;
this.showView('public');
return;
}
let mainContentHTML = '';
let title = '';
if (tournament.format === 'liga') {
mainContentHTML = this.getLeagueStandingsHTML(tournament);
title = 'Clasificación';
} else if (tournament.format === 'eliminatoria') {
mainContentHTML = this.renderBracketHTML(tournament, "Cuadro del Torneo");
title = 'Cuadro del Torneo';
}
this.ui.views.public.innerHTML = `
<header class="mb-8 text-center"><h1 class="text-4xl font-bold text-white">${tournament.name}</h1><p class="text-xl text-gray-400 mt-1">${tournament.sport}</p></header>
<div class="space-y-8"><div><h2 class="text-2xl font-semibold mb-4 border-b border-gray-700 pb-2 text-white">${title}</h2>${mainContentHTML}</div></div>`;
this.showView('public');
},
openLiveMatchModal(tournamentId, roundIndex, matchId, bracketType) {
const tournament = this.state.tournaments.find(t => t.id === tournamentId);
let match;
if (tournament.format === 'eliminatoria') {
match = (matchId === 'third_place') ? tournament.thirdPlaceMatch : tournament.rounds[roundIndex].matches.find(m => m.id === matchId);
} else { // Liga
match = tournament.rounds[roundIndex].matches.find(m => m.id === matchId);
}
if (!match || !match.team1 || !match.team2 || (match.score1 !== null || (match.sets && match.sets.length > 0))) return;
this.state.liveMatch = { tournamentId, roundIndex, matchId, bracketType, score1: 0, score2: 0, penaltyScore1: 0, penaltyScore2: 0, isPenaltyShootout: false, timerInterval: null, timeSeconds: 0, period: 1, sets: [], currentSet: { score1: 0, score2: 0 } };
if (tournament.sport.toLowerCase().includes('voley')) {
document.getElementById('voley-team1-name').textContent = match.team1;
document.getElementById('voley-team2-name').textContent = match.team2;
this.updateVoleyModal();
this.ui.volleyballMatchModal.classList.add('flex');
} else {
document.getElementById('live-team1-name').textContent = match.team1;
document.getElementById('live-team2-name').textContent = match.team2;
document.getElementById('live-team1-score').textContent = 0;
document.getElementById('live-team2-score').textContent = 0;
document.getElementById('penalty-team1-score').textContent = 0;
document.getElementById('penalty-team2-score').textContent = 0;
document.getElementById('penalty-shootout-section').classList.add('hidden');
document.getElementById('tiebreaker-options').classList.add('hidden');
document.getElementById('standard-match-main-controls').classList.remove('hidden');
document.getElementById('finish-match-btn').textContent = 'Finalizar Partido';
document.getElementById('finish-match-btn').classList.remove('hidden');
document.getElementById('live-match-period').textContent = `Tiempo: 1`;
this.updateTimerDisplay();
this.ui.liveMatchModal.classList.add('flex');
}
},
closeLiveMatchModal() {
clearInterval(this.state.liveMatch.timerInterval);
this.ui.liveMatchModal.classList.remove('flex');
this.ui.volleyballMatchModal.classList.remove('flex');
},
updateTimerDisplay() {
const minutes = Math.floor(this.state.liveMatch.timeSeconds / 60).toString().padStart(2, '0');
const seconds = (this.state.liveMatch.timeSeconds % 60).toString().padStart(2, '0');
document.getElementById('live-match-timer').textContent = `${minutes}:${seconds}`;
},
updateVoleyModal() {
const { sets, currentSet } = this.state.liveMatch;
document.getElementById('voley-team1-score').textContent = currentSet.score1;
document.getElementById('voley-team2-score').textContent = currentSet.score2;
document.getElementById('voley-current-set-label').textContent = `Set Actual: ${sets.length + 1}`;
let setsWon1 = 0, setsWon2 = 0;
sets.forEach(set => {
if (set.score1 > set.score2) setsWon1++;
else setsWon2++;
});
document.getElementById('voley-sets-won').textContent = `Sets: ${setsWon1} - ${setsWon2}`;
document.getElementById('voley-previous-sets').innerHTML = sets.map((s, i) => `Set ${i+1}: ${s.score1}-${s.score2}`).join(' | ');
},
showPodium(tournament) {
let first = 'No definido', second = 'No definido', third = 'No definido';
if (tournament.format === 'liga') {
const standingsHTML = this.getLeagueStandingsHTML(tournament);
const div = document.createElement('div');
div.innerHTML = standingsHTML;
const rows = div.querySelectorAll('tbody tr');
first = rows[0] ? rows[0].cells[0].textContent : 'No definido';
second = rows[1] ? rows[1].cells[0].textContent : 'No definido';
third = rows[2] ? rows[2].cells[0].textContent : 'No definido';
} else if (tournament.format === 'eliminatoria') {
first = tournament.winner || 'No definido';
second = tournament.runnerUp || 'No definido';
third = tournament.thirdPlace || 'No definido';
}
document.getElementById('podium-first').textContent = first;
document.getElementById('podium-second').textContent = second;
document.getElementById('podium-third').textContent = third;
this.ui.podiumModal.classList.add('flex');
},
// --- FIXTURE GENERATION & LOGIC ---
generateFixture(teams, format) {
if (format === 'liga') return { rounds: this.generateRoundRobin(teams) };
if (format === 'eliminatoria') return this.generateSingleElimination(teams);
},
generateRoundRobin(teams) {
const rounds = [];
let teamList = [...teams];
if (teamList.length % 2 !== 0) teamList.push("DESCANSA");
const numRounds = teamList.length - 1;
for (let i = 0; i < numRounds; i++) {
const round = { matches: [] };
for (let j = 0; j < teamList.length / 2; j++) {
const team1 = teamList[j], team2 = teamList[teamList.length - 1 - j];
if (team1 !== "DESCANSA" && team2 !== "DESCANSA") {
round.matches.push({ id: `r${i}m${j}`, roundIndex: i, team1, team2, score1: null, score2: null, sets: [], penaltyScore1: null, penaltyScore2: null });
}
}
rounds.push(round);
teamList.splice(1, 0, teamList.pop());
}
return rounds;
},
generateSingleElimination(teams) {
let teamList = [...teams].sort(() => 0.5 - Math.random());
let byeTeam = null;
let teamsThatHadBye = [];
if (teamList.length % 2 !== 0) {
const byeIndex = Math.floor(Math.random() * teamList.length);
byeTeam = teamList.splice(byeIndex, 1)[0];
teamsThatHadBye.push(byeTeam);
}
const firstRoundMatches = [];
for (let i = 0; i < teamList.length; i += 2) {
firstRoundMatches.push({ id: `r0m${i/2}`, roundIndex: 0, team1: teamList[i], team2: teamList[i+1], score1: null, score2: null, sets: [], penaltyScore1: null, penaltyScore2: null });
}
return {
rounds: [{ matches: firstRoundMatches }],
roundByes: { 0: byeTeam },
thirdPlaceMatch: null,
thirdPlaceWinner: null,
teamsThatHadBye: teamsThatHadBye
};
},
// =================================================================
// BUG FIX: Replaced the entire updateBrackets function
// =================================================================
updateBrackets(tournament, finishedMatch) {
if (tournament.format !== 'eliminatoria') return;
const isVoley = tournament.sport.toLowerCase().includes('voley');
const getMatchResult = (match) => {
let winner = null;
let loser = null;
if (isVoley) {
// For voley, score1 and score2 are set counts.
if (match.score1 > match.score2) {
winner = match.team1;
loser = match.team2;
} else {
winner = match.team2;
loser = match.team1;
}
} else {
// For other sports
if (match.penaltyScore1 !== null && match.penaltyScore1 !== undefined) {
if (match.penaltyScore1 > match.penaltyScore2) {
winner = match.team1;
loser = match.team2;
} else {
winner = match.team2;
loser = match.team1;
}
} else if (match.score1 !== match.score2) {
if (match.score1 > match.score2) {
winner = match.team1;
loser = match.team2;
} else {
winner = match.team2;
loser = match.team1;
}
}
}
return { winner, loser };
};
const currentRoundIndex = parseInt(finishedMatch.roundIndex);
// Handle third place match finish
if (currentRoundIndex === -1) {
tournament.thirdPlace = getMatchResult(finishedMatch).winner;
return;
}
const currentRound = tournament.rounds[currentRoundIndex];
// Check if all matches in the current round are finished
if (!currentRound.matches.every(m => m.score1 !== null)) {
return;
}
const winners = currentRound.matches.map(m => getMatchResult(m).winner);
if (tournament.roundByes && tournament.roundByes[currentRoundIndex]) {
winners.push(tournament.roundByes[currentRoundIndex]);
}
// Check for tournament end (final match finished)
if (winners.length === 1 && tournament.rounds.length === currentRoundIndex + 1) {
tournament.winner = winners[0];
const finalMatch = currentRound.matches[0];
tournament.runnerUp = getMatchResult(finalMatch).loser;
tournament.isFinished = true;
if (!tournament.thirdPlaceMatch && tournament.thirdPlaceWinner) {
tournament.thirdPlace = tournament.thirdPlaceWinner;
}
return;
}
const nextRoundIndex = currentRoundIndex + 1;
let nextRoundParticipants = [...winners];
let nextBye = null;
// Handle byes for the next round
if (nextRoundParticipants.length % 2 !== 0 && nextRoundParticipants.length > 1) {
let eligibleForBye = nextRoundParticipants.filter(team => !tournament.teamsThatHadBye.includes(team));
if (eligibleForBye.length === 0) {
eligibleForBye = nextRoundParticipants;
}
const byeIndex = Math.floor(Math.random() * eligibleForBye.length);
nextBye = eligibleForBye[byeIndex];
tournament.teamsThatHadBye.push(nextBye);
nextRoundParticipants = nextRoundParticipants.filter(team => team !== nextBye);
}
tournament.roundByes[nextRoundIndex] = nextBye;
// Create matches for the next round
const nextRoundMatches = [];
for (let i = 0; i < nextRoundParticipants.length; i += 2) {
nextRoundMatches.push({
id: `r${nextRoundIndex}m${i/2}`,
roundIndex: nextRoundIndex,
team1: nextRoundParticipants[i],
team2: nextRoundParticipants[i+1] || null,
score1: null, score2: null,
sets: [], // Ensure sets is always present for new matches
penaltyScore1: null, penaltyScore2: null
});
}
if (nextRoundMatches.length > 0) {
if (tournament.rounds.length === nextRoundIndex) {
tournament.rounds.push({ matches: nextRoundMatches });
} else {
tournament.rounds[nextRoundIndex].matches.push(...nextRoundMatches);
}
}
// Create third place match after semifinals
if (winners.length === 2 && currentRound.matches.length === 2) { // Semifinal round
const losers = currentRound.matches.map(m => getMatchResult(m).loser);
tournament.thirdPlaceMatch = {
id: 'third_place',
roundIndex: -1,
team1: losers[0],
team2: losers[1],
score1: null, score2: null,
sets: [], // FIX: Added missing sets property
penaltyScore1: null, penaltyScore2: null
};
} else if (winners.length === 2 && currentRound.matches.length === 1) { // Case where one semifinalist had a bye
const loser = getMatchResult(currentRound.matches[0]).loser;
tournament.thirdPlaceWinner = loser; // This team is 3rd or 4th, but no match is played
}
},
// --- EVENT LISTENERS ---
attachEventListeners() {
// Login/Register
document.getElementById('show-register-link').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('login-form-container').classList.add('hidden');
document.getElementById('register-form-container').classList.remove('hidden');
});
document.getElementById('show-login-link').addEventListener('click', (e) => {
e.preventDefault();
document.getElementById('register-form-container').classList.add('hidden');
document.getElementById('login-form-container').classList.remove('hidden');
});
document.getElementById('register-btn').addEventListener('click', () => {
const username = document.getElementById('register-username').value;
const password = document.getElementById('register-password').value;
if (!username || !password) { this.showAlert('Por favor, completa todos los campos.'); return; }
let users = JSON.parse(localStorage.getItem('users_v2')) || [];
if (users.find(u => u.username === username)) { this.showAlert('Este nombre de usuario ya está en uso.'); return; }
users.push({ username, password });
localStorage.setItem('users_v2', JSON.stringify(users));
this.showAlert('¡Registro exitoso! Ahora puedes iniciar sesión.', 'Éxito');
document.getElementById('show-login-link').click();
});
document.getElementById('login-btn').addEventListener('click', () => {
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
let users = JSON.parse(localStorage.getItem('users_v2')) || [];
const foundUser = users.find(u => u.username === username && u.password === password);
if (foundUser) {
this.state.currentUser = foundUser.username;
sessionStorage.setItem('currentUser', foundUser.username);
this.loadTournaments();
this.renderDashboard();
this.showView('dashboard');
} else {
this.showAlert('Usuario o contraseña incorrectos.', 'Error de Acceso');
}
});
document.getElementById('guest-btn').addEventListener('click', () => {
this.showAlert("Ingresarás como invitado. Los torneos que crees no se guardarán al cerrar la sesión. Para guardar tu progreso, por favor regístrate.");
this.state.currentUser = 'Invitado';
sessionStorage.setItem('currentUser', 'Invitado');
this.loadTournaments();
this.renderDashboard();
this.showView('dashboard');
});
document.getElementById('logout-btn').addEventListener('click', () => {
this.state.currentUser = null;
sessionStorage.removeItem('currentUser');
this.state.tournaments = [];
this.showView('login');
});
// Dashboard
document.getElementById('tournament-list').addEventListener('click', (e) => {
const button = e.target.closest('button');
const card = e.target.closest('[data-id]');
if (!card) return;
const tournamentId = card.dataset.id;
const action = button ? button.dataset.action : e.target.closest('[data-action]')?.dataset.action;
if (action === 'delete') {
e.stopPropagation();
this.showConfirm('¿Estás seguro de que quieres eliminar este torneo?', () => {
this.state.tournaments = this.state.tournaments.filter(t => t.id !== tournamentId);
this.saveTournaments();
this.renderDashboard();
});
} else if (action === 'edit-name') {
e.stopPropagation();
const h3 = card.querySelector('h3');
const currentName = h3.textContent;
h3.innerHTML = `<input type="text" class="w-full bg-gray-900 border border-violet-500 rounded p-1 text-lg font-bold text-white" value="${currentName}">`;
const input = h3.querySelector('input');
input.focus();
input.select();
const saveName = () => {
const newName = input.value.trim();
if (newName && newName !== currentName) {
const tournament = this.state.tournaments.find(t => t.id === tournamentId);
tournament.name = newName;
this.saveTournaments();
h3.textContent = newName;
} else {
h3.textContent = currentName;
}
};
input.addEventListener('blur', saveName, { once: true });
input.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') {
input.blur();
} else if (ev.key === 'Escape') {
h3.textContent = currentName;
input.removeEventListener('blur', saveName);
}
});
} else if (action === 'view') {
this.state.activeTournamentId = tournamentId;
this.renderManageView();
this.showView('manage');
}
});
// Create Tournament & Navigation
document.getElementById('go-to-create-btn').addEventListener('click', () => this.showView('create'));
document.querySelectorAll('.back-to-dashboard').forEach(btn => btn.addEventListener('click', () => this.showView('dashboard')));
document.getElementById('next-step-2-btn').addEventListener('click', () => { document.getElementById('step-1').classList.add('hidden'); document.getElementById('step-2').classList.remove('hidden'); });
document.getElementById('back-step-1-btn').addEventListener('click', () => { document.getElementById('step-2').classList.add('hidden'); document.getElementById('step-1').classList.remove('hidden'); });
document.getElementById('next-step-3-btn').addEventListener('click', () => {
const teams = Array.from(document.getElementById('teams-list').children);
if (teams.length < 2) {
this.showAlert('Debes añadir al menos 2 equipos para continuar.');
return;
}
this.showConfirm(
`Has añadido ${teams.length} equipos. Una vez generado el fixture, no podrás cambiar la lista de equipos. ¿Estás seguro de que quieres continuar?`,
() => {
document.getElementById('step-2').classList.add('hidden');
document.getElementById('step-3').classList.remove('hidden');
},
'Revisión Final'
);
});
document.getElementById('back-step-2-btn').addEventListener('click', () => { document.getElementById('step-3').classList.add('hidden'); document.getElementById('step-2').classList.remove('hidden'); });
document.getElementById('add-team-btn').addEventListener('click', () => {
const input = document.getElementById('team-name-input');
if (input.value.trim()) {
const li = document.createElement('li');
li.className = 'flex justify-between items-center bg-gray-800 p-2 rounded-md text-white';
li.innerHTML = `<span>${input.value.trim()}</span><button class="remove-team-btn text-red-500 hover:text-red-400 font-bold">X</button>`;
document.getElementById('teams-list').appendChild(li);
input.value = '';
}
});
document.getElementById('teams-list').addEventListener('click', e => { if (e.target.classList.contains('remove-team-btn')) e.target.parentElement.remove(); });
document.getElementById('generate-fixture-btn').addEventListener('click', () => {
const teams = Array.from(document.getElementById('teams-list').children).map(li => li.firstElementChild.textContent);
if (teams.length < 2) { this.showAlert('Necesitas al menos 2 equipos.'); return; }
const newTournament = {
id: `t_${Date.now()}`,
name: document.getElementById('tournament-name').value || 'Sin Título',
sport: document.getElementById('tournament-sport').value || 'Deporte',
teams,
format: document.getElementById('tournament-format').value,
creationDate: new Date().toLocaleDateString('es-ES'),
...this.generateFixture(teams, document.getElementById('tournament-format').value)
};
this.state.tournaments.push(newTournament);
this.saveTournaments();
this.renderDashboard();
this.showView('dashboard');
document.getElementById('create-tournament-view').querySelectorAll('input[type="text"]').forEach(i => i.value = '');
document.getElementById('teams-list').innerHTML = '';
document.getElementById('step-3').classList.add('hidden'); document.getElementById('step-1').classList.remove('hidden');
});
// Manage View Tabs & Matches
document.querySelector('.tab-btn').parentElement.addEventListener('click', e => {
if (e.target.classList.contains('tab-btn')) {
const tab = e.target.dataset.tab;
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
document.getElementById(`tab-content-${tab}`).classList.remove('hidden');
document.getElementById(`pdf-export-area`).querySelector('#tab-content-partidos').style.display = (tab === 'partidos') ? 'block' : 'none';
document.getElementById(`pdf-export-area`).querySelector('#tab-content-clasificacion').style.display = (tab === 'clasificacion') ? 'block' : 'none';
}
});
document.getElementById('tab-content-partidos').addEventListener('click', e => {
const matchDiv = e.target.closest('[data-match-id]');
if (matchDiv) {
const bracketType = matchDiv.dataset.bracketType;
this.openLiveMatchModal(this.state.activeTournamentId, matchDiv.dataset.roundIndex, matchDiv.dataset.matchId, bracketType);
}
});
// Standard Modal Listeners
document.getElementById('timer-start').addEventListener('click', () => {
if (!this.state.liveMatch.timerInterval) this.state.liveMatch.timerInterval = setInterval(() => { this.state.liveMatch.timeSeconds++; this.updateTimerDisplay(); }, 1000);
});
document.getElementById('timer-pause').addEventListener('click', () => { clearInterval(this.state.liveMatch.timerInterval); this.state.liveMatch.timerInterval = null; });
document.getElementById('timer-reset').addEventListener('click', () => {
clearInterval(this.state.liveMatch.timerInterval);
this.state.liveMatch.timerInterval = null;
this.state.liveMatch.timeSeconds = 0;
this.state.liveMatch.period = 1;
document.getElementById('live-match-period').textContent = `Tiempo: 1`;
this.updateTimerDisplay();
});
document.getElementById('timer-new-period').addEventListener('click', () => {
clearInterval(this.state.liveMatch.timerInterval);
this.state.liveMatch.timerInterval = null;
this.state.liveMatch.timeSeconds = 0;
this.state.liveMatch.period++;
document.getElementById('live-match-period').textContent = `Tiempo: ${this.state.liveMatch.period}`;
this.updateTimerDisplay();
});
this.ui.liveMatchModal.addEventListener('click', e => {
const target = e.target;
if (target.classList.contains('score-btn') || target.classList.contains('penalty-score-btn')) {
const isPenalty = target.classList.contains('penalty-score-btn');
const teamNum = target.dataset.team;
const op = target.dataset.op;
const scoreKey = isPenalty ? `penaltyScore${teamNum}` : `score${teamNum}`;
const scoreEl = document.getElementById(isPenalty ? `penalty-team${teamNum}-score` : `live-team${teamNum}-score`);
if (op === 'add') this.state.liveMatch[scoreKey]++;
else if (op === 'sub' && this.state.liveMatch[scoreKey] > 0) this.state.liveMatch[scoreKey]--;
scoreEl.textContent = this.state.liveMatch[scoreKey];
}
});
document.getElementById('finish-match-btn').addEventListener('click', () => {
this.showConfirm("¿Estás seguro de finalizar el partido? Esta acción guardará el resultado y no se podrá deshacer.", () => {
const { tournamentId, score1, score2, isPenaltyShootout, roundIndex, matchId, bracketType } = this.state.liveMatch;
const tournament = this.state.tournaments.find(t => t.id === tournamentId);
const sport = tournament.sport.toLowerCase();
const noPenaltySports = ['voley', 'basquet', 'baloncesto'];
const needsTiebreaker = !noPenaltySports.some(s => sport.includes(s));
if (score1 === score2 && needsTiebreaker && !isPenaltyShootout) {
document.getElementById('standard-match-main-controls').classList.add('hidden');
document.getElementById('tiebreaker-options').classList.remove('hidden');
document.getElementById('finish-match-btn').classList.add('hidden');
return;
}
let match;
if (tournament.format === 'eliminatoria') {
match = (matchId === 'third_place') ? tournament.thirdPlaceMatch : tournament.rounds[roundIndex].matches.find(m => m.id === matchId);
} else {
match = tournament.rounds[roundIndex].matches.find(m => m.id === matchId);
}
match.score1 = score1;
match.score2 = score2;
if (isPenaltyShootout) {
match.penaltyScore1 = this.state.liveMatch.penaltyScore1;
match.penaltyScore2 = this.state.liveMatch.penaltyScore2;
}
this.updateBrackets(tournament, match);
this.saveTournaments();
this.renderManageView();
this.closeLiveMatchModal();
}, "Finalizar Partido");
});
document.getElementById('start-penalty-btn').addEventListener('click', () => {
this.state.liveMatch.isPenaltyShootout = true;
document.getElementById('tiebreaker-options').classList.add('hidden');
document.getElementById('penalty-shootout-section').classList.remove('hidden');
const finishBtn = document.getElementById('finish-match-btn');
finishBtn.textContent = 'Finalizar Penales';
finishBtn.classList.remove('hidden');
});
document.getElementById('finish-as-draw-btn').addEventListener('click', () => {
const { tournamentId, roundIndex, matchId, score1, score2 } = this.state.liveMatch;
const tournament = this.state.tournaments.find(t => t.id === tournamentId);
let match = (matchId === 'third_place') ? tournament.thirdPlaceMatch : tournament.rounds[roundIndex].matches.find(m => m.id === matchId);
match.score1 = score1;
match.score2 = score2;
match.penaltyScore1 = null;
match.penaltyScore2 = null;
this.updateBrackets(tournament, match);
this.saveTournaments();
this.renderManageView();
this.closeLiveMatchModal();
});
document.getElementById('cancel-match-btn').addEventListener('click', () => this.closeLiveMatchModal());
// Voley Modal Listeners
this.ui.volleyballMatchModal.addEventListener('click', e => {
if (e.target.classList.contains('voley-score-btn')) {
const teamNum = e.target.dataset.team;
const op = e.target.dataset.op;
const scoreKey = `score${teamNum}`;
if (op === 'add') this.state.liveMatch.currentSet[scoreKey]++;
else if (op === 'sub' && this.state.liveMatch.currentSet[scoreKey] > 0) this.state.liveMatch.currentSet[scoreKey]--;
this.updateVoleyModal();
}
});
document.getElementById('voley-new-set-btn').addEventListener('click', () => {
this.state.liveMatch.sets.push({...this.state.liveMatch.currentSet});
this.state.liveMatch.currentSet = { score1: 0, score2: 0 };
this.updateVoleyModal();
});
document.getElementById('voley-finish-match-btn').addEventListener('click', () => {
this.showConfirm("¿Estás seguro de finalizar el partido? Esta acción guardará el resultado y no se podrá deshacer.", () => {
const { tournamentId, roundIndex, matchId, sets, currentSet, bracketType } = this.state.liveMatch;
const finalSets = [...sets];
if(currentSet.score1 > 0 || currentSet.score2 > 0) {
finalSets.push({...currentSet});
}
const tournament = this.state.tournaments.find(t => t.id === tournamentId);
let match;
if (tournament.format === 'eliminatoria') {
match = (matchId === 'third_place') ? tournament.thirdPlaceMatch : tournament.rounds[roundIndex].matches.find(m => m.id === matchId);
} else {
match = tournament.rounds[roundIndex].matches.find(m => m.id === matchId);
}
match.sets = finalSets;
let setsWon1 = 0, setsWon2 = 0;
match.sets.forEach(set => {
if(set.score1 > set.score2) setsWon1++; else setsWon2++;
});
match.score1 = setsWon1;
match.score2 = setsWon2;
this.updateBrackets(tournament, match);
this.saveTournaments();
this.renderManageView();
this.closeLiveMatchModal();
}, "Finalizar Partido");
});
document.getElementById('voley-cancel-match-btn').addEventListener('click', () => this.closeLiveMatchModal());
// Podium & Export Listeners
document.getElementById('show-podium-btn').addEventListener('click', () => {
const tournament = this.state.tournaments.find(t => t.id === this.state.activeTournamentId);
this.showPodium(tournament);
});
document.getElementById('close-podium-btn').addEventListener('click', () => {
this.ui.podiumModal.classList.remove('flex');
});
document.getElementById('export-pdf-btn').addEventListener('click', () => {
const activeTabContent = document.querySelector('#pdf-export-area .tab-content:not(.hidden)');
if (!activeTabContent || !activeTabContent.hasChildNodes()) {
this.showAlert("No hay contenido visible para exportar en esta pestaña.", "Error de Exportación");
return;
}
const { jsPDF } = window.jspdf;
html2canvas(activeTabContent, { scale: 2, backgroundColor: '#0f0c29' }).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const ratio = canvasWidth / canvasHeight;
const width = pdfWidth - 20; // Add some margin
const height = width / ratio;
pdf.addImage(imgData, 'PNG', 10, 10, width, height);
pdf.save("torneo.pdf");
});
});
}
};
App.init();
});
</script>
</body>
</html>