Skip to main content
The fitness questionnaire is a crucial onboarding step that collects user information to create personalized workout programs using AI.

Questionnaire Overview

The questionnaire (preguntas.html) consists of 10 questions that gather essential fitness data.
The questionnaire is shown immediately after registration or can be accessed later from the profile page to update fitness preferences.

Question Flow

The questionnaire uses a single-page application approach with smooth transitions between questions.

Question 1: Gender

Users select their biological gender for accurate calorie and exercise recommendations.
frontend/pages/preguntas.html
<div class="question active" id="q1">
  <h1>ALCANZA TUS METAS FITNESS CON FITAIID</h1>
  <h3>Elige tu género para iniciar</h3>
  <div class="option hombre" onclick="selectGender('hombre')">
    <span>Hombre</span>
  </div>
  <div class="option mujer" onclick="selectGender('mujer')">
    <span>Mujer</span>
  </div>
</div>

<script>
function selectGender(gender) {
  questionnaireData.gender = gender;
  nextQuestion('q2');
}
</script>

Question 2: Age

Age validation ensures users are at least 14 years old.
<div class="question" id="q2">
  <h2>¿Cuál es tu edad?</h2>
  <input type="number" id="edad" 
         placeholder="Ingresa tu edad (14 - 100)" 
         min="14" max="100" />
  <p id="errorEdad" class="error-msg"></p>
  <button onclick="validarEdad()">Siguiente →</button>
</div>

<script>
function validarEdad() {
  const edad = document.getElementById("edad");
  const error = document.getElementById("errorEdad");
  const valor = parseInt(edad.value);
  
  edad.classList.remove("invalid", "valid");
  error.textContent = "";
  
  if (!valor) {
    error.textContent = "Por favor ingresa tu edad.";
    edad.classList.add("invalid");
    return;
  }
  
  if (valor < 14 || valor > 100) {
    error.textContent = "La edad debe estar entre 14 y 100 años.";
    edad.classList.add("invalid");
    return;
  }
  
  questionnaireData.age = valor;
  edad.classList.add("valid");
  nextQuestion("q3");
}
</script>
Input length is limited to 3 digits to prevent excessively large values.

Question 3: Height

Height is collected in centimeters (100-250 cm range).
function validarAltura() {
  const altura = document.getElementById("altura");
  const error = document.getElementById("errorAltura");
  const valor = parseInt(altura.value);
  
  if (!valor) {
    error.textContent = "Por favor ingresa tu altura.";
    altura.classList.add("invalid");
    return;
  }
  
  if (valor < 100 || valor > 250) {
    error.textContent = "La altura debe estar entre 100 y 250 cm.";
    altura.classList.add("invalid");
    return;
  }
  
  questionnaireData.height = valor;
  altura.classList.add("valid");
  nextQuestion("q4");
}

Question 4: Weight

Current weight in kilograms (30-300 kg range).
function validarPeso() {
  const peso = document.getElementById("peso");
  const error = document.getElementById("errorPeso");
  const valor = parseInt(peso.value);
  
  if (!valor) {
    error.textContent = "Por favor ingresa tu peso.";
    peso.classList.add("invalid");
    return;
  }
  
  if (valor < 30 || valor > 300) {
    error.textContent = "El peso debe estar entre 30 y 300 kg.";
    peso.classList.add("invalid");
    return;
  }
  
  questionnaireData.weight = valor;
  peso.classList.add("valid");
  nextQuestion("q5");
}

Question 5: Fitness Level

Users select their current experience level with exercise.
<div class="question" id="q5">
  <h2>¿Cuál es tu nivel de experiencia?</h2>
  <div class="option" onclick="selectLevel('principiante')">Principiante</div>
  <div class="option" onclick="selectLevel('intermedio')">Intermedio</div>
  <div class="option" onclick="selectLevel('avanzado')">Avanzado</div>
</div>

<script>
function selectLevel(level) {
  questionnaireData.fitnessLevel = level;
  nextQuestion('q6');
}
</script>

Principiante

Little to no exercise experience

Intermedio

Regular exercise, 3-6 months experience

Avanzado

Consistent training, 1+ years experience

Question 6: Main Goal

Users select their primary fitness objective.
<div class="question" id="q6">
  <h2>¿Cuál es tu objetivo principal?</h2>
  <div class="option" onclick="selectGoal('tonificar')">Tonificar</div>
  <div class="option" onclick="selectGoal('ganar masa muscular')">
    Ganar masa muscular
  </div>
  <div class="option" onclick="selectGoal('bajar de peso')">
    Bajar de peso
  </div>
</div>

<script>
function selectGoal(goal) {
  questionnaireData.mainGoal = goal;
  nextQuestion('q7');
}
</script>

