Traducción pagina coach y cierre de sesión

This commit is contained in:
Jorge Luis Ortega Zenteno 2025-05-28 12:14:35 -06:00
parent 874ebd1b90
commit b22538e4aa
10 changed files with 352 additions and 70 deletions

View File

@ -3,96 +3,95 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Crear Rutina SwimmingArt</title>
<title data-i18n="title">Crear Rutina SwimmingArt</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<style>
body {
background-color: #f8f9fa;
}
.navbar {
background-color: #0d6efd;
}
.navbar a {
color: white !important;
}
.form-section {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-width: 800px;
margin: auto;
}
.form-title {
font-weight: bold;
margin-bottom: 1.5rem;
}
</style>
<link rel="stylesheet" href="css/navbar.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top shadow-sm px-4 py-3">
<div class="container-fluid">
<a class="navbar-brand fw-bold text-white" href="#">SwimArt</a>
<div class="ms-auto d-flex gap-4">
<a href="coach.html" class="nav-link text-white">Inicializar Rutina</a>
<a href="equipoDisponibles.html" class="nav-link text-white">Equipos Disponibles</a>
<a href="piscina.html" class="nav-link text-white">Piscina</a>
<div class="ms-auto d-flex gap-4 align-items-center">
<a href="coach.html" class="nav-link text-white" data-i18n="nav.init">Inicializar Rutina</a>
<a href="equipoDisponibles.html" class="nav-link text-white" data-i18n="nav.equip">Equipos Disponibles</a>
<a href="piscina.html" class="nav-link text-white" data-i18n="nav.pool">Piscina</a>
<div class="dropdown">
<button class="btn btn-outline-light dropdown-toggle d-flex align-items-center gap-2 px-3 py-1" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
<path d="M11 10a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-4.546 12.174c.03-.256.071-.512.124-.767C4.28 10.798 5.94 10 8 10s3.72.798 4.422 2.407c.053.255.094.511.124.767A7 7 0 0 0 8 1z"/>
</svg>
<span id="nombreUsuarioHeader" class="fw-semibold">Usuario</span>
</button>
<ul class="dropdown-menu dropdown-menu-end text-center p-3"
aria-labelledby="userDropdown"
style="background-color: #0d6efd; color: white; border-radius: 0 0 8px 8px; min-width: 100%; max-width: 250px;">
<li>
<select id="langSelector" class="form-select form-select-sm mb-3"
style="border: none; border-radius: 6px; background-color: white; color: black; padding: 0.4rem 0.5rem;">
<option value="es">Español</option>
<option value="en">English</option>
<option value="fr">Français</option>
</select>
</li>
<li>
<button class="btn btn-danger btn-sm w-100" onclick="logout()">Salir</button>
</li>
</ul>
</div>
</div>
</div>
</nav>
<main class="px-5 my-4" style="max-width: 90%; margin: 0 auto;">
<div class="bg-white p-4 shadow-sm rounded" style="max-width: 900px; margin: auto;">
<h3 class="fw-bold text-center mb-4">Inicializar Nueva Rutina</h3>
<div class="bg-white p-4 shadow-sm rounded" style="max-width: 900px; margin: auto; margin-top: 125px;">
<h3 class="fw-bold text-center mb-4" data-i18n="form.title">Inicializar Nueva Rutina</h3>
<form id="rutinaForm">
<div class="row g-3">
<div class="col-md-6">
<label for="title" class="form-label">Título de la rutina</label>
<label for="title" class="form-label" data-i18n="form.routineTitle">Título de la rutina</label>
<input type="text" class="form-control" id="title" required />
</div>
<div class="col-md-3">
<label for="duration" class="form-label">Duración (segundos)</label>
<input type="number" class="form-control" id="duration" required />
</div>
<div class="col-md-3">
<label for="language" class="form-label">Idioma</label>
<select id="language" class="form-select" required>
<option value="es">Español</option>
<option value="en">Inglés</option>
</select>
<div class="col-md-6">
<label for="duration" class="form-label" data-i18n="form.duration">Duración</label>
<input type="text" class="form-control" id="duration" placeholder="Selecciona tipo de competencia y modalidad" readonly />
</div>
<div class="col-md-6">
<label for="music" class="form-label">Subir música (mp3)</label>
<label for="music" class="form-label" data-i18n="form.music">Subir música (mp3)</label>
<input type="file" class="form-control" id="music" accept=".mp3" />
</div>
<div class="col-md-6">
<label for="nombreCompetencia" class="form-label">Nombre de la competencia</label>
<label for="nombreCompetencia" class="form-label" data-i18n="form.competition">Nombre de la competencia</label>
<input type="text" class="form-control" id="nombreCompetencia" required />
</div>
<div class="col-md-6">
<label for="tipoCompetencia" class="form-label">Tipo de competencia</label>
<label for="tipoCompetencia" class="form-label" data-i18n="form.type">Tipo de competencia</label>
<select class="form-select" id="tipoCompetencia" required>
<option value="libre">Libre</option>
<option value="técnica">Técnica</option>
<option value="libre" data-i18n="form.free">Libre</option>
<option value="técnica" data-i18n="form.technical">Técnica</option>
</select>
</div>
<div class="col-md-6">
<label for="modalidad" class="form-label">Modalidad</label>
<label for="modalidad" class="form-label" data-i18n="form.mode">Modalidad</label>
<select class="form-select" id="modalidad" required>
<option value="solo">Solo</option>
<option value="duo">Dúo</option>
<option value="equipo">Equipo</option>
<option value="solo" data-i18n="form.solo">Solo</option>
<option value="duo" data-i18n="form.duo">Dúo</option>
<option value="equipo" data-i18n="form.team">Equipo</option>
</select>
</div>
</div>
<div class="mt-4 text-center">
<button type="submit" class="btn btn-success px-5">Guardar Rutina</button>
<button type="submit" class="btn btn-success px-5" data-i18n="form.save">Guardar Rutina</button>
</div>
</form>
</div>
</main>
<script src="js/coach.js"></script>
</body>
<script src="js/traduccionCoach.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

