The profile setup system in FitAiid allows users to manage their personal information, view their fitness statistics, and customize their account settings.
Profile Overview
The profile page (perfil.html) provides a comprehensive dashboard for users to manage their account.
Personal Data Age, height, weight, and contact information
Fitness Profile Goals, experience level, and training preferences
Statistics Workouts completed, calories burned, active time
Profile Components
Hero Section
The profile hero displays key user information with interactive elements:
frontend/pages/perfil.html
< section class = "profile-hero" >
< div class = "avatar-container" >
< div class = "avatar" onclick = "openAvatarModal()" >
< img id = "userAvatar" src = "default-avatar.svg" alt = "Avatar" >
</ div >
< div class = "avatar-edit" onclick = "openAvatarModal()" >
< i class = "fas fa-camera" ></ i >
</ div >
</ div >
< div class = "profile-info" >
< h1 > HOLA, < span id = "userName" > USUARIO </ span ></ h1 >
< p id = "userEmail" > [email protected] </ p >
< div class = "profile-badges" >
< span class = "badge badge-level" >
< i class = "fas fa-star" ></ i > < span id = "userLevel" > Principiante </ span >
</ span >
< span class = "badge badge-goal" >
< i class = "fas fa-bullseye" ></ i > < span id = "userGoal" > Perder Peso </ span >
</ span >
< span class = "badge badge-streak" >
< i class = "fas fa-fire" ></ i > < span id = "streakBadge" > 7 días </ span >
</ span >
</ div >
</ div >
</ section >
The profile hero includes real-time statistics like workout count, calories burned, and active hours.
Loading Profile Data
Profile data is loaded from localStorage after successful login or questionnaire completion:
Load User Data
Backend Profile Endpoint
function loadUserData () {
const userString = localStorage . getItem ( 'user' );
const fitnessProfile = localStorage . getItem ( 'fitnessProfile' );
// Load basic user info
if ( userString ) {
const user = JSON . parse ( userString );
document . getElementById ( 'userName' ). textContent =
( user . firstName || 'USUARIO' ). toUpperCase ();
document . getElementById ( 'userEmail' ). textContent =
user . email || '[email protected] ' ;
}
// Load fitness profile from questionnaire
if ( fitnessProfile ) {
const profile = JSON . parse ( fitnessProfile );
// Update age
if ( profile . age ) {
document . getElementById ( 'userAge' ). textContent = profile . age + ' años' ;
}
// Update height
if ( profile . height ) {
document . getElementById ( 'userHeight' ). textContent = profile . height + ' cm' ;
}
// Update weight
if ( profile . weight ) {
document . getElementById ( 'userWeight' ). textContent = profile . weight + ' kg' ;
}
// Update goal
if ( profile . mainGoal ) {
const goalText = profile . mainGoal . charAt ( 0 ). toUpperCase () +
profile . mainGoal . slice ( 1 );
document . getElementById ( 'userGoal' ). textContent = goalText ;
document . getElementById ( 'userObjective' ). textContent = goalText ;
}
// Update fitness level
if ( profile . fitnessLevel ) {
const levelText = profile . fitnessLevel . charAt ( 0 ). toUpperCase () +
profile . fitnessLevel . slice ( 1 );
document . getElementById ( 'userLevel' ). textContent = levelText ;
document . getElementById ( 'userFitnessLevel' ). textContent = levelText ;
}
}
}
// Initialize on page load
loadUserData ();
Avatar Customization
Users can customize their profile picture by uploading an image or selecting from predefined avatars.
Open Avatar Modal
User clicks on their current avatar or the edit button: function openAvatarModal () {
const avatars = [
'😎' , '🤩' , '😁' , '🥳' , '🤓' , '😇' , '🥰' , '😊' ,
'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix' ,
'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka' ,
'https://api.dicebear.com/7.x/avataaars/svg?seed=Coco' ,
'https://api.dicebear.com/7.x/avataaars/svg?seed=Milo'
];
const grid = document . getElementById ( 'avatarGrid' );
grid . innerHTML = '' ;
avatars . forEach ( avatar => {
const div = document . createElement ( 'div' );
div . className = 'avatar-option' ;
div . onclick = () => selectAvatar ( div , avatar );
if ( avatar . startsWith ( 'http' )) {
const img = document . createElement ( 'img' );
img . src = avatar ;
img . style . width = '100%' ;
img . style . borderRadius = '50%' ;
div . appendChild ( img );
} else {
div . textContent = avatar ;
}
grid . appendChild ( div );
});
document . getElementById ( 'avatarModal' ). classList . add ( 'active' );
}
Upload or Select Avatar
Users can either upload their own image or select a predefined avatar: function handleImageUpload ( e ) {
const file = e . target . files [ 0 ];
if ( ! file ) return ;
const reader = new FileReader ();
reader . onload = ( ev ) => {
uploadedImageData = ev . target . result ;
document . getElementById ( 'previewImg' ). src = uploadedImageData ;
document . getElementById ( 'uploadPreview' ). style . display = 'block' ;
// Deselect predefined avatars
document . querySelectorAll ( '.avatar-option' ). forEach ( o =>
o . classList . remove ( 'selected' )
);
selectedAvatar = null ;
};
reader . readAsDataURL ( file );
}
Save Avatar
The selected avatar is saved and displayed: function saveAvatar () {
let avatarToSave = null ;
// Priority: uploaded image > selected avatar
if ( uploadedImageData ) {
avatarToSave = uploadedImageData ;
} else if ( selectedAvatar ) {
if ( selectedAvatar . startsWith ( 'http' )) {
avatarToSave = selectedAvatar ;
} else {
// Convert emoji to image
const canvas = document . createElement ( 'canvas' );
canvas . width = 100 ;
canvas . height = 100 ;
const ctx = canvas . getContext ( '2d' );
ctx . fillStyle = '#1a1a1a' ;
ctx . fillRect ( 0 , 0 , 100 , 100 );
ctx . font = '60px Arial' ;
ctx . textAlign = 'center' ;
ctx . textBaseline = 'middle' ;
ctx . fillText ( selectedAvatar , 50 , 55 );
avatarToSave = canvas . toDataURL ();
}
}
if ( avatarToSave ) {
document . getElementById ( 'userAvatar' ). src = avatarToSave ;
closeAvatarModal ();
} else {
alert ( 'Selecciona un avatar o sube una foto.' );
}
}
Users can update their profile by re-taking the questionnaire or making direct updates.
backend/src/controllers/authController.js
const updateProfile = async ( req , res ) => {
const userId = req . user ?. id ;
if ( ! userId ) {
throw new AppError ( 'ID de usuario requerido' , 400 );
}
// Allowed fields for update
const allowedUpdates = [
'firstName' ,
'lastName' ,
'phone' ,
'dateOfBirth' ,
'gender' ,
'avatar' ,
'address'
];
// Filter only allowed fields
const updates = {};
Object . keys ( req . body ). forEach ( key => {
if ( allowedUpdates . includes ( key )) {
updates [ key ] = req . body [ key ];
}
});
if ( Object . keys ( updates ). length === 0 ) {
throw new AppError ( 'No hay campos para actualizar' , 400 );
}
// Update user
const user = await User . findByIdAndUpdate (
userId ,
updates ,
{
new: true ,
runValidators: true
}
);
if ( ! user ) {
throw new AppError ( 'Usuario no encontrado' , 404 );
}
const publicProfile = user . getPublicProfile ();
res . status ( 200 ). json ({
success: true ,
message: 'Perfil actualizado exitosamente' ,
user: publicProfile
});
};
Certain sensitive fields like email and password require separate endpoints with additional verification.
Profile Statistics Dashboard
The profile displays real-time statistics cards:
Workouts Total completed workouts and weekly progress
Calories Total calories burned across all sessions
Active Time Cumulative hours spent training
< div class = "profile-stats" >
< div class = "profile-stat" >
< div class = "profile-stat-value" id = "totalWorkouts" > 24 </ div >
< div class = "profile-stat-label" > Entrenamientos </ div >
</ div >
< div class = "profile-stat" >
< div class = "profile-stat-value" id = "totalCalories" > 8,420 </ div >
< div class = "profile-stat-label" > Calorías Quemadas </ div >
</ div >
< div class = "profile-stat" >
< div class = "profile-stat-value" id = "totalHours" > 18h </ div >
< div class = "profile-stat-label" > Tiempo Activo </ div >
</ div >
</ div >
Fitness Calculators
The profile page includes three built-in calculators:
BMI Calculator
Calculates Body Mass Index based on height and weight:
function calcularIMC () {
const peso = parseFloat ( document . getElementById ( 'pesoIMC' ). value );
const altura = parseFloat ( document . getElementById ( 'alturaIMC' ). value );
if ( ! peso || ! altura || peso <= 0 || altura <= 0 ) {
alert ( 'Por favor ingresa peso y altura válidos.' );
return ;
}
const alturaM = altura / 100 ;
const imc = peso / ( alturaM * alturaM );
let clasificacion = '' ;
if ( imc < 18.5 ) clasificacion = 'Bajo peso' ;
else if ( imc < 24.9 ) clasificacion = 'Peso normal' ;
else if ( imc < 29.9 ) clasificacion = 'Sobrepeso' ;
else clasificacion = 'Obesidad' ;
document . getElementById ( 'valorIMC' ). textContent = imc . toFixed ( 1 );
document . getElementById ( 'clasificacionIMC' ). textContent = clasificacion ;
}
Water Consumption Calculator
Calculates daily water intake needs:
function calcularAgua () {
const peso = parseFloat ( document . getElementById ( 'pesoAgua' ). value );
const edad = parseInt ( document . getElementById ( 'edadAgua' ). value );
if ( ! peso || ! edad ) {
alert ( "Por favor ingresa todos los datos" );
return ;
}
// Formula: adults need ~35ml per kg of weight
let litros = peso * 0.035 ;
// Age adjustment
if ( edad < 14 ) litros *= 0.8 ; // children
else if ( edad > 65 ) litros *= 0.9 ; // elderly
document . getElementById ( 'valorAgua' ). textContent = litros . toFixed ( 2 ) + " litros" ;
}
Macronutrient Calculator
Calculates daily calorie and macronutrient needs:
function calcularMacronutrientes () {
const edad = parseInt ( document . getElementById ( 'edad' ). value );
const altura = parseFloat ( document . getElementById ( 'altura' ). value );
const peso = parseFloat ( document . getElementById ( 'peso' ). value );
const actividad = document . getElementById ( 'actividad' ). value ;
const meta = document . getElementById ( 'meta' ). value ;
const genero = document . querySelector ( 'input[name="genero"]:checked' );
// Harris-Benedict Formula
let bmr = 0 ;
if ( genero . value === "masculino" ) {
bmr = 88.362 + ( 13.397 * peso ) + ( 4.799 * altura ) - ( 5.677 * edad );
} else {
bmr = 447.593 + ( 9.247 * peso ) + ( 3.098 * altura ) - ( 4.330 * edad );
}
// Activity factor
let factorActividad = 1.2 ;
if ( actividad === 'ligero' ) factorActividad = 1.375 ;
if ( actividad === 'moderado' ) factorActividad = 1.55 ;
if ( actividad === 'intenso' ) factorActividad = 1.725 ;
let calorias = bmr * factorActividad ;
// Goal adjustment
if ( meta === 'bajar' ) calorias -= 400 ;
if ( meta === 'subir' ) calorias += 400 ;
document . getElementById ( 'valorKcal' ). textContent = ` ${ Math . round ( calorias ) } kcal` ;
}
Activity Feed
The profile displays recent user activities:
< div class = "activity-list" >
< div class = "activity-item" >
< div class = "activity-icon" > 🏋️ </ div >
< div class = "activity-info" >
< div class = "activity-title" > Circuito Funcional </ div >
< div class = "activity-date" > Hoy, 10:30 AM </ div >
</ div >
< span class = "activity-badge" > Completado </ span >
</ div >
< div class = "activity-item" >
< div class = "activity-icon" > 🥗 </ div >
< div class = "activity-info" >
< div class = "activity-title" > Plan Nutricional Revisado </ div >
< div class = "activity-date" > Ayer, 8:00 PM </ div >
</ div >
< span class = "activity-badge" > Completado </ span >
</ div >
</ div >
Achievements Display
Users can view their unlocked achievements:
< div class = "achievements-grid" >
< div class = "achievement" >
< div class = "achievement-icon" > 🔥 </ div >
< div class = "achievement-name" > 7 Días Racha </ div >
</ div >
< div class = "achievement" >
< div class = "achievement-icon" > 💪 </ div >
< div class = "achievement-name" > Primer Entreno </ div >
</ div >
< div class = "achievement" >
< div class = "achievement-icon" > 🎯 </ div >
< div class = "achievement-name" > Meta Cumplida </ div >
</ div >
< div class = "achievement locked" >
< div class = "achievement-icon" > 🏆 </ div >
< div class = "achievement-name" > 30 Días </ div >
</ div >
</ div >
Streak Tracking
The profile prominently displays the user’s workout streak:
< div class = "streak-card" >
< div class = "streak-icon" > 🔥 </ div >
< div class = "streak-value" id = "streakCount" > 7 </ div >
< div class = "streak-label" > Días consecutivos </ div >
< div class = "streak-days" >
< div class = "streak-day completed" > L </ div >
< div class = "streak-day completed" > M </ div >
< div class = "streak-day completed" > X </ div >
< div class = "streak-day completed" > J </ div >
< div class = "streak-day completed" > V </ div >
< div class = "streak-day completed" > S </ div >
< div class = "streak-day today" > D </ div >
</ div >
</ div >
The streak feature encourages user engagement by rewarding consistent workout habits.
API Endpoints
Endpoint Method Description Auth Required /api/auth/profileGET Get user profile data Yes /api/auth/profilePUT Update profile information Yes /api/profile/photoPOST Upload profile photo Yes
Data Persistence
Profile data is stored in multiple locations:
MongoDB Database
Permanent storage of user profile in the User model with fitness profile subdocument
LocalStorage
Client-side caching for quick access and offline viewing
JWT Token
User ID encoded in authentication token for secure API requests