fix: prevent form reset on error, add multi-select for goal/activity/injury
- Remove profileNextStep(1) from saveProfile catch block so users stay on current step when an error occurs instead of being sent back to step 1 - Convert Goal, Activity Level, and Injury Area fields from single-select (radio/dropdown) to multi-select checkboxes with comma-separated storage - Add validateMultiEnum backend validation for comma-separated enum values - Update trainer.js filterByInjury and goal checks for multi-value support - Update dietitian.js TDEE, calorie, and water calculations for multi-values Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,11 @@
|
||||
color: var(--gray);
|
||||
display: block;
|
||||
}
|
||||
.hint {
|
||||
font-size: 0.8rem;
|
||||
color: var(--gray);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Checkbox group */
|
||||
.checkbox-group {
|
||||
@@ -1159,47 +1164,47 @@
|
||||
<!-- Step 3: Yasam Tarzi -->
|
||||
<div id="profileStep3" class="hidden">
|
||||
<div class="form-group">
|
||||
<label>Hedef</label>
|
||||
<div class="radio-group">
|
||||
<label>Hedef <span class="hint">(birden fazla secilebilir)</span></label>
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="radio" name="profGoal" value="lose_weight">
|
||||
<input type="checkbox" name="profGoal" value="lose_weight">
|
||||
<span>Kilo Vermek</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="profGoal" value="gain_weight">
|
||||
<input type="checkbox" name="profGoal" value="gain_weight">
|
||||
<span>Kilo Almak</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="profGoal" value="build_muscle">
|
||||
<input type="checkbox" name="profGoal" value="build_muscle">
|
||||
<span>Kas Gelistirmek</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="profGoal" value="maintain">
|
||||
<input type="checkbox" name="profGoal" value="maintain">
|
||||
<span>Formu Korumak</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Aktivite Seviyesi</label>
|
||||
<div class="radio-group">
|
||||
<label>Aktivite Seviyesi <span class="hint">(birden fazla secilebilir)</span></label>
|
||||
<div class="checkbox-group">
|
||||
<label>
|
||||
<input type="radio" name="profActivity" value="sedentary">
|
||||
<input type="checkbox" name="profActivity" value="sedentary">
|
||||
<span>Hareketsiz<span class="radio-desc">Masa basi is, az hareket</span></span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="profActivity" value="light">
|
||||
<input type="checkbox" name="profActivity" value="light">
|
||||
<span>Hafif<span class="radio-desc">Haftada 1-2 gun hafif egzersiz</span></span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="profActivity" value="moderate">
|
||||
<input type="checkbox" name="profActivity" value="moderate">
|
||||
<span>Orta<span class="radio-desc">Haftada 3-5 gun egzersiz</span></span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="profActivity" value="active">
|
||||
<input type="checkbox" name="profActivity" value="active">
|
||||
<span>Aktif<span class="radio-desc">Haftada 6-7 gun egzersiz</span></span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="profActivity" value="very_active">
|
||||
<input type="checkbox" name="profActivity" value="very_active">
|
||||
<span>Cok Aktif<span class="radio-desc">Gunde 2 antrenman veya agir fiziksel is</span></span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -1414,18 +1419,17 @@
|
||||
<div id="injuryFields" class="cond-fields hidden">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Sakatlik Bolgesi</label>
|
||||
<select id="profInjuryArea">
|
||||
<option value="none">Seciniz</option>
|
||||
<option value="knee">Diz</option>
|
||||
<option value="back">Sirt / Bel</option>
|
||||
<option value="shoulder">Omuz</option>
|
||||
<option value="elbow">Dirsek</option>
|
||||
<option value="wrist">Bilek</option>
|
||||
<option value="ankle">Ayak Bilegi</option>
|
||||
<option value="hip">Kalca</option>
|
||||
<option value="neck">Boyun</option>
|
||||
</select>
|
||||
<label>Sakatlik Bolgesi <span class="hint">(birden fazla secilebilir)</span></label>
|
||||
<div class="checkbox-group">
|
||||
<label><input type="checkbox" name="profInjuryArea" value="knee"><span>Diz</span></label>
|
||||
<label><input type="checkbox" name="profInjuryArea" value="back"><span>Sirt / Bel</span></label>
|
||||
<label><input type="checkbox" name="profInjuryArea" value="shoulder"><span>Omuz</span></label>
|
||||
<label><input type="checkbox" name="profInjuryArea" value="elbow"><span>Dirsek</span></label>
|
||||
<label><input type="checkbox" name="profInjuryArea" value="wrist"><span>Bilek</span></label>
|
||||
<label><input type="checkbox" name="profInjuryArea" value="ankle"><span>Ayak Bilegi</span></label>
|
||||
<label><input type="checkbox" name="profInjuryArea" value="hip"><span>Kalca</span></label>
|
||||
<label><input type="checkbox" name="profInjuryArea" value="neck"><span>Boyun</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sakatlik Siddeti</label>
|
||||
@@ -2133,8 +2137,14 @@ function populateProfileForm(p) {
|
||||
setRadioValue('bodyType', p.body_type);
|
||||
|
||||
// Step 3
|
||||
setRadioValue('profGoal', p.goal);
|
||||
setRadioValue('profActivity', p.activity_level);
|
||||
if (p.goal) {
|
||||
var goals = typeof p.goal === 'string' ? p.goal.split(',').map(function(s){return s.trim();}) : [p.goal];
|
||||
setCheckedValues('profGoal', goals);
|
||||
}
|
||||
if (p.activity_level) {
|
||||
var activities = typeof p.activity_level === 'string' ? p.activity_level.split(',').map(function(s){return s.trim();}) : [p.activity_level];
|
||||
setCheckedValues('profActivity', activities);
|
||||
}
|
||||
if (p.job_type) document.getElementById('profJobType').value = p.job_type;
|
||||
if (p.sleep_hours) document.getElementById('profSleep').value = p.sleep_hours;
|
||||
setRadioValue('sleepQuality', p.sleep_quality);
|
||||
@@ -2170,7 +2180,10 @@ function populateProfileForm(p) {
|
||||
if (p.has_injury) {
|
||||
document.getElementById('profHasInjury').checked = true;
|
||||
toggleInjuryFields();
|
||||
if (p.injury_area) document.getElementById('profInjuryArea').value = p.injury_area;
|
||||
if (p.injury_area) {
|
||||
var areas = typeof p.injury_area === 'string' ? p.injury_area.split(',').map(function(s){return s.trim();}) : [p.injury_area];
|
||||
setCheckedValues('profInjuryArea', areas);
|
||||
}
|
||||
setRadioValue('injurySeverity', p.injury_severity);
|
||||
if (p.injury_notes) document.getElementById('profInjuryNotes').value = p.injury_notes;
|
||||
}
|
||||
@@ -2289,8 +2302,8 @@ async function saveProfile() {
|
||||
body_type: getRadioValue('bodyType') || null,
|
||||
|
||||
// Step 3
|
||||
goal: getRadioValue('profGoal'),
|
||||
activity_level: getRadioValue('profActivity'),
|
||||
goal: getCheckedValues('profGoal').join(','),
|
||||
activity_level: getCheckedValues('profActivity').join(','),
|
||||
job_type: document.getElementById('profJobType').value || null,
|
||||
sleep_hours: parseFloat(document.getElementById('profSleep').value) || null,
|
||||
sleep_quality: getRadioValue('sleepQuality') || null,
|
||||
@@ -2310,7 +2323,7 @@ async function saveProfile() {
|
||||
|
||||
// Step 5
|
||||
has_injury: document.getElementById('profHasInjury').checked ? 1 : 0,
|
||||
injury_area: document.getElementById('profInjuryArea').value || 'none',
|
||||
injury_area: getCheckedValues('profInjuryArea').join(',') || 'none',
|
||||
injury_severity: getRadioValue('injurySeverity') || null,
|
||||
injury_notes: document.getElementById('profInjuryNotes').value || null,
|
||||
has_disability: document.getElementById('profHasDisability').checked ? 1 : 0,
|
||||
@@ -2371,7 +2384,7 @@ async function saveProfile() {
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.style.display = 'block';
|
||||
profileNextStep(1);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Programimi Olustur';
|
||||
|
||||
@@ -72,6 +72,17 @@ function validateEnum(value, validValues, fieldName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateMultiEnum(value, validValues, fieldName) {
|
||||
if (value === undefined || value === null || value === "") return null;
|
||||
const parts = String(value).split(",").map((s) => s.trim()).filter(Boolean);
|
||||
for (const part of parts) {
|
||||
if (!validValues.includes(part)) {
|
||||
return `Invalid ${fieldName} value: ${part}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateNumber(value, min, max, fieldName) {
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
const num = Number(value);
|
||||
@@ -90,8 +101,8 @@ function validate(body) {
|
||||
validateNumber(body.height, 50, 300, "Height"),
|
||||
validateNumber(body.weight, 20, 500, "Weight"),
|
||||
// Optional enums
|
||||
validateEnum(body.activity_level, VALID_ACTIVITY_LEVELS, "activity_level"),
|
||||
validateEnum(body.goal, VALID_GOALS, "goal"),
|
||||
validateMultiEnum(body.activity_level, VALID_ACTIVITY_LEVELS, "activity_level"),
|
||||
validateMultiEnum(body.goal, VALID_GOALS, "goal"),
|
||||
validateEnum(body.body_type, VALID_BODY_TYPES, "body_type"),
|
||||
validateEnum(body.stress_level, VALID_STRESS_LEVELS, "stress_level"),
|
||||
validateEnum(body.sleep_quality, VALID_SLEEP_QUALITY, "sleep_quality"),
|
||||
@@ -100,7 +111,7 @@ function validate(body) {
|
||||
validateEnum(body.workout_location, VALID_WORKOUT_LOCATIONS, "workout_location"),
|
||||
validateEnum(body.fitness_goal, VALID_FITNESS_GOALS, "fitness_goal"),
|
||||
validateEnum(body.cardio_preference, VALID_CARDIO_PREFERENCES, "cardio_preference"),
|
||||
validateEnum(body.injury_area, VALID_INJURY_AREAS, "injury_area"),
|
||||
validateMultiEnum(body.injury_area, VALID_INJURY_AREAS, "injury_area"),
|
||||
validateEnum(body.injury_severity, VALID_INJURY_SEVERITY, "injury_severity"),
|
||||
validateEnum(body.disability_type, VALID_DISABILITY_TYPES, "disability_type"),
|
||||
validateEnum(body.mobility_level, VALID_MOBILITY_LEVELS, "mobility_level"),
|
||||
|
||||
@@ -58,24 +58,23 @@ const ACTIVITY_MULTIPLIERS = {
|
||||
};
|
||||
|
||||
function calculateTDEE(bmr, activityLevel) {
|
||||
return bmr * (ACTIVITY_MULTIPLIERS[activityLevel] || 1.55);
|
||||
// Support comma-separated multi-select: use highest multiplier
|
||||
const levels = String(activityLevel).split(",").map((s) => s.trim()).filter(Boolean);
|
||||
const multipliers = levels.map((lvl) => ACTIVITY_MULTIPLIERS[lvl]).filter(Boolean);
|
||||
const multiplier = multipliers.length > 0 ? Math.max(...multipliers) : 1.55;
|
||||
return bmr * multiplier;
|
||||
}
|
||||
|
||||
function adjustCaloriesForGoal(tdee, goal) {
|
||||
let calories = tdee;
|
||||
switch (goal) {
|
||||
case "lose_weight":
|
||||
const goals = String(goal).split(",").map((s) => s.trim());
|
||||
// Apply the first matching goal adjustment (priority order)
|
||||
if (goals.includes("lose_weight")) {
|
||||
calories -= 500;
|
||||
break;
|
||||
case "gain_weight":
|
||||
} else if (goals.includes("gain_weight")) {
|
||||
calories += 500;
|
||||
break;
|
||||
case "build_muscle":
|
||||
} else if (goals.includes("build_muscle")) {
|
||||
calories += 300;
|
||||
break;
|
||||
case "maintain":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Never exceed a 20% deficit
|
||||
const minAllowed = tdee * 0.8;
|
||||
@@ -156,8 +155,9 @@ function selectDietType(profile) {
|
||||
if (profile.has_diabetes) return "diabetic_friendly";
|
||||
if (profile.heart_condition || profile.blood_pressure === "high") return "heart_healthy";
|
||||
if (profile.has_arthritis || profile.has_fibromyalgia) return "anti_inflammatory";
|
||||
if (goal === "lose_weight") return "calorie_deficit";
|
||||
if (goal === "build_muscle") return "high_protein";
|
||||
const goals = goal.split(",").map((s) => s.trim());
|
||||
if (goals.includes("lose_weight")) return "calorie_deficit";
|
||||
if (goals.includes("build_muscle")) return "high_protein";
|
||||
if (profile.workout_location === "none") return "balanced";
|
||||
if (profile.post_surgery || profile.rehabilitation) return "recovery_gentle";
|
||||
|
||||
@@ -318,7 +318,7 @@ function getSupplements(profile, dietType) {
|
||||
rationale: "Supports bone health, immune function, and mood regulation",
|
||||
});
|
||||
|
||||
if (profile.goal === "build_muscle" || dietType === "high_protein") {
|
||||
if ((profile.goal || "").split(",").map((s) => s.trim()).includes("build_muscle") || dietType === "high_protein") {
|
||||
supplements.push({
|
||||
name: "Whey Protein (or plant-based protein)",
|
||||
dosage: "25-30g post-workout",
|
||||
@@ -451,8 +451,10 @@ function getSupplements(profile, dietType) {
|
||||
function calculateWaterIntake(weight, activityLevel, goal) {
|
||||
// Base: 33ml per kg of body weight
|
||||
let water = (weight * 33) / 1000;
|
||||
if (activityLevel === "active" || activityLevel === "very_active") water += 0.5;
|
||||
if (goal === "lose_weight") water += 0.3;
|
||||
const levels = String(activityLevel).split(",").map((s) => s.trim());
|
||||
if (levels.includes("active") || levels.includes("very_active")) water += 0.5;
|
||||
const goals = String(goal).split(",").map((s) => s.trim());
|
||||
if (goals.includes("lose_weight")) water += 0.3;
|
||||
return Math.round(water * 10) / 10;
|
||||
}
|
||||
|
||||
|
||||
@@ -406,9 +406,16 @@ function pickRandom(arr, count) {
|
||||
return shuffled.slice(0, Math.min(count, arr.length));
|
||||
}
|
||||
|
||||
function hasGoal(profile, goalValue) {
|
||||
const goal = profile.goal || "maintain";
|
||||
return goal.split(",").map((s) => s.trim()).includes(goalValue);
|
||||
}
|
||||
|
||||
function filterByInjury(exercises, injuryArea) {
|
||||
if (!injuryArea || injuryArea === "none") return exercises;
|
||||
return exercises.filter((e) => !e.contraindicated_injuries.includes(injuryArea));
|
||||
const areas = String(injuryArea).split(",").map((s) => s.trim()).filter((s) => s && s !== "none");
|
||||
if (areas.length === 0) return exercises;
|
||||
return exercises.filter((e) => !areas.some((area) => e.contraindicated_injuries.includes(area)));
|
||||
}
|
||||
|
||||
function filterByConditions(exercises, profile) {
|
||||
@@ -743,7 +750,8 @@ function buildInjuryNote(profile) {
|
||||
mild: "mild", moderate: "moderate", severe: "severe",
|
||||
};
|
||||
|
||||
const areaName = areaNames[profile.injury_area] || profile.injury_area;
|
||||
const areas = String(profile.injury_area).split(",").map((s) => s.trim()).filter((s) => s && s !== "none");
|
||||
const areaName = areas.map((a) => areaNames[a] || a).join(", ");
|
||||
const severityName = severityNames[profile.injury_severity] || "mild";
|
||||
let note = `Program adapted for ${severityName} ${areaName} injury.`;
|
||||
|
||||
@@ -872,7 +880,7 @@ function buildCardioDay(dayNumber, dayName, focus, profile, week) {
|
||||
const bodyType = profile.body_type || "mesomorph";
|
||||
let exercises = [];
|
||||
|
||||
if ((bodyType === "endomorph" || profile.goal === "lose_weight") && !profile.heart_condition) {
|
||||
if ((bodyType === "endomorph" || hasGoal(profile, "lose_weight")) && !profile.heart_condition) {
|
||||
const safeHiit = filterByLocation(filterByEquipment(filterByConditions(filterByInjury(EXERCISES.hiit, injuryArea), profile), profile), location);
|
||||
const safeCardio = filterByLocation(filterByEquipment(filterByConditions(filterByInjury(EXERCISES.cardio, injuryArea), profile), profile), location);
|
||||
exercises = [...pickRandom(safeHiit, 4), ...pickRandom(safeCardio, 1)];
|
||||
@@ -1120,7 +1128,6 @@ function generateWheelchairProgram(profile, week) {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function applyGoalAdjustments(days, profile, week) {
|
||||
const goal = profile.goal || "maintain";
|
||||
const bodyType = profile.body_type || "mesomorph";
|
||||
const location = profile.workout_location || "gym";
|
||||
|
||||
@@ -1130,7 +1137,7 @@ function applyGoalAdjustments(days, profile, week) {
|
||||
let modified = { ...day, exercises: [...day.exercises] };
|
||||
|
||||
// For lose_weight: add cardio to strength days
|
||||
if (goal === "lose_weight" && day.type === "strength" && !profile.heart_condition) {
|
||||
if (hasGoal(profile, "lose_weight") && day.type === "strength" && !profile.heart_condition) {
|
||||
const injuryArea = profile.has_injury ? (profile.injury_area || "none") : "none";
|
||||
let safeCardio = filterByLocation(filterByEquipment(filterByConditions(filterByInjury(EXERCISES.cardio, injuryArea), profile), profile), location);
|
||||
if (safeCardio.length > 0) {
|
||||
@@ -1154,7 +1161,7 @@ function applyGoalAdjustments(days, profile, week) {
|
||||
}
|
||||
|
||||
// Gain weight: remove excess cardio from strength days
|
||||
if (goal === "gain_weight") {
|
||||
if (hasGoal(profile, "gain_weight")) {
|
||||
modified.exercises = modified.exercises.filter((e) => {
|
||||
return !EXERCISES.cardio.some((c) => c.name === e.name);
|
||||
});
|
||||
@@ -1165,11 +1172,10 @@ function applyGoalAdjustments(days, profile, week) {
|
||||
}
|
||||
|
||||
function adjustCardioRestDays(days, profile, week) {
|
||||
const goal = profile.goal || "maintain";
|
||||
const bodyType = profile.body_type || "mesomorph";
|
||||
let cardioDaysNeeded = 0;
|
||||
|
||||
if (goal === "lose_weight" && !profile.heart_condition) cardioDaysNeeded = 2;
|
||||
if (hasGoal(profile, "lose_weight") && !profile.heart_condition) cardioDaysNeeded = 2;
|
||||
else if (bodyType === "endomorph" && !profile.heart_condition) cardioDaysNeeded = 1;
|
||||
|
||||
let converted = 0;
|
||||
|
||||
Reference in New Issue
Block a user