107
public/css/navbar.css Normal file
View File

@ -0,0 +1,107 @@
body {
background-color: #f8f9fa;
}
.navbar {
background-color: #0d6efd;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.navbar a {
color: white !important;
transition: 0.2s ease-in-out;
}
.navbar a:hover {
text-decoration: underline;
color: #e6e6e6 !important;
}
.form-section {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
max-width: 800px;
margin: auto;
}
.form-title {
font-weight: bold;
margin-bottom: 1.5rem;
}
/* ======== USER DROPDOWN ========= */
#userDropdown {
background-color: #0d6efd;
color: white;
border: none;
border-radius: 8px 8px 0 0;
font-weight: 500;
transition: background 0.2s;
}
#userDropdown:hover {
background-color: #0b5ed7;
}
/* Menú del dropdown azul continuo */
.dropdown-menu {
margin-top: 0;
padding: 0.75rem;
border: none;
border-radius: 0 0 8px 8px;
background-color: #0d6efd;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
min-width: 200px;
color: white;
}
/* Eliminar nombre superior */
.dropdown-menu strong {
display: none;
}
/* Selector de idioma estilizado */
.dropdown-menu select,
#langSelector {
width: 100%;
padding: 0.4rem 2rem 0.4rem 0.75rem;
font-size: 0.875rem;
border-radius: 6px;
background-color: white;
color: #212529;
border: none;
appearance: none;
margin-bottom: 0.75rem;
background-image: url("data:image/svg+xml,%3Csvg fill='gray' height='16' viewBox='0 0 20 20' width='16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7 7l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.6rem center;
background-size: 1rem;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
transition: all 0.2s ease-in-out;
}
#langSelector:hover {
background-color: #f1f1f1;
}
#langSelector:focus {
outline: none;
box-shadow: 0 0 0 0.15rem rgba(13,110,253,.25);
}
/* Botón de cerrar sesión rojo sólido */
.dropdown-menu .btn-danger {
width: 100%;
color: white;
background-color: #dc3545;
border: none;
font-weight: 500;
border-radius: 6px;
transition: background 0.2s;
}
.dropdown-menu .btn-danger:hover {
background-color: #b02a37;
}

View File

