<!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>