diff --git a/public/atleta.html b/public/atleta.html index 6615f1a..c9d8ae9 100644 --- a/public/atleta.html +++ b/public/atleta.html @@ -9,7 +9,11 @@

Bienvenido Atleta 🏊‍♂️

Esta es tu zona para consultar tus rutinas y progreso.

+
Cerrar sesión +
+
+ diff --git a/public/css/editorPiscina.css b/public/css/editorPiscina.css index 919e074..ad63cd3 100644 --- a/public/css/editorPiscina.css +++ b/public/css/editorPiscina.css @@ -19,6 +19,16 @@ body { display: block; } +/* Multimedia */ +#waveform { + width: 100%; + height: 100px; + margin-bottom: 10px; +} +#audioPlayer { + display: none; +} + /* Contenedor de la piscina */ #piscinaContainer { background-color: #e6f7ff; @@ -130,6 +140,31 @@ body { margin-top: 4px; } +#playPauseBtn { + background-color: #0d6efd; + color: white; + font-weight: 500; + padding: 10px 20px; + border: none; + border-radius: 8px; + font-size: 1rem; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); + transition: background-color 0.3s ease, transform 0.2s ease; +} + +#playPauseBtn:hover { + background-color: #084298; + transform: scale(1.05); +} + +.step.active { + background-color: #0d6efd !important; + color: white !important; + font-weight: bold; +} + + /* Responsivo */ @media (max-width: 768px) { #piscina { diff --git a/public/equipoDisponibles.html b/public/equipoDisponibles.html index 8020453..4a31591 100644 --- a/public/equipoDisponibles.html +++ b/public/equipoDisponibles.html @@ -28,13 +28,13 @@ + diff --git a/public/js/atleta.js b/public/js/atleta.js new file mode 100644 index 0000000..6302504 --- /dev/null +++ b/public/js/atleta.js @@ -0,0 +1,46 @@ +document.addEventListener("DOMContentLoaded", () => { + const atletaId = sessionStorage.getItem("userId"); + + if (!atletaId) { + document.getElementById("rutinas-list").innerHTML = + "

No se encontró tu sesión. Inicia sesión nuevamente.

"; + return; + } + + fetch(`/api/rutinas/atleta/${atletaId}`) + .then(res => { + if (!res.ok) { + throw new Error("Error al obtener rutinas"); + } + return res.json(); + }) + .then(rutinas => { + const contenedor = document.getElementById("rutinas-list"); + if (rutinas.length === 0) { + contenedor.innerHTML = "

No tienes rutinas asignadas todavía.

"; + return; + } + rutinas.forEach(rutina => { + const card = document.createElement("div"); + card.className = "card my-3"; + card.innerHTML = ` +
+
${rutina.title}
+

${rutina.nombreCompetencia || "Sin descripción"}