@ -1,15 +1,47 @@
const duracionesPorModalidadYTipo = {
solo: {
técnica: { texto: "2:00", valor: 120 },
libre: { texto: "2:15", valor: 135 }
},
duo: {
técnica: { texto: "2:20", valor: 140 },
libre: { texto: "2:45", valor: 165 }
},
equipo: {
técnica: { texto: "2:50", valor: 170 },
libre: { texto: "3:30", valor: 210 }
}
};
function actualizarDuracionTexto() {
const tipo = document.getElementById("tipoCompetencia").value;
const modalidad = document.getElementById("modalidad").value;
const input = document.getElementById("duration");
if (duracionesPorModalidadYTipo[modalidad] && duracionesPorModalidadYTipo[modalidad][tipo]) {
input.value = duracionesPorModalidadYTipo[modalidad][tipo].texto;
input.dataset.segundos = duracionesPorModalidadYTipo[modalidad][tipo].valor;
} else {
input.value = traducciones[langActual].form.durationPlaceholder;
input.dataset.segundos = "";
}
}
document.getElementById("tipoCompetencia").addEventListener("change", actualizarDuracionTexto);
document.getElementById("modalidad").addEventListener("change", actualizarDuracionTexto);
// Envío del formulario
document.getElementById('rutinaForm').addEventListener('submit', async function (e) {
e.preventDefault();
const title = document.getElementById('title').value.trim();
const duration = parseInt(document.getElementById('duration').value.trim());
const language = document.getElementById('language').value;
const duration = parseInt(document.getElementById('duration').dataset.segundos);
const nombreCompetencia = document.getElementById('nombreCompetencia').value.trim();
const tipoCompetencia = document.getElementById('tipoCompetencia').value;
const modalidad = document.getElementById('modalidad').value;
const musicFile = document.getElementById('music').files[0];
if (!title || isNaN(duration) || !language || !nombreCompetencia || !tipoCompetencia || !modalidad) {
if (!title || isNaN(duration) || !nombreCompetencia || !tipoCompetencia || !modalidad) {
alert("Completa todos los campos.");
return;
}
@ -43,7 +75,6 @@ document.getElementById('rutinaForm').addEventListener('submit', async function
const routine = {
title,
duration,
language,
nombreCompetencia,
tipoCompetencia,
modalidad,
@ -74,3 +105,32 @@ document.getElementById('rutinaForm').addEventListener('submit', async function
alert("Error al guardar la rutina.");
}
});
function logout() {
sessionStorage.clear();
alert("Sesión cerrada");
window.location.href = "../index.html";
}
// Mostrar nombre del usuario logueado en el header
window.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;
document.getElementById("nombreUsuarioDropdown").textContent = "Coach " + user.name;
}
} catch (err) {
console.error("❌ Error al obtener datos del usuario:", err);
}
});

View File

@ -0,0 +1,107 @@
const traducciones = {
es: {
title: "Crear Rutina SwimmingArt",
nav: {
init: "Inicializar Rutina",
equip: "Equipos Disponibles",
pool: "Piscina"
},
form: {
title: "Inicializar Nueva Rutina",
routineTitle: "Título de la rutina",
duration: "Duración",
durationPlaceholder: "Selecciona tipo de competencia y modalidad",
music: "Subir música (mp3)",
competition: "Nombre de la competencia",
type: "Tipo de competencia",
free: "Libre",
technical: "Técnica",
mode: "Modalidad",
solo: "Solo",
duo: "Dúo",
team: "Equipo",
save: "Guardar Rutina"
}
},
en: {
title: "Create Routine SwimmingArt",
nav: {
init: "Initialize Routine",
equip: "Available Equipment",
pool: "Pool"
},
form: {
title: "Initialize New Routine",
routineTitle: "Title of the routine",
duration: "Duration",
durationPlaceholder: "Select type and modality",
music: "Upload music (mp3)",
competition: "Name of the competition",
type: "Type of competition",
free: "Free",
technical: "Technical",
mode: "Mode",
solo: "Only",
duo: "Duo",
team: "Team",
save: "Save Routine"
}
},
fr: {
title: "Créer une Routine SwimmingArt",
nav: {
init: "Initialiser la Routine",
equip: "Équipements Disponibles",
pool: "Piscine"
},
form: {
title: "Initialiser une Nouvelle Routine",
routineTitle: "Titre de la routine",
duration: "Durée",
durationPlaceholder: "Sélectionnez le type et la modalité",
music: "Télécharger musique (mp3)",
competition: "Nom de la compétition",
type: "Type de compétition",
free: "Libre",
technical: "Technique",
mode: "Modalité",
solo: "Solo",
duo: "Duo",
team: "Équipe",
save: "Enregistrer la Routine"
}
}
};
let langActual = "es";
function updateLanguage(lang) {
langActual = lang;
const t = traducciones[lang];
document.querySelector("title").innerText = t.title;
document.querySelectorAll("[data-i18n]").forEach(el => {
const keys = el.getAttribute("data-i18n").split(".");
let value = t;
for (let k of keys) value = value[k];
if (el.tagName === "OPTION") {
el.text = value;
} else if (el.tagName === "INPUT" && el.placeholder !== undefined) {
el.placeholder = value;
} else {
el.textContent = value;
}
});
// actualizar el texto si la duración está vacía
const input = document.getElementById("duration");
if (input && !input.dataset.segundos) {
input.value = traducciones[langActual].form.durationPlaceholder;
}
}
document.getElementById("langSelector").addEventListener("change", function () {
updateLanguage(this.value);
});
window.onload = () => updateLanguage("es");