Question 7: Medical Conditions

Users disclose any injuries or medical conditions that may affect exercise.
function validarCondicion() {
  const condicion = document.getElementById("condicion");
  const error = document.getElementById("errorCondicion");
  const valor = condicion.value.trim();
  
  condicion.classList.remove("invalid", "valid");
  error.textContent = "";
  
  if (!valor) {
    error.textContent = "Debes escribir una respuesta (o escribe 'ninguna').";
    condicion.classList.add("invalid");
    return;
  }
  
  questionnaireData.medicalConditions = valor;
  condicion.classList.add("valid");
  nextQuestion("q8");
}
This information is critical for generating safe, appropriate workout recommendations.

Question 8: Training Location

Users choose where they prefer to train.
<div class="question" id="q8">
  <h2>¿Dónde prefieres entrenar?</h2>
  <div class="option" onclick="selectLocation('casa')">Casa</div>
  <div class="option" onclick="selectLocation('gym')">Gym</div>
</div>

<script>
function selectLocation(location) {
  questionnaireData.trainingLocation = location;
  nextQuestion('q9');
}
</script>

Question 9: Training Days

Users specify how many days per week they can commit to training.
<div class="question" id="q9">
  <h2>¿Cuántos días a la semana puedes entrenar?</h2>
  <div class="option" onclick="selectDays(1)">1</div>
  <div class="option" onclick="selectDays(2)">2</div>
  <div class="option" onclick="selectDays(3)">3</div>
  <div class="option" onclick="selectDays(4)">4</div>
  <div class="option" onclick="selectDays(5)">5</div>
</div>

<script>
function selectDays(days) {
  questionnaireData.trainingDaysPerWeek = days;
  nextQuestion('q10');
}
</script>

Question 10: Session Duration

Final question about preferred workout duration.
<div class="question" id="q10">
  <h2>¿Cuánto tiempo por sesión?</h2>
  <div class="option" onclick="selectDuration('30 min')">30 min</div>
  <div class="option" onclick="selectDuration('45 min')">45 min</div>
  <div class="option" onclick="selectDuration('1 hr')">1 hr</div>
  <div class="option" onclick="selectDuration('+1 hr')">+1 hr</div>
</div>

<script>
function selectDuration(duration) {
  questionnaireData.sessionDuration = duration;
  finishQuestionnaire(); // Last question - submit data
}
</script>

Data Structure

The questionnaire data is stored in a JavaScript object:
const questionnaireData = {
  gender: null,                  // 'hombre' | 'mujer'
  age: null,                     // 14-100
  height: null,                  // cm (100-250)
  weight: null,                  // kg (30-300)
  fitnessLevel: null,           // 'principiante' | 'intermedio' | 'avanzado'
  mainGoal: null,               // 'tonificar' | 'ganar masa muscular' | 'bajar de peso'
  medicalConditions: null,      // string
  trainingLocation: null,       // 'casa' | 'gym'
  trainingDaysPerWeek: null,   // 1-5
  sessionDuration: null         // '30 min' | '45 min' | '1 hr' | '+1 hr'
};

Submitting Questionnaire Data

When the user completes all questions, the data is sent to the backend.
1

Show Finish Screen

