👥 Роли учеников в команде
Распределение ответственности при работе над проектом
Полный вариант 5 ролей · 5 компьютеров
| Роль | Что делает |
|---|---|
| 🧬 Аналитик данных | Отвечает за биологические показатели (возраст, ЧСС, зоны), проверяет формулы |
| 💻 Frontend-инженер | Пишет HTML/CSS/JS код, работает в онлайн-редакторе |
| 🤖 Prompt-инженер | Составляет промпты для ИИ, проверяет результат |
| 🔍 Тестировщик (QA) | Запускает Peer Review, проверяет на мобильных, фиксирует баги |
| 🎤 Спикер | Презентует продукт, защищает решения команды |
Адаптация для класса с малым числом компьютеров 2–3 ПК
| Объединённая роль | Что делает |
|---|---|
| 🧬🔍 Аналитик-тестировщик | Отвечает за данные + тестирует продукт на мобильных устройствах |
| 💻 Frontend-Prompt инженер | Пишет код + составляет промпты для ИИ |
| 🎤 Спикер | Презентует продукт (роль остаётся отдельной) |
📋 Справка по проекту «Цифровой атлет»
Полная справка для самостоятельной работы учеников и учителя
1. О проекте
Проект «Цифровой атлет» — веб-приложение для отслеживания физической активности и показателей здоровья пользователя.
Основные компоненты
- Фронтенд — пользовательский интерфейс (HTML/CSS/JS)
- Бэкенд — логика работы приложения
- База данных — хранение данных пользователей
- Система расчётов — вычисление физиологических показателей
Обязательные модули
- Регистрация пользователя
- Профиль с личными данными
- Календарь тренировок
- Журнал показателей
- Система расчётов
- Отчётность
2. Технические требования
Необходимое ПО
- Онлайн-редактор кода (CodePen / Replit)
- Браузер с поддержкой современных стандартов
- ИИ-чат-бот для генерации кода
Базовые технологии
- HTML/CSS — создание интерфейса
- JavaScript — логика работы
- API — интеграция данных
3. Ключевые расчёты и формулы
Зоны пульса (от максимального)
Типы нагрузок
| Тип | Описание | Примеры |
|---|---|---|
| Силовые | Работа с отягощениями | Подъём штанги, жим, приседания |
| Кардио | Аэробные нагрузки | Бег, плавание, велосипед |
| Функциональные | Комплексные упражнения | Кроссфит, HIIT, круговая тренировка |
4. Ключи к заданиям — правильные ответы только для учителя
| Вопрос | Ответ |
|---|---|
| Какое нормальное артериальное давление? | 120/80 мм рт. ст. |
| Какой нормальный пульс в покое? | 60–90 уд/мин |
| Какой диапазон ИМТ считается нормой? | 18.5–24.9 |
| Как рассчитать максимальный пульс? | 220 − возраст (лет) |
| Какие зоны пульса существуют? | Разминка (50–60%), Аэробная (60–70%), Анаэробная (70–85%), Максимальная (85–95%) |
| Какие типы нагрузок бывают? | Силовые, кардионагрузки, функциональные |
| Как рассчитать ИМТ? | Вес (кг) / рост² (м) |
| Какие обязательные модули должны быть в проекте? | Регистрация, профиль, календарь тренировок, журнал показателей, система расчётов, отчётность |
5. Критерии оценки
Обязательные элементы
- Корректная работа всех форм
- Правильные расчёты показателей
- Функционирующий календарь
- Система сохранения данных
- Удобный интерфейс
6. Типичные ошибки
- Отсутствие валидации данных
- Ошибки в математических расчётах
- Проблемы с сохранением информации
- Неработающие элементы интерфейса
- Нарушение логики работы
7. Порядок проверки (чек-лист)
Функциональная проверка
- Тестирование всех форм
- Проверка расчётов (ИМТ, пульс, зоны)
- Работа с данными (сохранение/загрузка)
- Корректность календаря
Интерфейс
- Удобство использования
- Адаптивность (мобильные устройства)
- Эстетичный внешний вид
Документация
- Наличие инструкции (README)
- Описание функционала
- Руководство пользователя
🤖 Эталонные промпты для ИИ
Промпты проверены и дают стабильный результат. Ученики могут использовать их как образец для составления собственных.
📄 Промпт 1: HTML-каркас занятие 1
PROMPTСоздай HTML-разметку для электронного дневника тренировок. Используй семантические теги: - <form> с полями: дата, тип тренировки, длительность (мин), пульс до, пульс после, самочувствие (1-5) - <table> для истории тренировок (колонки: дата, тип, длительность, пульс, зона) - <div id="hr-zones"> для вывода расчёта пульсовых зон - <button> «Рассчитать» и «Добавить тренировку» Добавь комментарии к каждому блоку кода.
🎨 Промпт 2: CSS-стилизация занятие 2
PROMPTОформи HTML-дневник тренировок в спортивном стиле. Цветовая палитра: - Основной: #10b981 (зелёный, энергия) - Акцент: #3b82f6 (синий, доверие) - Фон: #f9fafb (светло-серый) - Перегрузка: #ef4444 (красный, предупреждение) Сделай: - Адаптивную вёрстку (mobile-first) - Кликабельные кнопки с hover-эффектом - Читаемый шрифт (system-ui, 16px) - Таблицу с чередующимися строками
⚙️ Промпт 3: JS-калькулятор ЧСС занятие 3
PROMPTНапиши JavaScript-функцию calcHR(), которая: 1. Рассчитывает максимальную ЧСС по формуле: maxHR = 220 - age 2. Возвращает объект с 5 пульсовыми зонами (в % от maxHR): - Зона 1 (восстановление): 50-60% - Зона 2 (жиросжигание): 60-70% - Зона 3 (аэробная): 70-80% - Зона 4 (анаэробная): 80-90% - Зона 5 (красная): 90-100% 3. Выводит результат в формате: «Зона X: от Y до Z уд/мин» Добавь обработку ошибок: если age < 10 или age > 100, вывести предупреждение.
🧠 Промпт 4: ИИ-тренер занятие 4
PROMPTТы — спортивный тренер с медицинским образованием. Составь индивидуальную разминку на 10 минут для пользователя: - Возраст: [возраст ученика] - Цель тренировки: [бег / силовая / йога] - Текущий пульс в покое: [значение] уд/мин - Уровень подготовки: [начинающий / средний / продвинутый] Включи: 5 упражнений с описанием, временем и целевым пульсом. Объясни, почему выбрал именно эти упражнения с точки зрения физиологии.
💻 Эталонный код JavaScript
Готовый пример для проверки работ учеников. Можно использовать как «золотой стандарт» при Peer Review.
Функция расчёта пульсовых зон calcHR()
function calcHR() {
const age = +document.getElementById('age').value;
const currentPulse = +document.getElementById('pulse').value;
if (age < 10 || age > 100) {
alert('Введите возраст от 10 до 100 лет');
return;
}
const maxHR = 220 - age;
const zones = [
{ name: 'Восстановление', min: 0.50, max: 0.60 },
{ name: 'Жиросжигание', min: 0.60, max: 0.70 },
{ name: 'Аэробная', min: 0.70, max: 0.80 },
{ name: 'Анаэробная', min: 0.80, max: 0.90 },
{ name: 'Красная зона', min: 0.90, max: 1.00 }
];
let result = `ЧССmax: ${maxHR} уд/мин\n\n`;
zones.forEach((zone, i) => {
const from = Math.round(maxHR * zone.min);
const to = Math.round(maxHR * zone.max);
result += `Зона ${i+1} (${zone.name}): ${from}–${to} уд/мин\n`;
});
const element = document.getElementById('result');
if (currentPulse > maxHR * 0.9) {
element.style.background = '#fecaca';
element.style.color = '#1f2937';
result += '\n⚠️ ПЕРЕГРУЗКА! Пульс выше 90% от ЧССmax';
} else {
element.style.background = '#d1fae5';
}
element.textContent = result;
}
🔧 Практический кейс: «Найди 4 ошибки» + Ключ
⚠️ Для занятия 4: скопируйте код ниже и раздайте ученикам. Задание: найти все 4 ошибки, исправить их и объяснить каждую.
📋 «Испорченный» код для учеников раздать ученикам
<!-- Найдите и исправьте 4 ошибки -->
<script>
function calcHR() {
const age = +document.getElementById('age').value;
const pulse = +document.getElementById('pulse').value;
const maxHR = 200 - age; // ошибка здесь?
let result = `ЧССmax: ${maxHR} уд/мин\n`;
if (pulse > maxHR * 0.8) {
document.getElementById('result').style.background = '#fecaca';
result += '\n⚠️ ПЕРЕГРУЗКА!';
}
document.getElementById('result').textContent = result;
}
</script>
<!-- Промпт: "Составь разминку для пользователя. Цель: бег." -->
🔑 Ключ для учителя список ошибок
| № | Ошибка в коде | Как должно быть | Проверяемая компетенция |
|---|---|---|---|
| 1 | В формуле ЧСС стоит 200 - age | 220 - age | Межпредметная интеграция |
| 2 | Условие подсветки: pulse > maxHR * 0.8 | pulse > maxHR * 0.9 | Алгоритмизация |
| 3 | В CSS не указан color для текста на красном фоне | Добавить color: #1f2937; | Качество продукта (UI/UX) |
| 4 | В промпте ИИ-тренеру не указан возраст пользователя | Добавить параметр Возраст: [X] лет | Prompt Engineering |
✅ Чек-лист Peer Review (взаимопроверка)
Протестируй сайт одноклассника и оцени по критериям: отмечено 0 из 9
📁 Структура проекта (для работы без интернета)
Полная файловая структура веб-приложения «Цифровой атлет — Твой персональный тренер» с исходными кодами всех модулей.
- Скопируйте приведённые файлы в соответствующую структуру папок.
- Откройте
index.htmlв браузере. - Заполните профиль — данные сохранятся в localStorage.
- Добавляйте тренировки, записывайте вес, генерируйте планы и смотрите прогресс.
Все модули независимы и легко расширяются. Вы можете заменить Tailwind на свои стили или добавить серверную часть для хранения данных.
Файловая структура
1. index.html главная страница
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FitCoach — Твой персональный тренер</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<header class="hero-header sticky top-0 z-50 shadow-lg">
<div class="max-w-6xl mx-auto px-4 py-5 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 relative z-10">
<div>
<h1 class="hero-title">
<span>🏋️</span>
<span>FitCoach</span>
</h1>
<p class="hero-subtitle">Твой путь к лучшей форме</p>
</div>
<nav class="flex flex-wrap gap-2">
<button onclick="App.switchTab('profile')" id="tab-profile" class="tab-btn tab-active px-4 py-2 rounded-xl text-sm">👤 Профиль</button>
<button onclick="App.switchTab('diary')" id="tab-diary" class="tab-btn tab-inactive px-4 py-2 rounded-xl text-sm">📓 Дневник</button>
<button onclick="App.switchTab('ai')" id="tab-ai" class="tab-btn tab-inactive px-4 py-2 rounded-xl text-sm">🤖 ИИ-тренер</button>
<button onclick="App.switchTab('progress')" id="tab-progress" class="tab-btn tab-inactive px-4 py-2 rounded-xl text-sm">📈 Прогресс</button>
</nav>
</div>
</header>
<main class="max-w-6xl mx-auto px-4 py-8">
<section id="section-profile" class="space-y-6"></section>
<section id="section-diary" class="space-y-6 hidden"></section>
<section id="section-ai" class="space-y-6 hidden"></section>
<section id="section-progress" class="space-y-6 hidden"></section>
</main>
<footer class="max-w-6xl mx-auto px-4 py-8 text-center text-sm text-gray-500">
<p>Твой персональный тренер · 2026</p>
</footer>
<script type="module" src="js/app.js"></script>
</body>
</html>
2. css/style.css основные стили + анимации
/* ====== Глобальные переменные ====== */
:root {
--primary: #10b981;
--primary-dark: #059669;
--accent: #3b82f6;
--accent-dark: #2563eb;
--bg: #f9fafb;
--danger: #ef4444;
--danger-dark: #dc2626;
}
* { box-sizing: border-box; }
body {
font-family: 'Inter', system-ui, sans-serif;
background-color: var(--bg);
background-image: radial-gradient(circle at 10% 20%, rgba(16, 185, 129, 0.05) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.05) 0%, transparent 40%);
min-height: 100vh;
color: #1f2937;
}
.font-display { font-family: 'Oswald', sans-serif; letter-spacing: 0.02em; }
/* ====== Шапка ====== */
.hero-header {
background: linear-gradient(135deg, #10b981 0%, #059669 50%, #3b82f6 100%);
position: relative;
overflow: hidden;
}
.hero-title {
font-family: 'Oswald', sans-serif;
font-weight: 700;
font-size: 36px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: white;
display: flex;
align-items: center;
gap: 12px;
}
/* ====== Кнопки табов, карточки, поля, таблицы, зоны ====== */
.tab-active { background: white; color: var(--primary); box-shadow: 0 4px 15px rgba(0,0,0,0.15); }
.tab-inactive { background: rgba(255,255,255,0.15); color: white; }
.sport-card { background: white; border-radius: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.06); }
.sport-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(16,185,129,0.1); }
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; }
.btn-accent { background: linear-gradient(135deg, var(--accent), var(--accent-dark)); color: white; }
.stat-green { background: linear-gradient(135deg, #10b981, #059669); }
.stat-blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
.zone-badge { display: inline-block; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; color: white; }
.hidden { display: none !important; }
3. js/utils.js вспомогательные функции
export function generateId() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID();
}
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
export function estimateCalories(duration, weight, hrBefore, hrAfter, type) {
const hrAvg = (hrBefore + hrAfter) / 2;
const typeFactors = {
'🏃 Кардио': 1.1,
'🏋️ Силовая': 0.9,
'🧘 Йога': 0.5,
'👟 Бег': 1.2,
'🚴 Велосипед': 1.0,
'🏊 Плавание': 1.15
};
const base = duration * (0.035 * weight + (0.029 * hrAvg * weight) / 200);
return Math.round(base * (typeFactors[type] || 1));
}
export function formatDate(dateStr) {
const d = new Date(dateStr);
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: 'short', year: 'numeric' });
}
export function sortByDateDesc(arr, dateKey = 'date') {
return [...arr].sort((a, b) => b[dateKey].localeCompare(a[dateKey]));
}
export function today() {
return new Date().toISOString().slice(0, 10);
}
4. js/storage.js localStorage
const PROFILE_KEY = 'fitcoach_profile';
const WORKOUTS_KEY = 'fitcoach_workouts';
const WEIGHT_LOG_KEY = 'fitcoach_weight_log';
const AI_PLAN_KEY = 'fitcoach_ai_plan';
export function loadProfile() {
try {
const raw = localStorage.getItem(PROFILE_KEY);
return raw ? JSON.parse(raw) : null;
} catch { return null; }
}
export function saveProfile(profile) {
localStorage.setItem(PROFILE_KEY, JSON.stringify(profile));
}
export function loadWorkouts() {
try {
const raw = localStorage.getItem(WORKOUTS_KEY);
return raw ? JSON.parse(raw) : [];
} catch { return []; }
}
export function saveWorkouts(workouts) {
localStorage.setItem(WORKOUTS_KEY, JSON.stringify(workouts));
}
export function loadWeightLog() {
try {
const raw = localStorage.getItem(WEIGHT_LOG_KEY);
return raw ? JSON.parse(raw) : [];
} catch { return []; }
}
export function saveWeightLog(log) {
localStorage.setItem(WEIGHT_LOG_KEY, JSON.stringify(log));
}
export function loadAIPlan() {
try {
const raw = localStorage.getItem(AI_PLAN_KEY);
return raw ? JSON.parse(raw) : null;
} catch { return null; }
}
export function saveAIPlan(planData) {
localStorage.setItem(AI_PLAN_KEY, JSON.stringify(planData));
}
export function clearAIPlan() {
localStorage.removeItem(AI_PLAN_KEY);
}
5. js/profile.js модуль профиля
import { loadProfile, saveProfile } from './storage.js';
const defaultProfile = {
name: '', surname: '', age: 0, height: 0,
weight: 0, goal: '', level: '', location: ''
};
let profile = loadProfile() || { ...defaultProfile };
export function getProfile() { return profile; }
export function updateProfile(newData) {
profile = { ...profile, ...newData };
saveProfile(profile);
return profile;
}
export function setProfileField(field, value) {
profile[field] = value;
saveProfile(profile);
}
export function getZones(age) {
const max = 220 - age;
return [
{ name: 'Зона 1', label: 'Восстановительная', min: max * 0.5, max: max * 0.6, color: '#10b981' },
{ name: 'Зона 2', label: 'Жиросжигающая', min: max * 0.6, max: max * 0.7, color: '#84cc16' },
{ name: 'Зона 3', label: 'Аэробная', min: max * 0.7, max: max * 0.8, color: '#eab308' },
{ name: 'Зона 4', label: 'Анаэробная', min: max * 0.8, max: max * 0.9, color: '#f97316' },
{ name: 'Зона 5', label: 'Максимальная', min: max * 0.9, max: max * 1.0, color: '#ef4444' }
];
}
export function getZoneForHR(hr, age) {
const zones = getZones(age);
return zones.find(z => hr >= z.min && hr < z.max) || null;
}
6. js/workouts.js дневник тренировок
import { loadWorkouts, saveWorkouts } from './storage.js';
import { getProfile } from './profile.js';
import { generateId, estimateCalories, sortByDateDesc } from './utils.js';
let workouts = loadWorkouts();
export function getWorkouts() { return workouts; }
export function saveWorkout(workout) {
const existingIndex = workouts.findIndex(w => w.id === workout.id);
if (existingIndex !== -1) {
workouts[existingIndex] = workout;
} else {
workout.id = workout.id || generateId();
workouts.unshift(workout);
}
saveWorkouts(workouts);
return workouts;
}
export function deleteWorkout(id) {
workouts = workouts.filter(w => w.id !== id);
saveWorkouts(workouts);
return workouts;
}
export function getStats() {
const total = workouts.length;
const minutes = workouts.reduce((s, w) => s + w.duration, 0);
const avgHR = total ? Math.round(workouts.reduce((s, w) => s + w.hrAfter, 0) / total) : 0;
const profile = getProfile();
const kcal = workouts.reduce((s, w) => s + estimateCalories(w.duration, profile.weight, w.hrBefore, w.hrAfter, w.type), 0);
return { total, minutes, avgHR, kcal };
}
7. js/weightLog.js журнал веса
import { loadWeightLog, saveWeightLog } from './storage.js';
import { getProfile, updateProfile } from './profile.js';
let weightLog = loadWeightLog();
export function getWeightLog() { return weightLog; }
export function saveWeightEntry(date, weight) {
const existing = weightLog.find(entry => entry.date === date);
if (existing) {
existing.weight = weight;
} else {
weightLog.push({ date, weight });
}
saveWeightLog(weightLog);
const latest = weightLog.reduce((a, b) => a.date > b.date ? a : b);
updateProfile({ weight: latest.weight });
document.getElementById('profile-weight').value = latest.weight;
return weightLog;
}
export function deleteWeightEntry(date) {
weightLog = weightLog.filter(w => w.date !== date);
saveWeightLog(weightLog);
return weightLog;
}
8. js/aiTrainer.js ИИ-тренер
import { getProfile, getZones } from './profile.js';
import { loadAIPlan, saveAIPlan, clearAIPlan } from './storage.js';
export function generatePlan() {
const profile = getProfile();
if (!profile.name || !profile.age) {
alert('Заполните имя и возраст в профиле.');
return;
}
const levelBase = { 'Новичок': 25, 'Средний': 40, 'Продвинутый': 60 };
const baseDuration = levelBase[profile.level] || 40;
const templates = {
'Похудение': {
'Новичок': ['Кардио', 'Отдых', 'Кардио', 'Отдых', 'Силовая', 'Кардио', 'Отдых'],
'Средний': ['Кардио', 'Силовая', 'Кардио', 'Отдых', 'Силовая', 'Бег', 'Отдых'],
'Продвинутый': ['Кардио', 'Силовая', 'Кардио', 'Силовая', 'Кардио', 'Бег', 'Отдых']
},
'Набор массы': {
'Новичок': ['Силовая', 'Отдых', 'Силовая', 'Отдых', 'Силовая', 'Отдых', 'Отдых'],
'Средний': ['Силовая', 'Силовая', 'Отдых', 'Силовая', 'Кардио', 'Силовая', 'Отдых'],
'Продвинутый': ['Силовая', 'Силовая', 'Силовая', 'Отдых', 'Силовая', 'Силовая', 'Отдых']
},
'Поддержание формы': {
'Новичок': ['Кардио', 'Отдых', 'Йога', 'Отдых', 'Кардио', 'Отдых', 'Отдых'],
'Средний': ['Кардио', 'Силовая', 'Йога', 'Кардио', 'Силовая', 'Бег', 'Отдых'],
'Продвинутый': ['Кардио', 'Силовая', 'Силовая', 'Кардио', 'Силовая', 'Бег', 'Йога']
}
};
const goalTpl = templates[profile.goal] || templates['Поддержание формы'];
const weekTypes = goalTpl[profile.level] || goalTpl['Средний'];
const days = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье'];
const plan = weekTypes.map((type, i) => {
const duration = type === 'Отдых' ? 30 : Math.max(20, Math.round(baseDuration + (Math.random() - 0.5) * 10));
return { day: days[i], type, duration };
});
saveAIPlan({
plan,
params: { goal: profile.goal, level: profile.level, location: profile.location },
generatedAt: new Date().toISOString()
});
}
export function clearPlan() {
if (confirm('Удалить сохранённый план?')) clearAIPlan();
}
9. js/chart.js график прогресса
import { getWorkouts } from './workouts.js';
import { getWeightLog } from './weightLog.js';
let currentChart = null;
export function renderChart() {
const container = document.getElementById('chart-container');
const workouts = getWorkouts();
const weightLog = getWeightLog();
const dateSet = new Set();
workouts.forEach(w => dateSet.add(w.date));
weightLog.forEach(w => dateSet.add(w.date));
if (dateSet.size === 0) {
container.innerHTML = '<div>Добавьте тренировки или записи веса</div>';
return;
}
const sortedDates = Array.from(dateSet).sort();
const weightData = sortedDates.map(date => {
const entries = weightLog.filter(w => w.date === date);
return entries.length ? entries[entries.length - 1].weight : null;
});
const hrData = sortedDates.map(date => {
const dayWorkouts = workouts.filter(w => w.date === date);
if (dayWorkouts.length === 0) return null;
return Math.round(dayWorkouts.reduce((s, w) => s + w.hrAfter, 0) / dayWorkouts.length);
});
if (currentChart) currentChart.destroy();
const canvas = document.createElement('canvas');
container.innerHTML = '';
container.appendChild(canvas);
currentChart = new Chart(canvas, {
type: 'line',
data: {
labels: sortedDates.map(d => d.slice(5)),
datasets: [
{ label: 'Вес (кг)', data: weightData, borderColor: '#14b8a6', yAxisID: 'y', fill: true, tension: 0.4 },
{ label: 'Пульс после (сред.)', data: hrData, borderColor: '#ef4444', yAxisID: 'y1', fill: true, tension: 0.4 }
]
},
options: {
responsive: true, maintainAspectRatio: false,
scales: {
y: { position: 'left', title: { display: true, text: 'Вес (кг)' } },
y1: { position: 'right', title: { display: true, text: 'Пульс' }, grid: { drawOnChartArea: false } }
}
}
});
}
10. js/app.js главный модуль
import { initProfileForm } from './profile.js';
import { initWorkoutForm, renderWorkoutsTable, updateStatsUI } from './workouts.js';
import { initWeightForm, renderWeightTable, updateWeightStatsUI } from './weightLog.js';
import { restoreSavedPlan, initAITrainer } from './aiTrainer.js';
import { renderChart } from './chart.js';
const App = {
currentTab: 'profile',
switchTab(tabName) {
document.querySelectorAll('main > section').forEach(s => s.classList.add('hidden'));
const section = document.getElementById(`section-${tabName}`);
if (section) section.classList.remove('hidden');
document.querySelectorAll('nav button').forEach(b => {
b.classList.remove('tab-active');
b.classList.add('tab-inactive');
});
const tabBtn = document.getElementById(`tab-${tabName}`);
if (tabBtn) {
tabBtn.classList.remove('tab-inactive');
tabBtn.classList.add('tab-active');
}
this.currentTab = tabName;
if (tabName === 'progress') setTimeout(renderChart, 100);
},
init() {
initProfileForm();
initWorkoutForm();
initWeightForm();
initAITrainer();
renderWorkoutsTable();
renderWeightTable();
updateStatsUI();
updateWeightStatsUI();
restoreSavedPlan();
this.switchTab('profile');
}
};
window.App = App;
document.addEventListener('DOMContentLoaded', () => App.init());