Traduccion parte de la piscina

This commit is contained in:
JorgeLuisOZ 2025-06-09 23:51:57 -06:00
parent dc8bc854f9
commit 56cba7f7ab
5 changed files with 533 additions and 74 deletions

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -128,7 +128,6 @@ window.addEventListener('DOMContentLoaded', async () => {
if (user?.name) {
document.getElementById("nombreUsuarioHeader").textContent = user.name;
document.getElementById("nombreUsuarioDropdown").textContent = "Coach " + user.name;
}
} catch (err) {
console.error("❌ Error al obtener datos del usuario:", err);

View File

@ -1,4 +1,52 @@
document.addEventListener('DOMContentLoaded', async () => {
const userId = sessionStorage.getItem("userId");
if (!userId) {
alert("Sesión expirada, inicia sesión de nuevo.");
window.location.href = "index.html";
return;
}
try {
const res = await fetch(`/api/users/${userId}`);
const user = await res.json();
if (user?.name) {
document.getElementById("nombreUsuarioHeader").textContent = user.name;
}
let lang = 'es'; // valor por defecto
// 1. Determinar idioma
if (sessionStorage.getItem('langJustChanged') === 'true') {
lang = sessionStorage.getItem('lang') || user.language || 'es';
} else if (sessionStorage.getItem('lang')) {
lang = sessionStorage.getItem('lang');
} else if (user.language) {
lang = user.language;
sessionStorage.setItem('lang', lang);
}
window.tLang = lang;
localStorage.setItem('lang', lang); // opcional si usas localStorage
// 🌀 Recarga solo una vez con delay para evitar parpadeo inmediato
if (!sessionStorage.getItem('langLoadedOnce')) {
sessionStorage.setItem('langLoadedOnce', 'true');
setTimeout(() => location.reload(), 100); //
return;
}
// ✅ Aplicar traducciones después de confirmar idioma
const selector = document.getElementById("langSelector");
if (selector) selector.value = lang;
aplicarTraducciones();
actualizarTextosDinamicos();
} catch (err) {
console.error("❌ Error al obtener datos del usuario:", err);
}
const rutinaId = new URLSearchParams(window.location.search).get('routineId');
if (!rutinaId) return alert('No se proporcionó ID de rutina.');
@ -38,21 +86,24 @@ atletas.forEach(a => {
});
let figuraActual = null; // 🔁 Guardar la figura seleccionada
select.addEventListener('change', () => {
const datos = JSON.parse(select.value || '{}');
select.dataset.id = datos.id; // << GUARDAS el ObjectId real
select.dataset.id = datos.id;
const idInput = document.getElementById('idPersonalizado');
if (datos.idPers) {
document.getElementById('idPersonalizado').value = datos.idPers;
document.getElementById('idPersonalizado').disabled = true;
idInput.value = datos.idPers;
idInput.disabled = true;
} else {
document.getElementById('idPersonalizado').value = '';
document.getElementById('idPersonalizado').disabled = false;
idInput.value = '';
idInput.disabled = false;
}
});
let figurasFINA = [];
try {
let figurasFINA = [];
try {
const res = await fetch('/catalog/figurasFINA.json');
figurasFINA = await res.json();
@ -67,18 +118,20 @@ select.addEventListener('change', () => {
matches.forEach(figura => {
const item = document.createElement('button');
item.type = 'button';
item.className = 'list-group-item list-group-item-action';
item.textContent = figura.nombre;
item.onclick = () => {
inputFigura.value = figura.nombre;
inputFigura.title = `${figura.descripcion} (${figura.categoria})`;
sugerenciasBox.innerHTML = '';
figuraActual = figura; // guardamos figura activa
mostrarPreview(figura);
const codigoInput = document.getElementById("codigoElemento");
if (codigoInput && figura.codigoElemento) {
codigoInput.value = figura.codigoElemento;
}
};
};
sugerenciasBox.appendChild(item);
});
});
@ -88,18 +141,39 @@ select.addEventListener('change', () => {
});
function mostrarPreview(figura) {
const nombreTrad = window.figurasTraducidas?.[figura.nombre]?.[window.tLang] || figura.nombre;
const descripcionTrad = window.descripcionesTraducidas?.[figura.descripcion]?.[window.tLang] || figura.descripcion;
// Traducción visual adicional debajo del input
const nombreTraducidoVisual = document.getElementById("nombreFiguraTraducido");
if (nombreTraducidoVisual) {
nombreTraducidoVisual.textContent = nombreTrad;
}
document.getElementById('previewFigura').innerHTML = `
<img src="catalog/figuras/${figura.imagen}" alt="${figura.nombre}" class="img-fluid rounded border" style="max-height:150px;" />
<p class="mt-2">${figura.descripcion} <span class="badge bg-info">${figura.categoria}</span></p>
<img src="catalog/figuras/${figura.imagen}" alt="${nombreTrad}" class="img-fluid rounded border" style="max-height:150px;" />
<p class="mt-2"><strong>${nombreTrad}</strong><br>${descripcionTrad} <span class="badge bg-info">${figura.categoria}</span></p>
`;
const codigoInput = document.getElementById("codigoElemento");
if (codigoInput && figura.codigoElemento) {
codigoInput.value = figura.codigoElemento;
}
}
} catch (err) {
console.warn('❌ No se pudo cargar el catálogo FINA:', err);
// Esto es clave: vuelve a mostrar la figura al cambiar idioma
document.getElementById('langSelector').addEventListener('change', () => {
window.tLang = sessionStorage.getItem('lang') || user.language || 'es';
if (figuraActual) {
mostrarPreview(figuraActual); // 🔁 refresca con idioma nuevo
}
});
} catch (err) {
console.warn('❌ No se pudo cargar el catálogo FINA:', err);
}
const tipoPiscinaSelect = document.getElementById('tipoPiscina');
const medidasPiscina = {
@ -652,9 +726,9 @@ formaciones.forEach((f, i) => {
inputFigura.title = '';
btnEditarFormacion.className = 'btn btn-warning btn-sm d-inline-flex align-items-center gap-2';
btnEditarFormacion.innerHTML = '🟡 En edición activa';
btnEditarFormacion.innerHTML = t('editor.edicionActiva');
btnGuardarFormacion.textContent = 'Actualizar Formación';
btnGuardarFormacion.textContent = t('editor.actualizar');
btnGuardarFormacion.classList.remove('btn-primary');
btnGuardarFormacion.classList.add('btn-warning');
} else {
@ -764,3 +838,31 @@ wave.on('audioprocess', (currentTime) => {
});
});
document.getElementById('langSelector').addEventListener('change', async (e) => {
const selectedLang = e.target.value;
sessionStorage.setItem('lang', selectedLang);
window.tLang = selectedLang;
const userId = sessionStorage.getItem("userId");
if (userId) {
await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language: selectedLang })
});
}
// 🟡 Recargar solo 1 vez
sessionStorage.setItem('langJustChanged', 'true');
window.location.reload();
});
function logout() {
sessionStorage.clear();
localStorage.removeItem('lang'); // ✅ Limpia persistencia de idioma
alert("Sesión cerrada");
window.location.href = "../index.html";
}
window.logout = logout;

View File

@ -0,0 +1,355 @@
const traducciones = {
es: {
editor: {
titulo: "Editor de Formación",
tipo: "Tipo",
modalidad: "Modalidad",
agregar: "Agregar Atleta",
seleccionarAtleta: "Seleccionar atleta",
rol: "Rol",
rolSeleccionar: "Seleccionar rol",
volador: "Volador",
pilar: "Pilar",
otro: "Otro",
idPers: "ID personalizado",
idEj: "Ej: A, 1, V2",
figura: "Figura técnica",
figuraEj: "Ej: Barracuda, Flamingo...",
tipoElemento: "Tipo de elemento",
codigoElemento: "Código del elemento",
codigoEj: "Ej: TRE 1A, H4...",
hibrido: "Híbrido",
transicion: "Transición",
btnAgregar: "Añadir Atleta",
editar: "Editar formación",
eliminar: "Eliminar Formación",
guardar: "Guardar Formación",
linea: "Línea de Tiempo",
piscina: "Piscina",
tipoPiscina: "Tipo de piscina",
olimpica: "Olímpica (50m x 25m)",
semiolimpica: "Semiolímpica (25m x 12.5m)",
fosa: "Fosa/Poza (20m x 20m)",
duracion: "Duración (segundos)",
duracionEj: "Ej: 5",
nombre: "Nombre",
nombreEj: "Nombre de la formación",
notas: "Notas tácticas",
notasEj: "Instrucciones, marcajes, etc.",
datos: "Datos de Formación",
play: "▶ Reproducir",
pause: "⏸︎ Pausar",
actualizar: "Actualizar Formación",
sinFigura: "Sin figura",
direccion: "Añadir Dirección",
eliminarDir: "Eliminar Dirección",
edicionActiva: "🟡 En edición activa",
alertaFaltanDatos: "Faltan datos del atleta",
alertaMaxAtletas: "Modalidad \"{MOD}\" permite un máximo de {MAX} atletas.",
alertaClickPiscina: "Haz clic en la piscina para colocarlo",
alertaNombreFaltante: "Agrega nombre de formación y al menos un atleta",
alertaSeleccionaFormacion: "Selecciona una formación desde la línea de tiempo primero.",
confirmEliminar: {
titulo: "¿Eliminar formación?",
texto: "Esta acción no se puede deshacer",
confirmar: "Sí, eliminar",
cancelar: "Cancelar",
eliminado: "La formación ha sido eliminada.",
error: "No se pudo eliminar la formación."
}
},
nav: {
init: "Inicializar Rutina",
equip: "Equipos Disponibles"
},
logout: "Salir"
},
en: {
editor: {
titulo: "Formation Editor",
tipo: "Type",
modalidad: "Modality",
agregar: "Add Athlete",
seleccionarAtleta: "Select athlete",
rol: "Role",
rolSeleccionar: "Select role",
volador: "Flyer",
pilar: "Base",
otro: "Other",
idPers: "Custom ID",
idEj: "E.g.: A, 1, V2",
figura: "Technical figure",
figuraEj: "E.g.: Barracuda, Flamingo...",
tipoElemento: "Element type",
codigoElemento: "Element code",
codigoEj: "E.g.: TRE 1A, H4...",
hibrido: "Hybrid",
transicion: "Transition",
btnAgregar: "Add Athlete",
editar: "Edit formation",
eliminar: "Delete Formation",
guardar: "Save Formation",
linea: "Timeline",
piscina: "Pool",
tipoPiscina: "Pool type",
olimpica: "Olympic (50m x 25m)",
semiolimpica: "Semi-Olympic (25m x 12.5m)",
fosa: "Pit (20m x 20m)",
duracion: "Duration (seconds)",
duracionEj: "E.g.: 5",
nombre: "Name",
nombreEj: "Formation name",
notas: "Tactical notes",
notasEj: "Instructions, markings, etc.",
datos: "Formation Data",
play: "▶ Play",
pause: "⏸︎ Pause",
actualizar: "Update Formation",
sinFigura: "No figure",
direccion: "Add Direction",
eliminarDir: "Remove Direction",
edicionActiva: "🟡 Editing active",
alertaFaltanDatos: "Missing athlete data",
alertaMaxAtletas: "Modality \"{MOD}\" allows a maximum of {MAX} athletes.",
alertaClickPiscina: "Click on the pool to place the athlete",
alertaNombreFaltante: "Add formation name and at least one athlete",
alertaSeleccionaFormacion: "Select a formation from the timeline first.",
confirmEliminar: {
titulo: "Delete formation?",
texto: "This action cannot be undone",
confirmar: "Yes, delete",
cancelar: "Cancel",
eliminado: "Formation has been deleted.",
error: "Failed to delete formation."
}
},
nav: {
init: "Initialize Routine",
equip: "Available Teams"
},
logout: "Logout"
},
fr: {
editor: {
titulo: "Éditeur de Formation",
tipo: "Type",
modalidad: "Modalité",
agregar: "Ajouter un Athlète",
seleccionarAtleta: "Sélectionner un athlète",
rol: "Rôle",
rolSeleccionar: "Sélectionner un rôle",
volador: "Voltigeur",
pilar: "Pilier",
otro: "Autre",
idPers: "ID personnalisé",
idEj: "Ex. : A, 1, V2",
figura: "Figure technique",
figuraEj: "Ex. : Barracuda, Flamant...",
tipoElemento: "Type d'élément",
codigoElemento: "Code de l'élément",
codigoEj: "Ex. : TRE 1A, H4...",
hibrido: "Hybride",
transicion: "Transition",
btnAgregar: "Ajouter un Athlète",
editar: "Modifier la formation",
eliminar: "Supprimer la Formation",
guardar: "Enregistrer la Formation",
linea: "Chronologie",
piscina: "Piscine",
tipoPiscina: "Type de piscine",
olimpica: "Olympique (50m x 25m)",
semiolimpica: "Semi-olympique (25m x 12,5m)",
fosa: "Fosse (20m x 20m)",
duracion: "Durée (secondes)",
duracionEj: "Ex. : 5",
nombre: "Nom",
nombreEj: "Nom de la formation",
notas: "Notes tactiques",
notasEj: "Instructions, repères, etc.",
datos: "Données de la Formation",
play: "▶ Lire",
pause: "⏸︎ Pause",
actualizar: "Mettre à jour la Formation",
sinFigura: "Pas de figure",
direccion: "Ajouter une Direction",
eliminarDir: "Supprimer la Direction",
edicionActiva: "🟡 Édition active",
alertaFaltanDatos: "Données de l'athlète manquantes",
alertaMaxAtletas: "La modalité \"{MOD}\" permet un maximum de {MAX} athlètes.",
alertaClickPiscina: "Cliquez sur la piscine pour placer l'athlète",
alertaNombreFaltante: "Ajoutez un nom de formation et au moins un athlète",
alertaSeleccionaFormacion: "Sélectionnez d'abord une formation depuis la chronologie.",
confirmEliminar: {
titulo: "Supprimer la formation ?",
texto: "Cette action est irréversible",
confirmer: "Oui, supprimer",
cancelar: "Annuler",
eliminado: "La formation a été supprimée.",
error: "Échec de la suppression de la formation."
}
},
nav: {
init: "Démarrer la routine",
equip: "Équipes disponibles"
},
logout: "Se déconnecter"
}
};
window.figurasTraducidas = {
"Pez Volador con Giro": {
en: "Flying Fish with Twist",
fr: "Poisson Volant avec Torsion"
},
"Pez Volador": {
en: "Flying Fish",
fr: "Poisson Volant"
},
"Rodilla Flexionada con Giro Completo": {
en: "Bent Knee with Full Twist",
fr: "Genou Fléchi avec Rotation Complète"
},
"Rodilla Flexionada con Medio Giro": {
en: "Bent Knee with Half Twist",
fr: "Genou Fléchi avec Demi-Tour"
},
"Fouetté con Giro 720°": {
en: "Fouetté with 720° Turn",
fr: "Fouetté avec Rotation de 720°"
},
"Fouetté con Giro 360°": {
en: "Fouetté with 360° Turn",
fr: "Fouetté avec Rotation de 360°"
},
"Mariposa": {
en: "Butterfly",
fr: "Papillon"
},
"Split con Giro 180°": {
en: "Split with 180° Turn",
fr: "Grand Écart avec Rotation de 180°"
},
"Split a Rodilla Flexionada": {
en: "Split to Bent Knee",
fr: "Grand Écart vers Genou Fléchi"
}
};
window.descripcionesTraducidas = {
"Impulso a vertical desde pike, transición a cola de pez aérea, regreso a vertical y giro de 180°.": {
en: "Thrust to vertical from pike, transition to aerial fish tail, return to vertical and 180° twist.",
fr: "Poussée en vertical depuis pike, transition vers queue de poisson aérienne, retour en vertical et torsion de 180°."
},
"Impulso a vertical desde pike, transición a cola de pez aérea, regreso a vertical y descenso.": {
en: "Thrust to vertical from pike, transition to aerial fish tail, return to vertical and descent.",
fr: "Poussée en vertical depuis pike, transition vers queue de poisson aérienne, retour en vertical et descente."
},
"Vertical con giro completo a rodilla flexionada, otro giro a vertical, apertura 180° a split y walkout.": {
en: "Vertical with full twist to bent knee, another twist to vertical, 180° split and walkout.",
fr: "Vertical avec rotation complète vers genou fléchi, nouvelle rotation vers vertical, ouverture à 180° en écart et sortie."
},
"Vertical con medio giro a rodilla flexionada, otro medio giro a vertical, split y walkout.": {
en: "Vertical with half twist to bent knee, another half twist to vertical, split and walkout.",
fr: "Vertical avec demi-tour vers genou fléchi, autre demi-tour vers vertical, écart et sortie."
},
"Desde cola de pez, dos giros Fouetté, paso a vertical y giro continuo de 720°.": {
en: "From fish tail, two Fouetté turns, transition to vertical and continuous 720° turn.",
fr: "Depuis queue de poisson, deux rotations Fouetté, passage en vertical et rotation continue de 720°."
},
"Desde cola de pez, dos giros Fouetté, paso a vertical y giro rápido de 360°.": {
en: "From fish tail, two Fouetté turns, transition to vertical and quick 360° turn.",
fr: "Depuis queue de poisson, deux rotations Fouetté, passage en vertical et rotation rapide de 360°."
},
"Secuencia dinámica con pike, cola de pez, split, giros, arco superficial y salida en layout.": {
en: "Dynamic sequence with pike, fish tail, split, turns, surface arch and layout exit.",
fr: "Séquence dynamique avec pike, queue de poisson, écart, rotations, arc en surface et sortie en layout."
},
"Impulso a vertical, split aéreo, giro 180° a rodilla flexionada y descenso vertical.": {
en: "Thrust to vertical, aerial split, 180° turn to bent knee and vertical descent.",
fr: "Poussée en vertical, grand écart aérien, rotation de 180° vers genou fléchi et descente verticale."
},
"Impulso a vertical, split aéreo, transición a rodilla flexionada y descenso vertical.": {
en: "Thrust to vertical, aerial split, transition to bent knee and vertical descent.",
fr: "Poussée en vertical, grand écart aérien, transition vers genou fléchi et descente verticale."
}
};
let lang = window.tLang || localStorage.getItem('lang') || 'es';
window.tLang = lang;
const t = (key, replacements = {}) => {
const keys = key.split('.');
let value = keys.reduce((obj, k) => obj?.[k], traducciones[lang]) ?? key;
for (const [k, v] of Object.entries(replacements)) {
value = value.replace(`{${k}}`, v);
}
return value;
};
function aplicarTraducciones() {
// Traduce elementos con data-i18n
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
el.textContent = t(key);
});
// Traduce placeholders
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.dataset.i18nPlaceholder;
el.placeholder = t(key);
});
// Traducción de <option> del select tipoElemento
const tipoElementoSelect = document.getElementById('tipoElemento');
if (tipoElementoSelect) {
tipoElementoSelect.querySelectorAll('option').forEach(option => {
const val = option.value.toLowerCase();
const text = option.textContent.toLowerCase();
if (val === 'tre') {
option.textContent = 'TRE'; // no traducir
} else if (val === 'híbrido' || text.includes('hybrid') || val === 'hybride') {
option.textContent = t('editor.hibrido') || 'Híbrido';
} else if (val === 'transición' || text.includes('transition') || val === 'transition') {
option.textContent = t('editor.transicion') || 'Transición';
}
});
}
}
document.addEventListener('DOMContentLoaded', () => {
aplicarTraducciones();
actualizarTextosDinamicos();
});
document.getElementById('langSelector').addEventListener('change', (e) => {
const selectedLang = e.target.value;
lang = selectedLang;
window.tLang = selectedLang; // ✅ sincroniza con global
localStorage.setItem('lang', selectedLang); // opcional, si quieres persistencia local
aplicarTraducciones();
actualizarTextosDinamicos();
});
function actualizarTextosDinamicos() {
document.getElementById("btnGuardarFormacion").textContent = `💾 ${t('editor.guardar')}`;
document.getElementById("btnEditarFormacion").innerHTML = `${t('editor.editar')}`;
document.getElementById("btnEliminarFormacion").innerHTML = `${t('editor.eliminar')}`;
if (btnEditarFormacion.classList.contains('btn-warning')) {
btnEditarFormacion.innerHTML = t('editor.edicionActiva'); // refresca si está en modo edición
}
}
// Expone variables globales para otros scripts
window.traducciones = traducciones;
window.figurasTraducidas = figurasTraducidas;
window.descripcionesTraducidas = descripcionesTraducidas;
window.tLang = lang;
window.aplicarTraducciones = aplicarTraducciones;
window.actualizarTextosDinamicos = actualizarTextosDinamicos;