Display completion message while submitting:
async function finishQuestionnaire() {
  const current = document.querySelector(".question.active");
  current.classList.remove("active");
  document.getElementById("finish").classList.add("active");
  
  console.log('📤 Enviando cuestionario a MongoDB:', questionnaireData);
2

Get Authentication Token

Retrieve JWT token from localStorage:
  const authToken = localStorage.getItem('token') || 
                    localStorage.getItem('authToken');
  const userId = localStorage.getItem('userId');
  
  if (!authToken) {
    throw new Error('No se encontró token de autenticación');
  }
3

Send POST Request

Submit questionnaire data to protected endpoint:
  const response = await fetch(`${CONFIG.API_URL}/api/questionnaire`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${authToken}`
    },
    body: JSON.stringify({
      userId: userId,
      ...questionnaireData
    })
  });
4

Handle Response

Process the response and redirect:
  const data = await response.json();
  
  if (data.success) {
    // Save to localStorage for quick access
    localStorage.setItem('fitnessProfile', JSON.stringify(questionnaireData));
    
    document.getElementById('finishMessage').textContent =
      '¡Perfil fitness guardado exitosamente! 🎉 Redirigiendo';
    
    // Redirect to home after 2 seconds
    setTimeout(() => {
      window.location.href = 'home.html';
    }, 2000);
  }
}

Backend Processing

The backend validates and stores the questionnaire data in the user’s fitness profile.
// POST /api/questionnaire
router.post("/", protect, catchAsync(async (req, res) => {
  const { 
    userId, gender, age, height, weight, fitnessLevel, 
    mainGoal, medicalConditions, trainingLocation, 
    trainingDaysPerWeek, sessionDuration 
  } = req.body;
  
  // Get userId from body or token
  const finalUserId = userId || req.user._id.toString();
  
  // Verify ownership
  if (req.user._id.toString() !== finalUserId) {
    throw new AppError("No tienes permiso para guardar este cuestionario", 403);
  }
  
  // Validate MongoDB ID
  if (!mongoose.Types.ObjectId.isValid(finalUserId)) {
    throw new AppError("ID de usuario inválido", 400);
  }
  
  // Find user
  const user = await User.findById(finalUserId);
  if (!user) {
    throw new AppError("Usuario no encontrado", 404);
  }
  
  // Update fitness profile
  user.fitnessProfile = {
    gender,
    age,
    height,
    weight,
    fitnessLevel,
    mainGoal,
    medicalConditions,
    trainingLocation,
    trainingDaysPerWeek,
    sessionDuration,
    questionnaireCompleted: true,
    questionnaireCompletedAt: new Date()
  };
  
  await user.save();
  
  res.status(201).json({
    success: true,
    message: "Cuestionario guardado correctamente",
    data: {
      user: user.getPublicProfile(),
      fitnessProfile: user.fitnessProfile,
      bmi: user.bmi,
      bmiCategory: user.bmiCategory
    }
  });
}));

BMI Calculation

The backend automatically calculates BMI when height and weight are provided:
User Model Virtual
userSchema.virtual('bmi').get(function() {
  if (!this.fitnessProfile?.height || !this.fitnessProfile?.weight) {
    return null;
  }
  
  const heightInMeters = this.fitnessProfile.height / 100;
  const bmi = this.fitnessProfile.weight / (heightInMeters * heightInMeters);
  return Math.round(bmi * 10) / 10;
});

userSchema.virtual('bmiCategory').get(function() {
  const bmi = this.bmi;
  if (!bmi) return null;
  
  if (bmi < 18.5) return 'Bajo peso';
  if (bmi < 25) return 'Normal';
  if (bmi < 30) return 'Sobrepeso';
  return 'Obesidad';
});

UI Transitions

The questionnaire uses smooth CSS animations for transitions:
function nextQuestion(nextId) {
  const current = document.querySelector(".question.active");
  current.classList.remove("active");
  document.getElementById(nextId).classList.add("active");
  updateLogoPosition(nextId);
}

function updateLogoPosition(nextId) {
  const logo = document.querySelector(".logo-container");
  if (nextId !== "q1") {
    logo.classList.add("logo-centered");
  } else {
    logo.classList.remove("logo-centered");
  }
}

Error Handling

Comprehensive error handling ensures a smooth user experience:
try {
  const response = await fetch(`${CONFIG.API_URL}/api/questionnaire`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${authToken}`
    },
    body: JSON.stringify({ userId, ...questionnaireData })
  });
  
  const data = await response.json();
  
  if (data.success) {
    // Success handling
  } else {
    throw new Error(data.message || 'Error al guardar');
  }
} catch (error) {
  console.error('❌ Error:', error);
  
  if (error.message.includes('token')) {
    document.getElementById('finishMessage').innerHTML =
      '🔐 Tu sesión expiró.<br>Por favor inicia sesión nuevamente.';
    setTimeout(() => {
      window.location.href = 'login.html';
    }, 3000);
  } else {
    document.getElementById('finishMessage').innerHTML =
      '⚠️ Hubo un problema al guardar tu perfil.<br>Por favor intenta de nuevo.';
    
    // Show retry button
    const retryBtn = document.createElement('button');
    retryBtn.textContent = 'Reintentar';
    retryBtn.onclick = finishQuestionnaire;
    document.getElementById('finish').appendChild(retryBtn);
  }
}

Input Validation

Client-side validation prevents invalid data submission:
// Limit input length
document.getElementById("edad").addEventListener("input", function () {
  if (this.value.length > 3) this.value = this.value.slice(0, 3);
});

document.getElementById("altura").addEventListener("input", function () {
  if (this.value.length > 3) this.value = this.value.slice(0, 3);
});

document.getElementById("peso").addEventListener("input", function () {
  if (this.value.length > 3) this.value = this.value.slice(0, 3);
});

API Endpoints

EndpointMethodDescriptionAuth Required
/api/questionnairePOSTSubmit fitness questionnaireYes
/api/questionnaire/:userIdGETGet user fitness profileYes
/api/questionnaire/:userIdPUTUpdate fitness questionnaireYes
The questionnaire can be retaken at any time from the profile page to update fitness preferences and goals.

Build docs developers (and LLMs) love