+ +
+ `; + contenedor.appendChild(card); + }); + }) + .catch(err => { + console.error(err); + document.getElementById("rutinas-list").innerHTML = + "

Error al cargar tus rutinas.

"; + }); +}); + +function verRutina(id) { + window.location.href = `simulador.html?routineId=${id}`; +} + diff --git a/public/js/coach.js b/public/js/coach.js index 256091f..d95de5d 100644 --- a/public/js/coach.js +++ b/public/js/coach.js @@ -21,7 +21,7 @@ document.getElementById('rutinaForm').addEventListener('submit', async function formData.append('music', musicFile); try { - const uploadRes = await fetch('/routines/upload/music', { + const uploadRes = await fetch('/api/rutinas/upload/music', { method: 'POST', body: formData }); @@ -55,7 +55,7 @@ document.getElementById('rutinaForm').addEventListener('submit', async function }; try { - const res = await fetch('/routines', { + const res = await fetch('/api/rutinas', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(routine) diff --git a/public/js/login.js b/public/js/login.js index fe520d5..d37e616 100644 --- a/public/js/login.js +++ b/public/js/login.js @@ -1,9 +1,29 @@ -document.getElementById('loginForm').addEventListener('submit', function (e) { - e.preventDefault(); - alert('Inicio de sesión simulado.'); +document.getElementById('loginForm').addEventListener('submit', async function (e) { + e.preventDefault(); + + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + + const res = await fetch('/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }) }); - document.getElementById('registerForm').addEventListener('submit', function (e) { - e.preventDefault(); - alert('Registro exitoso simulado.'); - }); \ No newline at end of file + if (!res.ok) { + alert("Credenciales incorrectas."); + return; + } + + const data = await res.json(); + sessionStorage.setItem("userId", data.userId); + sessionStorage.setItem("role", data.role); + + if (data.role === "coach") { + window.location.href = "coach.html"; + } else if (data.role === "athlete") { + window.location.href = "atleta.html"; + } else { + window.location.href = "ventanaPrincipal.html"; + } +}); diff --git a/public/js/piscina.js b/public/js/piscina.js index 07c6d7d..706a045 100644 --- a/public/js/piscina.js +++ b/public/js/piscina.js @@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', async () => { let modoDireccion = false; let indexAtletaDireccion = null; - const rutina = await fetch(`/routines/${rutinaId}`).then(res => res.json()); + const rutina = await fetch(`/api/rutinas/${rutinaId}`).then(res => res.json()); const modalidad = rutina.modalidad; const audioPlayer = document.getElementById('audioPlayer'); @@ -29,7 +29,7 @@ document.addEventListener('DOMContentLoaded', async () => { tipoRutina.textContent = rutina.tipoCompetencia; modalidadRutina.textContent = modalidad; - const atletas = await fetch('/users/athletes').then(res => res.json()); + const atletas = await fetch('/api/users/athletes').then(res => res.json()); atletas.forEach(a => { const opt = document.createElement('option'); opt.value = a._id; @@ -123,6 +123,7 @@ document.addEventListener('DOMContentLoaded', async () => { } let formacionActual = []; + let atletasKonva = {}; let currentAtleta = null; const maxAtletas = modalidad === 'solo' ? 1 : modalidad === 'duo' ? 2 : 8; @@ -252,6 +253,12 @@ document.addEventListener('DOMContentLoaded', async () => { }); layer.add(circle); + atletasKonva[atleta.idPersonalizado] = { + circle, + text, + figuraText, + coordText + }; layer.add(text); layer.add(figuraText); layer.add(coordText); @@ -338,6 +345,58 @@ document.addEventListener('DOMContentLoaded', async () => { }); }, 100); } + + function animarTransicion(nuevaFormacion) { + nuevaFormacion.forEach(a => { + const obj = atletasKonva[a.idPersonalizado]; + if (obj) { + const { circle, text, figuraText, coordText } = obj; + + // Animar círculo + new Konva.Tween({ + node: circle, + duration: 1.0, + x: a.x, + y: a.y, + easing: Konva.Easings.EaseInOut, + }).play(); + + // Mover textos en paralelo + new Konva.Tween({ + node: text, + duration: 1.0, + x: a.x - 10, + y: a.y - 7, + easing: Konva.Easings.EaseInOut, + }).play(); + + new Konva.Tween({ + node: figuraText, + duration: 1.0, + x: a.x - 25, + y: a.y + 18, + easing: Konva.Easings.EaseInOut, + }).play(); + + new Konva.Tween({ + node: coordText, + duration: 1.0, + x: a.x - 35, + y: a.y + 30, + easing: Konva.Easings.EaseInOut, + onUpdate: () => { + const metros = convertirAMetros(a.x, a.y); + coordText.text(`${metros.metrosX}m, ${metros.metrosY}m`); + } + }).play(); + + } else { + dibujarAtleta(a); + } + }); + } + + btnGuardarFormacion.addEventListener('click', async () => { const nombre = document.getElementById('nombreFormacion').value.trim(); const notas = document.getElementById('notasFormacion').value.trim(); @@ -368,8 +427,8 @@ document.addEventListener('DOMContentLoaded', async () => { const method = editIndex === null ? 'POST' : 'PUT'; const endpoint = editIndex === null - ? `/routines/${rutinaId}/formations` - : `/routines/${rutinaId}/formations/${editIndex}`; + ? `/api/rutinas/${rutinaId}/formations` + : `/api/rutinas/${rutinaId}/formations/${editIndex}`; const res = await fetch(endpoint, { method, @@ -393,7 +452,7 @@ document.addEventListener('DOMContentLoaded', async () => { const nuevasFormaciones = nuevaLista.map(i => formaciones[i]); try { - const res = await fetch(`/routines/${rutinaId}`, { + const res = await fetch(`/api/rutinas/${rutinaId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ formaciones: nuevasFormaciones }) @@ -412,7 +471,7 @@ try { } }); - const formaciones = await fetch(`/routines/${rutinaId}/formations`).then(res => res.json()); + const formaciones = await fetch(`/api/rutinas/${rutinaId}/formations`).then(res => res.json()); if (Array.isArray(formaciones)) { formaciones.forEach((f, i) => { @@ -437,9 +496,10 @@ try { formacionActual = seleccionada.atletas.map(a => ({ ...a })); - layer.destroyChildren(); - dibujarCuadricula(layer, stage.width(), stage.height()); - formacionActual.forEach(a => dibujarAtleta(a)); + const nuevaFormacion = seleccionada.atletas.map(a => ({ ...a })); + animarTransicion(nuevaFormacion); + formacionActual = nuevaFormacion; + btnEditarFormacion.disabled = false; btnEditarFormacion.className = 'btn btn-outline-secondary btn-sm'; @@ -452,6 +512,19 @@ try { editIndex = null; }); + const syncPuntos = []; +let tiempoAcumulado = 0; + +formaciones.forEach((f, i) => { + syncPuntos.push({ + index: i, + inicio: tiempoAcumulado, + fin: tiempoAcumulado + (f.duracion || 0) + }); + tiempoAcumulado += (f.duracion || 0); +}); + + btnEditarFormacion.addEventListener('click', () => { if (indexSeleccionado !== null) { editIndex = indexSeleccionado; @@ -485,7 +558,7 @@ try { } try { - const res = await fetch(`/routines/${rutinaId}/piscina`, { + const res = await fetch(`/api/rutinas/${rutinaId}/piscina`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ width: newWidth, height: newHeight }) @@ -525,4 +598,61 @@ try { } }); resizeObserver.observe(document.getElementById('piscina')); + + // Crear WaveSurfer +const wave = WaveSurfer.create({ + container: '#waveform', + waveColor: '#A8DBA8', + progressColor: '#3B8686', + height: 100, + responsive: true +}); + +if (rutina.musicUrl) { + wave.load(rutina.musicUrl); +} + +const playPauseBtn = document.getElementById('playPauseBtn'); + +playPauseBtn.addEventListener('click', () => { + wave.playPause(); +}); + +wave.on('play', () => { + playPauseBtn.textContent = '⏸︎ Pausar'; +}); + +wave.on('pause', () => { + playPauseBtn.textContent = '▶ Reproducir'; +}); + +wave.on('finish', () => { + playPauseBtn.textContent = '▶ Reproducir'; +}); + +wave.on('audioprocess', (currentTime) => { + syncPuntos.forEach(punto => { + const btn = document.querySelector(`.step[data-index="${punto.index}"]`); + if (!btn) return; + + if (currentTime >= punto.inicio && currentTime < punto.fin) { + btn.classList.add('active'); + + if (indexSeleccionado !== punto.index) { + indexSeleccionado = punto.index; + const seleccionada = formaciones[punto.index]; + if (seleccionada) { + formacionActual = seleccionada.atletas.map(a => ({ ...a })); + const nuevaFormacion = seleccionada.atletas.map(a => ({ ...a })); + animarTransicion(nuevaFormacion); + formacionActual = nuevaFormacion; + } + } + + } else { + btn.classList.remove('active'); + } + }); +}); + }); diff --git a/public/js/simulador.js b/public/js/simulador.js new file mode 100644 index 0000000..e28d7ca --- /dev/null +++ b/public/js/simulador.js @@ -0,0 +1,145 @@ +document.addEventListener("DOMContentLoaded", async () => { + const rutinaId = new URLSearchParams(window.location.search).get("routineId"); + if (!rutinaId) return alert("No se proporcionó ID de rutina."); + + const tituloRutina = document.getElementById("tituloRutina"); + const tipoRutina = document.getElementById("tipoRutina"); + const modalidadRutina = document.getElementById("modalidadRutina"); + const audioPlayer = document.getElementById("audioPlayer"); + const timeline = document.getElementById("lineaTiempo"); + + const rutina = await fetch(`/api/rutinas/${rutinaId}`).then(res => res.json()); + const modalidad = rutina.modalidad; + + tituloRutina.textContent = rutina.title || rutina.nombreCompetencia; + tipoRutina.textContent = rutina.tipoCompetencia; + modalidadRutina.textContent = modalidad; + if (rutina.musicUrl) { + audioPlayer.src = rutina.musicUrl; + } + + const piscinaWidth = rutina.piscina?.width || 850; + const piscinaHeight = rutina.piscina?.height || 400; + const escalaX = 25 / piscinaWidth; + const escalaY = 20 / piscinaHeight; + + function convertirAMetros(pxX, pxY) { + return { + metrosX: (pxX * escalaX).toFixed(2), + metrosY: (pxY * escalaY).toFixed(2) + }; + } + + const stage = new Konva.Stage({ + container: "piscina", + width: piscinaWidth, + height: piscinaHeight + }); + const layer = new Konva.Layer(); + stage.add(layer); + + const divPiscina = document.getElementById("piscina"); + divPiscina.style.width = `${piscinaWidth}px`; + divPiscina.style.height = `${piscinaHeight}px`; + document.getElementById("piscinaContainer").style.width = `${piscinaWidth + 20}px`; + + function dibujarCuadricula(layer, ancho, alto, tamano = 45) { + for (let x = 0; x <= ancho; x += tamano) { + layer.add(new Konva.Line({ + points: [x, 0, x, alto], + stroke: '#cceeff', + strokeWidth: 1 + })); + } + for (let y = 0; y <= alto; y += tamano) { + layer.add(new Konva.Line({ + points: [0, y, ancho, y], + stroke: '#cceeff', + strokeWidth: 1 + })); + } + } + + dibujarCuadricula(layer, piscinaWidth, piscinaHeight); + + function dibujarAtleta(atleta) { + const color = atleta.rol === 'volador' ? 'purple' : atleta.rol === 'pilar' ? 'blue' : 'red'; + const metros = convertirAMetros(atleta.x, atleta.y); + + const circle = new Konva.Circle({ + x: atleta.x, + y: atleta.y, + radius: 15, + fill: color, + stroke: 'white', + strokeWidth: 2 + }); + + const text = new Konva.Text({ + x: atleta.x - 10, + y: atleta.y - 7, + text: atleta.idPersonalizado, + fontSize: 12, + fill: 'white' + }); + + const figuraText = new Konva.Text({ + x: atleta.x - 25, + y: atleta.y + 18, + text: atleta.figura || '', + fontSize: 10, + fill: '#333', + fontStyle: 'italic' + }); + + const coordText = new Konva.Text({ + x: atleta.x - 35, + y: atleta.y + 30, + text: `${metros.metrosX}m, ${metros.metrosY}m`, + fontSize: 9, + fill: '#666' + }); + + layer.add(circle); + layer.add(text); + layer.add(figuraText); + layer.add(coordText); + + if (atleta.direccion) { + const dir = new Konva.Line({ + points: [atleta.direccion.x1, atleta.direccion.y1, atleta.direccion.x2, atleta.direccion.y2], + stroke: 'black', + strokeWidth: 2, + dash: [4, 4] + }); + layer.add(dir); + } + + layer.draw(); + } + + const formaciones = await fetch(`/api/rutinas/${rutinaId}/formations`).then(res => res.json()); + + if (Array.isArray(formaciones)) { + formaciones.forEach((f, i) => { + const block = document.createElement("button"); + block.className = "btn btn-outline-primary btn-sm me-2 step"; + block.textContent = `${f.nombreColoquial || `Formación ${i + 1}`} (${f.duracion || '?'}s)`; + block.dataset.index = i; + block.title = f.notasTacticas || ''; + timeline.appendChild(block); + }); + } + + timeline.addEventListener("click", (e) => { + if (!e.target.classList.contains("step")) return; + + const index = parseInt(e.target.dataset.index); + const formacion = formaciones[index]; + if (!formacion) return; + + layer.destroyChildren(); + dibujarCuadricula(layer, stage.width(), stage.height()); + formacion.atletas.forEach(a => dibujarAtleta(a)); + }); +}); diff --git a/public/piscina.html b/public/piscina.html index c1e2084..003360c 100644 --- a/public/piscina.html +++ b/public/piscina.html @@ -7,6 +7,7 @@ + @@ -129,17 +130,19 @@ +
+ + +
- +
-
🎵 Música de la rutina
-

🕒 Línea de tiempo

diff --git a/public/simulador.html b/public/simulador.html new file mode 100644 index 0000000..f835a04 --- /dev/null +++ b/public/simulador.html @@ -0,0 +1,31 @@ + + + + + Simulador de Rutina + + + +
+

Cargando rutina...

+

|

+ + + +
+
+
+ +
Línea de Tiempo
+
+ + +
+ + + + + + diff --git a/routes/auth.js b/routes/auth.js index d470a5f..e1509a3 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -45,29 +45,26 @@ router.post('/register', async (req, res) => { // === Ruta: Login de usuario === router.post('/login', async (req, res) => { - const { email, password } = req.body; - - try { - const user = await User.findOne({ email }); - if (!user) return res.status(401).send('Correo no registrado'); - - const valid = await bcrypt.compare(password, user.passwordHash); - if (!valid) return res.status(401).send('Contraseña incorrecta'); - - // Redirección por rol - if (user.role === 'coach') { - return res.redirect('/coach.html'); - } else if (user.role === 'athlete') { - return res.redirect('/atleta.html'); - } else { - return res.redirect('/ventanaPrincipal.html'); // fallback - } - - } catch (error) { - console.error('Error en login:', error); - res.status(500).send('Error interno del servidor'); - } - }); + const { email, password } = req.body; + + try { + const user = await User.findOne({ email }); + if (!user) return res.status(401).send('Correo no registrado'); + + const valid = await bcrypt.compare(password, user.passwordHash); + if (!valid) return res.status(401).send('Contraseña incorrecta'); + + res.json({ + userId: user._id, + name: user.name, + role: user.role + }); + } catch (error) { + console.error('Error en login:', error); + res.status(500).send('Error interno del servidor'); + } +}); + module.exports = router; diff --git a/routes/routines.js b/routes/routines.js index 8ac0227..02de879 100644 --- a/routes/routines.js +++ b/routes/routines.js @@ -117,13 +117,24 @@ router.post('/:id/formations', async (req, res) => { if (!rutina) return res.status(404).json({ error: 'Rutina no encontrada' }); rutina.formaciones.push(req.body); + + const nuevosAtletas = req.body.atletas.map(a => a.atletaId); + + nuevosAtletas.forEach(idAtleta => { + if (!rutina.participantes.includes(idAtleta.toString())) { + rutina.participantes.push(idAtleta); + } + }); + await rutina.save(); res.status(200).json({ message: 'Formación agregada exitosamente' }); } catch (error) { + console.error('Error al guardar formación:', error); res.status(500).json({ error: 'Error al guardar formación', details: error }); } }); + // GET formaciones de rutina router.get('/:id/formations', async (req, res) => { try { @@ -206,6 +217,19 @@ router.post('/upload/music', upload.single('music'), (req, res) => { res.status(200).json({ url: fileUrl }); }); +// GET rutinas asignadas a un atleta +router.get('/atleta/:id', async (req, res) => { + const atletaId = req.params.id; + try { + const rutinas = await Routine.find({ participantes: atletaId }) + .populate('participantes', 'name') + .populate('formaciones.atletas.atletaId', 'name'); + res.json(rutinas); + } catch (err) { + console.error(err); + res.status(500).json({ mensaje: "Error al obtener rutinas del atleta" }); + } +}); module.exports = router; diff --git a/routes/users.js b/routes/users.js index ec00e17..15b3c90 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,6 +1,8 @@ const express = require('express'); const router = express.Router(); const { MongoClient } = require('mongodb'); +require('dotenv').config(); + const uri = process.env.MONGO_URI; const client = new MongoClient(uri); @@ -10,8 +12,9 @@ router.get('/athletes', async (req, res) => { const db = client.db('swimartdb'); const athletes = await db.collection('users') .find({ role: 'athlete' }) - .project({ _id: 1, name: 1 }) + .project({ _id: 1, name: 1 }) .toArray(); + res.json(athletes); } catch (err) { console.error('Error al obtener atletas:', err); diff --git a/server.js b/server.js index 9ba785d..ef2cac2 100644 --- a/server.js +++ b/server.js @@ -15,8 +15,8 @@ const routineRoutes = require('./routes/routines'); const userRoutes = require('./routes/users'); app.use('/auth', authRoutes); -app.use('/routines', routineRoutes); -app.use('/users', userRoutes); +app.use('/api/rutinas', routineRoutes); +app.use('/api/users', userRoutes); app.use('/catalog', express.static(path.join(__dirname, 'catalog'))); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); diff --git a/uploads/music/1747973943747-708022861.mp3 b/uploads/music/1747973943747-708022861.mp3 new file mode 100644 index 0000000..71b290d Binary files /dev/null and b/uploads/music/1747973943747-708022861.mp3 differ diff --git a/uploads/music/1747978564328-779981294.mp3 b/uploads/music/1747978564328-779981294.mp3 new file mode 100644 index 0000000..71b290d Binary files /dev/null and b/uploads/music/1747978564328-779981294.mp3 differ