From 56cba7f7ab4c67c2a52cef7c521b3de2659788c3 Mon Sep 17 00:00:00 2001 From: JorgeLuisOZ Date: Mon, 9 Jun 2025 23:51:57 -0600 Subject: [PATCH] Traduccion parte de la piscina --- ...completo.png => rodilla-giro-completo.png} | Bin public/js/coach.js | 1 - public/js/piscina.js | 180 +++++++-- public/js/traduccionPiscina.js | 355 ++++++++++++++++++ public/piscina.html | 71 ++-- 5 files changed, 533 insertions(+), 74 deletions(-) rename public/catalog/figuras/{rodilla.giro.completo.png => rodilla-giro-completo.png} (100%) create mode 100644 public/js/traduccionPiscina.js 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.nombre} -

${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} +

${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