document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('tdee-form');
const steps = document.querySelectorAll('.tdee-step');
const progressBar = document.getElementById('tdee-progress');
const resultsDiv = document.getElementById('tdee-results');
const resetBtn = document.querySelector('.tdee-reset-btn');
const stepOrder = ['1', '2', '3', '3_1', '4', '5'];
let currentStepIndex = 0;
const heightUnitSelect = document.getElementById('height-unit');
const heightCmGroup = document.getElementById('height-cm-group');
const heightFtInGroup = document.getElementById('height-ft-in-group');
const heightCmInput = document.getElementById('height-cm');
const heightFtInput = document.getElementById('height-ft');
const heightInInput = document.getElementById('height-in');
const weightUnitSelect = document.getElementById('weight-unit');
const weightKgGroup = document.getElementById('weight-kg-group');
const weightLbGroup = document.getElementById('weight-lb-group');
const weightKgInput = document.getElementById('weight-kg');
const weightLbInput = document.getElementById('weight-lb');
const goalSelect = document.getElementById('goal');
const weightChangeSpeedGroup = document.getElementById('weight-change-speed-group');
const weightChangeSpeedInput = document.getElementById('weight-change-speed-input');
const weightChangeSpeedButtonsContainer = document.getElementById('weight-change-speed-buttons');
const targetWeightKgInput = document.getElementById('target-weight-kg');
const targetWeightDisplay = document.getElementById('target-weight-display');
function updateProgressBar() {
let actualStepOrder;
if (goalSelect.value === 'maintain') {
actualStepOrder = stepOrder.filter(s => s !== '3_1');
} else {
actualStepOrder = stepOrder;
}
const currentActualIndex = actualStepOrder.indexOf(stepOrder[currentStepIndex]);
const progress = ((currentActualIndex + 1) / actualStepOrder.length) * 100;
progressBar.style.width = `${progress}%`;
}
function showStep(stepNumId, direction = 'next') {
const prevStepElement = document.querySelector(`.tdee-step[data-step="${stepOrder[currentStepIndex]}"]`);
const nextStepElement = document.querySelector(`.tdee-step[data-step="${stepNumId}"]`);
if (prevStepElement) {
if (direction === 'next') {
prevStepElement.classList.add('tdee-slide-left');
} else {
prevStepElement.classList.add('tdee-hidden');
}
prevStepElement.style.pointerEvents = 'none';
}
if (nextStepElement) {
nextStepElement.classList.remove('tdee-hidden', 'tdee-slide-left');
nextStepElement.style.position = 'relative';
nextStepElement.style.transform = 'translateX(0)';
nextStepElement.style.opacity = '1';
nextStepElement.style.pointerEvents = 'auto';
}
currentStepIndex = stepOrder.indexOf(stepNumId);
updateProgressBar();
}
function validateStep(stepId) {
let isValid = true;
const currentStepElement = document.querySelector(`.tdee-step[data-step="${stepId}"]`);
const requiredInputs = currentStepElement.querySelectorAll('[required]:not([style*="display: none"])');
requiredInputs.forEach(input => {
if (!input.value || (input.type === 'number' && isNaN(parseFloat(input.value)))) {
isValid = false;
input.reportValidity();
}
});
if (stepId === '1') {
if (heightUnitSelect.value === 'ft_in' && (!heightFtInput.value || !heightInInput.value)) {
isValid = false;
heightFtInput.reportValidity();
heightInInput.reportValidity();
}
}
if (stepId === '3_1') {
const currentWeightKg = parseFloat(document.getElementById('weight-kg').value) || (parseFloat(document.getElementById('weight-lb').value) * 0.453592);
const targetWeightKg = parseFloat(targetWeightKgInput.value);
const goal = goalSelect.value;
if (isNaN(targetWeightKg)) {
isValid = false;
targetWeightKgInput.reportValidity();
} else if (goal === 'lose' && targetWeightKg >= currentWeightKg) {
isValid = false;
alert('Желаемый вес для снижения должен быть меньше текущего веса.');
targetWeightKgInput.focus();
} else if (goal === 'gain' && targetWeightKg <= currentWeightKg) {
isValid = false;
alert('Желаемый вес для набора массы должен быть больше текущего веса.');
targetWeightKgInput.focus();
}
}
if (stepId === '5') {
const proteinPercentage = parseFloat(document.getElementById('protein-percentage').value) || 0;
const carbsPercentage = parseFloat(document.getElementById('carbs-percentage').value) || 0;
const fatPercentage = parseFloat(document.getElementById('fat-percentage').value) || 0;
const totalMacroPercentage = proteinPercentage + carbsPercentage + fatPercentage;
if (totalMacroPercentage !== 100) {
isValid = false;
alert('Сумма процентов белков, углеводов и жиров должна быть равна 100%. Пожалуйста, скорректируйте.');
document.getElementById('protein-percentage').focus();
}
}
if (stepId === '3' && goalSelect.value !== 'maintain') {
if (!weightChangeSpeedInput.value || isNaN(parseFloat(weightChangeSpeedInput.value))) {
isValid = false;
alert('Пожалуйста, выберите желаемую скорость изменения веса.');
}
}
return isValid;
}
document.querySelectorAll('.tdee-next-btn').forEach(button => {
button.addEventListener('click', () => {
const currentStepId = stepOrder[currentStepIndex];
if (validateStep(currentStepId)) {
let nextStepId = button.dataset.nextStep;
if (currentStepId === '3') {
const goal = goalSelect.value;
if (goal === 'maintain') {
nextStepId = '4';
} else {
nextStepId = '3_1';
}
}
showStep(nextStepId, 'next');
}
});
});
document.querySelectorAll('.tdee-prev-btn').forEach(button => {
button.addEventListener('click', () => {
let prevStepId = button.dataset.prevStep;
if (stepOrder[currentStepIndex] === '4') {
const goal = goalSelect.value;
if (goal === 'maintain') {
prevStepId = '3';
} else {
prevStepId = '3_1';
}
}
showStep(prevStepId, 'prev');
});
});
heightUnitSelect.addEventListener('change', () => {
if (heightUnitSelect.value === 'cm') {
heightCmGroup.classList.remove('tdee-hidden');
heightFtInGroup.classList.add('tdee-hidden');
heightCmInput.setAttribute('required', 'true');
heightFtInput.removeAttribute('required');
heightInInput.removeAttribute('required');
} else {
heightCmGroup.classList.add('tdee-hidden');
heightFtInGroup.classList.remove('tdee-hidden');
heightCmInput.removeAttribute('required');
heightFtInput.setAttribute('required', 'true');
heightInInput.setAttribute('required', 'true');
}
});
heightUnitSelect.dispatchEvent(new Event('change'));
weightUnitSelect.addEventListener('change', () => {
if (weightUnitSelect.value === 'kg') {
weightKgGroup.classList.remove('tdee-hidden');
weightLbGroup.classList.add('tdee-hidden');
weightKgInput.setAttribute('required', 'true');
weightLbInput.removeAttribute('required');
} else {
weightKgGroup.classList.add('tdee-hidden');
weightLbGroup.classList.remove('tdee-hidden');
weightKgInput.removeAttribute('required');
weightLbInput.setAttribute('required', 'true');
}
});
weightUnitSelect.dispatchEvent(new Event('change'));
goalSelect.addEventListener('change', () => {
if (goalSelect.value === 'maintain') {
weightChangeSpeedGroup.style.display = 'none';
weightChangeSpeedInput.removeAttribute('required');
targetWeightKgInput.removeAttribute('required');
targetWeightKgInput.value = '';
} else {
weightChangeSpeedGroup.style.display = 'flex';
weightChangeSpeedInput.setAttribute('required', 'true');
targetWeightKgInput.setAttribute('required', 'true');
}
updateProgressBar();
});
goalSelect.dispatchEvent(new Event('change'));
weightChangeSpeedButtonsContainer.addEventListener('click', (event) => {
const clickedButton = event.target.closest('.tdee-button-option');
if (clickedButton) {
weightChangeSpeedButtonsContainer.querySelectorAll('.tdee-button-option').forEach(btn => {
btn.classList.remove('tdee-selected');
});
clickedButton.classList.add('tdee-selected');
weightChangeSpeedInput.value = clickedButton.dataset.value;
}
});
weightChangeSpeedButtonsContainer.querySelector(`[data-value="${weightChangeSpeedInput.value}"]`).classList.add('tdee-selected');
resetBtn.addEventListener('click', () => {
form.reset();
resultsDiv.classList.remove('tdee-show');
resultsDiv.classList.add('tdee-hidden');
showStep(stepOrder[0]);
heightUnitSelect.dispatchEvent(new Event('change'));
weightUnitSelect.dispatchEvent(new Event('change'));
weightChangeSpeedInput.value = '0.5';
weightChangeSpeedButtonsContainer.querySelectorAll('.tdee-button-option').forEach(btn => {
btn.classList.remove('tdee-selected');
});
weightChangeSpeedButtonsContainer.querySelector(`[data-value="0.5"]`).classList.add('tdee-selected');
goalSelect.dispatchEvent(new Event('change'));
});
form.addEventListener('submit', (e) => {
e.preventDefault();
if (!validateStep(stepOrder[currentStepIndex])) {
return;
}
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
let heightCm = parseFloat(data['height-cm']);
if (data['height-unit'] === 'ft_in') {
const ft = parseFloat(data['height-ft']) || 0;
const inches = parseFloat(data['height-in']) || 0;
heightCm = (ft * 30.48) + (inches * 2.54);
}
let weightKg = parseFloat(data['weight-kg']);
if (data['weight-unit'] === 'lb') {
weightKg = parseFloat(data['weight-lb']) * 0.453592;
}
const gender = data.gender;
const age = parseInt(data.age);
const activityLevel = data['activity-level'];
const goal = data.goal;
const weightChangeSpeed = parseFloat(data['weight-change-speed']) || 0;
const targetWeightKg = parseFloat(data['target-weight-kg']) || null;
const proteinPercentage = parseFloat(data['protein-percentage']) || 25;
const carbsPercentage = parseFloat(data['carbs-percentage']) || 50;
const fatPercentage = parseFloat(data['fat-percentage']) || 25;
let bmrMifflin;
if (gender === 'male') {
bmrMifflin = (10 * weightKg) + (6.25 * heightCm) - (5 * age) + 5;
} else {
bmrMifflin = (10 * weightKg) + (6.25 * heightCm) - (5 * age) - 161;
}
let bmrHarris;
if (gender === 'male') {
bmrHarris = 88.362 + (13.397 * weightKg) + (4.799 * heightCm) - (5.677 * age);
} else {
bmrHarris = 447.593 + (9.247 * weightKg) + (3.098 * heightCm) - (4.330 * age);
}
const activityFactors = {
'sedentary': 1.2,
'lightly_active': 1.375,
'moderately_active': 1.55,
'active': 1.725,
'very_active': 1.9
};
const activityFactor = activityFactors[activityLevel];
const tdeeKcal = bmrMifflin * activityFactor;
const tdeeKj = tdeeKcal * 4.184;
const tefKcal = tdeeKcal * 0.10;
let targetKcal = tdeeKcal;
let deficitSurplusText = '';
let deficitSurplusValue = 0;
const caloriesPerKg = 7700;
if (goal === 'lose') {
const deficitKcal = (weightChangeSpeed * caloriesPerKg) / 7;
targetKcal = tdeeKcal - deficitKcal;
deficitSurplusText = 'дефицит';
deficitSurplusValue = deficitKcal;
} else if (goal === 'gain') {
const surplusKcal = (weightChangeSpeed * caloriesPerKg) / 7;
targetKcal = tdeeKcal + surplusKcal;
deficitSurplusText = 'профицит';
deficitSurplusValue = surplusKcal;
}
const targetKj = targetKcal * 4.184;
const proteinGrams = (targetKcal * (proteinPercentage / 100)) / 4;
const carbsGrams = (targetKcal * (carbsPercentage / 100)) / 4;
const fatGrams = (targetKcal * (fatPercentage / 100)) / 9;
let weeksToGoal = 'Не применимо';
if (goal !== 'maintain' && targetWeightKg !== null && weightChangeSpeed > 0) {
const weightDifference = Math.abs(weightKg - targetWeightKg);
weeksToGoal = (weightDifference / weightChangeSpeed).toFixed(0);
}
const recommendations = [];
const MIN_CALORIES_MALE = 1500;
const MIN_CALORIES_FEMALE = 1200;
if (targetKcal < (gender === 'male' ? MIN_CALORIES_MALE : MIN_CALORIES_FEMALE)) {
recommendations.push(`⚠️ Ваше целевое потребление калорий (${targetKcal.toFixed(0)} ккал) ниже рекомендованного минимума.`);
}
if (goal === 'lose' && weightChangeSpeed > 1) {
recommendations.push('❗ Рекомендуется снижение веса не более 0.5-1 кг в неделю.');
}
if (goal === 'gain' && weightChangeSpeed > 0.5) {
recommendations.push('❗ Рекомендуется набирать не более 0.25-0.5 кг в неделю для минимизации набора жира.');
}
document.getElementById('result-tdee-kcal').textContent = tdeeKcal.toFixed(0);
document.getElementById('result-tdee-kj').textContent = tdeeKj.toFixed(0);
document.getElementById('result-bmr-mifflin-kcal').textContent = bmrMifflin.toFixed(0);
document.getElementById('result-bmr-mifflin-kj').textContent = (bmrMifflin * 4.184).toFixed(0);
document.getElementById('result-bmr-harris-kcal').textContent = bmrHarris.toFixed(0);
document.getElementById('result-bmr-harris-kj').textContent = (bmrHarris * 4.184).toFixed(0);
document.getElementById('result-tef').textContent = tefKcal.toFixed(0);
if (goal !== 'maintain') {
document.getElementById('target-calories-title').style.display = 'block';
document.getElementById('target-calories-value').style.display = 'block';
document.getElementById('deficit-surplus-value').style.display = 'block';
document.getElementById('target-goal-text').textContent = goal === 'lose' ? 'снижения веса' : 'набора массы';
document.getElementById('result-target-kcal').textContent = targetKcal.toFixed(0);
document.getElementById('result-target-kj').textContent = targetKj.toFixed(0);
document.getElementById('deficit-surplus-text').textContent = deficitSurplusText;
document.getElementById('result-deficit-surplus').textContent = deficitSurplusValue.toFixed(0);
document.getElementById('weight-change-title').style.display = 'block';
document.getElementById('weight-change-info').style.display = 'block';
document.getElementById('goal-speed-info').textContent = `${weightChangeSpeed} кг/нед`;
document.getElementById('weeks-to-goal').textContent = weeksToGoal;
targetWeightDisplay.textContent = targetWeightKg.toFixed(1);
} else {
document.getElementById('target-calories-title').style.display = 'none';
document.getElementById('target-calories-value').style.display = 'none';
document.getElementById('deficit-surplus-value').style.display = 'none';
document.getElementById('weight-change-title').style.display = 'none';
document.getElementById('weight-change-info').style.display = 'none';
}
document.getElementById('result-protein-g').textContent = proteinGrams.toFixed(1);
document.getElementById('result-protein-percent').textContent = proteinPercentage;
document.getElementById('result-carbs-g').textContent = carbsGrams.toFixed(1);
document.getElementById('result-carbs-percent').textContent = carbsPercentage;
document.getElementById('result-fat-g').textContent = fatGrams.toFixed(1);
document.getElementById('result-fat-percent').textContent = fatPercentage;
const recommendationsList = document.getElementById('recommendations-list');
recommendationsList.innerHTML = '';
if (recommendations.length > 0) {
document.getElementById('recommendations-title').style.display = 'block';
recommendations.forEach(rec => {
const li = document.createElement('li');
li.innerHTML = rec;
recommendationsList.appendChild(li);
});
} else {
document.getElementById('recommendations-title').style.display = 'none';
}
// --- БЛОК СОЗДАНИЯ ТАБЛИЦЫ ПРОГНОЗА ---
const tableContainer = document.getElementById('weight-prognosis-table-container');
const tableElement = document.getElementById('weight-prognosis-table');
if (goal !== 'maintain' && targetWeightKg !== null && weightChangeSpeed > 0) {
tableContainer.classList.remove('tdee-hidden');
const initialWeight = weightKg;
let tableHTML = `
Неделя
Прогнозируемый вес (кг)
`;
tableHTML += `
Начало
${initialWeight.toFixed(1)}
`;
// Безопасный лимит в 520 недель (10 лет)
for (let i = 1; i <= 520; i++) {
const predictedWeight = (goal === 'lose')
? initialWeight - (weightChangeSpeed * i)
: initialWeight + (weightChangeSpeed * i);
const isTargetReached = (goal === 'lose')
? predictedWeight <= targetWeightKg
: predictedWeight >= targetWeightKg;
if (isTargetReached) {
tableHTML += `
Цель
${targetWeightKg.toFixed(1)}
`;
break;
} else {
tableHTML += `
${i}
${predictedWeight.toFixed(1)}
`;
}
}
tableHTML += ``;
tableElement.innerHTML = tableHTML;
} else {
tableContainer.classList.add('tdee-hidden');
}
form.classList.add('tdee-hidden');
resultsDiv.classList.remove('tdee-hidden');
setTimeout(() => {
resultsDiv.classList.add('tdee-show');
}, 50);
});
showStep(stepOrder[0]);
// Загрузчик Chart.js полностью удален
});