diff --git a/public/catalog/figuras/rodilla.giro.completo.png b/public/catalog/figuras/rodilla-giro-completo.png
similarity index 100%
rename from public/catalog/figuras/rodilla.giro.completo.png
rename to public/catalog/figuras/rodilla-giro-completo.png
diff --git a/public/js/coach.js b/public/js/coach.js
index ffd98de..0bcff83 100644
--- a/public/js/coach.js
+++ b/public/js/coach.js
@@ -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);
diff --git a/public/js/piscina.js b/public/js/piscina.js
index 480e929..5525fcc 100644
--- a/public/js/piscina.js
+++ b/public/js/piscina.js
@@ -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,68 +86,94 @@ 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 {
- const res = await fetch('/catalog/figurasFINA.json');
- figurasFINA = await res.json();
+let figurasFINA = [];
+try {
+ const res = await fetch('/catalog/figurasFINA.json');
+ figurasFINA = await res.json();
- const sugerenciasBox = document.getElementById('sugerenciasFigura');
+ const sugerenciasBox = document.getElementById('sugerenciasFigura');
- inputFigura.addEventListener('input', () => {
- const val = inputFigura.value.trim().toLowerCase();
- sugerenciasBox.innerHTML = '';
- if (val.length < 1) return;
+ inputFigura.addEventListener('input', () => {
+ const val = inputFigura.value.trim().toLowerCase();
+ sugerenciasBox.innerHTML = '';
+ if (val.length < 1) return;
- const matches = figurasFINA.filter(f => f.nombre.toLowerCase().includes(val));
- matches.forEach(figura => {
- const item = document.createElement('button');
- item.type = 'button';
- item.textContent = figura.nombre;
- item.onclick = () => {
+ const matches = figurasFINA.filter(f => f.nombre.toLowerCase().includes(val));
+ 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);
- });
+ };
+ sugerenciasBox.appendChild(item);
});
+ });
- inputFigura.addEventListener('blur', () => {
- setTimeout(() => sugerenciasBox.innerHTML = '', 200);
- });
+ inputFigura.addEventListener('blur', () => {
+ setTimeout(() => sugerenciasBox.innerHTML = '', 200);
+ });
- function mostrarPreview(figura) {
- document.getElementById('previewFigura').innerHTML = `
-
-
${figura.descripcion} ${figura.categoria}
- `;
- const codigoInput = document.getElementById("codigoElemento");
- if (codigoInput && figura.codigoElemento) {
- codigoInput.value = figura.codigoElemento;
- }
+ 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 = `
+
+ ${nombreTrad}
${descripcionTrad} ${figura.categoria}
+ `;
+
+ 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;
\ No newline at end of file
diff --git a/public/js/traduccionPiscina.js b/public/js/traduccionPiscina.js
new file mode 100644
index 0000000..8262954
--- /dev/null
+++ b/public/js/traduccionPiscina.js
@@ -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