View File

@ -66,47 +66,48 @@
<main class="container-xl px-4 my-4">
<section class="mb-4">
<h2 id="tituloRutina" class="fw-bold fs-2 mb-2">Editor de Formación</h2>
<h2 id="tituloRutina" class="fw-bold fs-2 mb-2" data-i18n="editor.titulo">Editor de Formación</h2>
<div class="d-flex flex-wrap gap-3 small text-muted">
<div><strong>Tipo:</strong> <span id="tipoRutina">Cargando...</span></div>
<div><strong>Modalidad:</strong> <span id="modalidadRutina">Cargando...</span></div>
<div><strong data-i18n="editor.tipo">Tipo:</strong> <span id="tipoRutina">Cargando...</span></div>
<div><strong data-i18n="editor.modalidad">Modalidad:</strong> <span id="modalidadRutina">Cargando...</span></div>
</div>
</section>
<div class="row gx-4 gy-4">
<div class="col-lg-4">
<div class="card p-4 h-100 d-flex flex-column">
<h5 class="fw-semibold mb-3 text-primary">Agregar Atleta</h5>
<h5 class="fw-semibold mb-3 text-primary" data-i18n="editor.agregar">Agregar Atleta</h5>
<div class="mb-3">
<label for="selectAtleta" class="form-label">Seleccionar atleta</label>
<label for="selectAtleta" class="form-label" data-i18n="editor.seleccionarAtleta">Seleccionar atleta</label>
<select id="selectAtleta" class="form-select"></select>
</div>
<div class="mb-3">
<label for="rolAtleta" class="form-label">Rol</label>
<label for="rolAtleta" class="form-label" data-i18n="editor.rol">Rol</label>
<select id="rolAtleta" class="form-select">
<option value="">Seleccionar rol</option>
<option value="volador">Volador</option>
<option value="pilar">Pilar</option>
<option value="otro">Otro</option>
<option value="" data-i18n="editor.rolSeleccionar">Seleccionar rol</option>
<option value="volador" data-i18n="editor.volador">Volador</option>
<option value="pilar" data-i18n="editor.pilar">Pilar</option>
<option value="otro" data-i18n="editor.otro">Otro</option>
</select>
</div>
<div class="mb-3">
<label for="idPersonalizado" class="form-label">ID personalizado</label>
<input type="text" id="idPersonalizado" class="form-control" placeholder="Ej: A, 1, V2" readonly>
<label for="idPersonalizado" class="form-label" data-i18n="editor.idPers">ID personalizado</label>
<input type="text" id="idPersonalizado" class="form-control" placeholder="Ej: A, 1, V2" data-i18n-placeholder="editor.idEj" readonly>
</div>
<div class="mb-3">
<label for="figura" class="form-label">Figura técnica</label>
<input type="text" id="figura" class="form-control" placeholder="Ej: Barracuda, Flamingo..." autocomplete="off">
<label for="figura" class="form-label" data-i18n="editor.figura">Figura técnica</label>
<input type="text" id="figura" class="form-control" placeholder="Ej: Barracuda, Flamingo..." data-i18n-placeholder="editor.figuraEj" autocomplete="off">
<small id="nombreFiguraTraducido" class="form-text text-muted"></small>
<div id="sugerenciasFigura" class="list-group position-relative"></div>
<div id="previewFigura" class="text-center mt-3 mb-3"></div>
</div>
<div class="mb-3">
<label for="tipoElemento" class="form-label">Tipo de elemento</label>
<label for="tipoElemento" class="form-label" data-i18n="editor.tipoElemento">Tipo de elemento</label>
<select id="tipoElemento" class="form-select">
<option value="TRE">TRE</option>
<option value="híbrido">Híbrido</option>
@ -115,23 +116,23 @@
</div>
<div class="mb-4">
<label for="codigoElemento" class="form-label">Código del elemento</label>
<input type="text" id="codigoElemento" class="form-control" placeholder="Ej: TRE 1A, H4...">
<label for="codigoElemento" class="form-label" data-i18n="editor.codigoElemento">Código del elemento</label>
<input type="text" id="codigoElemento" class="form-control" placeholder="Ej: TRE 1A, H4..." data-i18n-placeholder="editor.codigoEj">
</div>
<button id="btnAgregarAtleta" class="btn btn-primary mt-auto">Añadir Atleta</button>
<button id="btnAgregarAtleta" class="btn btn-primary mt-auto" data-i18n="editor.btnAgregar">Añadir Atleta</button>
</div>
</div>
<div class="col-lg-8 d-flex flex-column gap-3">
<div class="card p-4">
<h5 class="fw-semibold mb-3 text-primary">Piscina</h5>
<h5 class="fw-semibold mb-3 text-primary" data-i18n="editor.piscina">Piscina</h5>
<div class="mb-3">
<label for="tipoPiscina" class="form-label">Tipo de piscina</label>
<label for="tipoPiscina" class="form-label" data-i18n="editor.tipoPiscina">Tipo de piscina</label>
<select id="tipoPiscina" class="form-select form-select-sm w-100" style="max-width: 280px;">
<option value="olimpica">Olímpica (50m x 25m)</option>
<option value="semiolimpica">Semiolímpica (25m x 12.5m)</option>
<option value="fosa">Fosa/Poza (20m x 20m)</option>
<option value="olimpica" data-i18n="editor.olimpica">Olímpica (50m x 25m)</option>
<option value="semiolimpica" data-i18n="editor.semiolimpica">Semiolímpica (25m x 12.5m)</option>
<option value="fosa" data-i18n="editor.fosa">Fosa/Poza (20m x 20m)</option>
</select>
</div>
@ -144,14 +145,14 @@
<button id="btnEliminarFormacion" class="btn btn-outline-danger btn-sm">🗑 Eliminar Formación</button>
</div>
<label class="form-label fw-semibold">Línea de Tiempo</label>
<label class="form-label fw-semibold" data-i18n="editor.linea">Línea de Tiempo</label>
<div id="lineaTiempo" class="timeline-placeholder d-flex flex-wrap gap-2 mb-3"></div>
<!-- Reproductor abajo -->
<div class="mb-3">
<div id="waveform" style="height: 80px;"></div>
<div class="text-end mt-2">
<button id="playPauseBtn" class="btn btn-outline-primary btn-sm">▶ Reproducir</button>
<button id="playPauseBtn" class="btn btn-outline-primary btn-sm" data-i18n="editor.play">▶ Reproducir</button>
</div>
</div>
</div>
@ -159,26 +160,26 @@
<div class="card p-4 mt-4">
<h5 class="fw-semibold mb-3 text-primary">Datos de Formación</h5>
<h5 class="fw-semibold mb-3 text-primary" data-i18n="editor.datos">Datos de Formación</h5>
<div class="row gy-3">
<div class="col-md-4">
<label for="nombreFormacion" class="form-label">Nombre</label>
<input type="text" id="nombreFormacion" class="form-control" placeholder="Nombre de la formación">
<label for="nombreFormacion" class="form-label" data-i18n="editor.nombre">Nombre</label>
<input type="text" id="nombreFormacion" class="form-control" placeholder="Nombre de la formación" data-i18n-placeholder="editor.nombreEj">
</div>
<div class="col-md-4">
<label for="duracionFormacion" class="form-label">Duración (segundos)</label>
<input type="number" id="duracionFormacion" class="form-control" min="1" placeholder="Ej: 5">
<label for="duracionFormacion" class="form-label" data-i18n="editor.duracion">Duración (segundos)</label>
<input type="number" id="duracionFormacion" class="form-control" min="1" placeholder="Ej: 5" data-i18n-placeholder="editor.duracionEj">
</div>
<div class="col-md-4">
<label for="notasFormacion" class="form-label">Notas tácticas</label>
<textarea id="notasFormacion" class="form-control" rows="1" placeholder="Instrucciones, marcajes, etc."></textarea>
<label for="notasFormacion" class="form-label" data-i18n="editor.notas">Notas tácticas</label>
<textarea id="notasFormacion" class="form-control" rows="1" placeholder="Instrucciones, marcajes, etc." data-i18n-placeholder="editor.notasEj"></textarea>
</div>
</div>
<div class="text-end mt-4">
<button id="btnGuardarFormacion" class="btn btn-primary">Guardar Formación</button>
<button id="btnGuardarFormacion" class="btn btn-primary" data-i18n="editor.guardar">Guardar Formación</button>
</div>
</div>
</main>
@ -186,6 +187,8 @@
<audio id="audioPlayer" class="d-none" controls></audio>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="js/piscina.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/traduccionPiscina.js"></script>
<script type="module" src="js/piscina.js"></script>
</body>
</html>