View File

@ -36,14 +36,6 @@
<option value="athlete">Atleta</option>
</select>
</div>
<div class="mb-3">
<label for="language" class="form-label">Idioma</label>
<select class="form-select" id="language" name="language">
<option value="es">Español</option>
<option value="en">Inglés</option>
<option value="fr">Francés</option>
</select>
</div>
<button type="submit" class="btn btn-success w-100">Registrarse</button>
</form>
<div class="text-center mt-3">

View File

@ -18,7 +18,6 @@ const userSchema = new mongoose.Schema({
email: { type: String, unique: true, required: true },
passwordHash: String,
role: { type: String, enum: ['coach', 'athlete'], required: true },
language: { type: String, enum: ['es', 'en', 'fr'], default: 'es' },
createdAt: { type: Date, default: Date.now }
});
@ -26,14 +25,14 @@ const User = mongoose.model('User', userSchema);
// === Ruta: Registro de usuario ===
router.post('/register', async (req, res) => {
const { name, username, email, password, role, language } = req.body;
const { name, username, email, password, role } = req.body;
try {
const existing = await User.findOne({ email });
if (existing) return res.status(400).send('Correo ya registrado');
const passwordHash = await bcrypt.hash(password, 10);
const user = new User({ name, username, email, passwordHash, role, language });
const user = new User({ name, username, email, passwordHash, role });
await user.save();
res.redirect('/index.html');

View File

@ -5,7 +5,6 @@ const router = express.Router();
const routineSchema = new mongoose.Schema({
title: String,
createdBy: { type: String, default: "coach-id-ejemplo" },
language: { type: String, enum: ['es', 'en', 'fr'], default: 'es' },
duration: Number,
musicUrl: { type: String, default: "" },
nombreCompetencia: String,

View File

@ -30,7 +30,6 @@ const upload = multer({
const routineSchema = new mongoose.Schema({
title: String,
createdBy: { type: String, default: "coach-id-ejemplo" },
language: { type: String, enum: ['es', 'en', 'fr'], default: 'es' },
duration: Number,
musicUrl: { type: String, default: "" },
nombreCompetencia: String,

View File

@ -1,11 +1,12 @@
const express = require('express');
const router = express.Router();
const { MongoClient } = require('mongodb');
const { MongoClient, ObjectId } = require('mongodb');
require('dotenv').config();
const uri = process.env.MONGO_URI;
const client = new MongoClient(uri);
// Obtener todos los atletas
router.get('/athletes', async (req, res) => {
try {
await client.connect();
@ -22,4 +23,23 @@ router.get('/athletes', async (req, res) => {
}
});
// Obtener usuario por ID
router.get('/:id', async (req, res) => {
try {
await client.connect();
const db = client.db('swimartdb');
const user = await db.collection('users')
.findOne({ _id: new ObjectId(req.params.id) }, { projection: { name: 1, email: 1, role: 1 } });
if (!user) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
res.json(user);
} catch (error) {
console.error('Error al obtener usuario:', error);
res.status(500).json({ error: 'Error al obtener usuario', details: error });
}
});
module.exports = router;

Binary file not shown.