avances reunion

This commit is contained in:
angel.alducin 2025-05-23 02:44:58 -06:00
parent adc5c49f69
commit c63a8cfa4a
17 changed files with 490 additions and 51 deletions

View File

@ -9,7 +9,11 @@
<div class="container mt-5">
<h1 class="text-success">Bienvenido Atleta 🏊‍♂️</h1>
<p>Esta es tu zona para consultar tus rutinas y progreso.</p>
<div id="contenedor-rutinas" class="row mt-4 justify-content-center"></div>
<a href="index.html" class="btn btn-outline-success mt-3">Cerrar sesión</a>
<div id="rutinas-list"></div>
</div>
<script src="js/atleta.js"></script>
</body>
</html>

View File

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

View File

@ -28,13 +28,13 @@
<script>
async function loadRoutines() {
const response = await fetch('/routines');
const response = await fetch('/api/rutinas');
const routines = await response.json();
const routinesList = document.getElementById('routinesList');
routinesList.innerHTML = '';
for (const routine of routines) {
const formaciones = await fetch(`/routines/${routine._id}/formations`).then(res => res.json());
const formaciones = await fetch(`/api/rutinas/${routine._id}/formations`).then(res => res.json());
const ultimaFormacion = formaciones?.at(-1);
const atletas = ultimaFormacion?.atletas || [];

View File

@ -12,7 +12,7 @@
<div class="container d-flex justify-content-center align-items-center" style="min-height: 100vh;">
<div class="card shadow p-4" style="width: 100%; max-width: 400px;">
<h3 class="text-center mb-4">Iniciar Sesión</h3>
<form action="/auth/login" method="POST">
<form id="loginForm">
<div class="mb-3">
<label for="email" class="form-label">Correo electrónico</label>
<input type="email" class="form-control" id="email" name="email" placeholder="Ingresar correo" required>
@ -31,5 +31,6 @@
<!-- Bootstrap JS (opcional) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/login.js"></script>
</body>
</html>

46
public/js/atleta.js Normal file
View File

@ -0,0 +1,46 @@
document.addEventListener("DOMContentLoaded", () => {
const atletaId = sessionStorage.getItem("userId");
if (!atletaId) {
document.getElementById("rutinas-list").innerHTML =
"<p>No se encontró tu sesión. Inicia sesión nuevamente.</p>";
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 = "<p>No tienes rutinas asignadas todavía.</p>";
return;
}
rutinas.forEach(rutina => {
const card = document.createElement("div");
card.className = "card my-3";
card.innerHTML = `
<div class="card-body">
<h5 class="card-title">${rutina.title}</h5>
<p class="card-text">${rutina.nombreCompetencia || "Sin descripción"}</p>
<button class="btn btn-success" onclick="verRutina('${rutina._id}')">Ver rutina</button>
</div>
`;
contenedor.appendChild(card);
});
})
.catch(err => {
console.error(err);
document.getElementById("rutinas-list").innerHTML =
"<p>Error al cargar tus rutinas.</p>";
});
});
function verRutina(id) {
window.location.href = `simulador.html?routineId=${id}`;
}

View File

@ -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)

View File

@ -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.');
});
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";
}
});

View File

@ -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');
}
});
});
});

145
public/js/simulador.js Normal file
View File

@ -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));
});
});

View File

@ -7,6 +7,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="css/editorPiscina.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/wavesurfer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/konva@9.2.0/konva.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
@ -129,17 +130,19 @@
<button id="btnGuardarFormacion" class="btn btn-primary btn-lg px-5">💾 Guardar Formación</button>
</div>
<div id="waveform"></div>
<button id="playPauseBtn">▶ Reproducir</button>
<hr class="my-5" />
<!-- Reproductor de música -->
<!-- Línea de música anterior, sin wave -->
<section class="mb-4">
<h5 class="fw-semibold">🎵 Música de la rutina</h5>
<audio id="audioPlayer" class="w-100" controls>
Tu navegador no soporta audio.
</audio>
</section>
<!-- Línea de tiempo -->
<section>
<h4 class="mb-3 fw-semibold">🕒 Línea de tiempo</h4>
<div id="lineaTiempo" class="timeline-placeholder d-flex flex-wrap gap-2"></div>

31
public/simulador.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Simulador de Rutina</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body class="bg-light">
<div class="container mt-4">
<h2 id="tituloRutina" class="text-center text-primary mb-3">Cargando rutina...</h2>
<p class="text-center"><span id="tipoRutina" class="badge bg-info"></span> | <span id="modalidadRutina" class="badge bg-secondary"></span></p>
<audio id="audioPlayer" controls class="d-block mx-auto mb-4"></audio>
<div id="piscinaContainer" class="mx-auto mb-4">
<div id="piscina" class="border bg-white" style="margin: 0 auto;"></div>
</div>
<h5 class="mt-3">Línea de Tiempo</h5>
<div id="lineaTiempo" class="d-flex flex-wrap gap-2"></div>
<div class="mt-4 text-center">
<a href="atleta.html" class="btn btn-outline-secondary">Volver al Panel</a>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/konva@8.3.12/konva.min.js"></script>
<script src="js/simulador.js"></script>
</body>
</html>

View File

@ -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;
const { email, password } = req.body;
try {
const user = await User.findOne({ email });
if (!user) return res.status(401).send('Correo no registrado');
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');
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
}
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');
}
});
} catch (error) {
console.error('Error en login:', error);
res.status(500).send('Error interno del servidor');
}
});
module.exports = router;

View File

@ -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;

View File

@ -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);
@ -12,6 +14,7 @@ router.get('/athletes', async (req, res) => {
.find({ role: 'athlete' })
.project({ _id: 1, name: 1 })
.toArray();
res.json(athletes);
} catch (err) {
console.error('Error al obtener atletas:', err);

View File

@ -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')));

Binary file not shown.

Binary file not shown.