doctype-html-html-lang-ru-he/app.js

495 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* FORMA — client logic. All data stays in this browser (localStorage). */
(function () {
"use strict";
var $ = function (s) { return document.querySelector(s); };
var $$ = function (s) { return Array.prototype.slice.call(document.querySelectorAll(s)); };
// ---------- i18n ----------
var I18N = {
ru: {
menu: "МЕНЮ", nav_home: "Обзор", nav_train: "Тренировки", nav_health: "Здоровье", nav_coach: "ИИ помощник",
cTA: "Спросить ИИ", profile_name: "А", profile_name_lbl: "Мой профиль", profile_sub: "Бесплатный план",
hi: "Привет, Айгерим", sub: "Маленький шаг сегодня — сильная привычка завтра.", date: "25 сентября",
kpi_streak: "Серия", streak_unit: "дня", kpi_streak_d: "к прошлой неделе",
kpi_min: "Активность", min_unit: "мин", kpi_min_d: "за эту неделю",
kpi_water: "Вода", water_add: "+ добавить стакан",
kpi_weight: "Последний вес", kg: "кг", kpi_weight_d: "обновлено сегодня",
wk_eb: "Сегодня · 22 мин · 6 упражнений", wk_title: "Сильные ноги",
wk_desc: "Уверенный темп и забота о теле. Без оборудования, в комфортном ритме.",
wk_min: "22 мин", wk_ex: "6 упражнений", wk_lvl: "Средний", start: "Начать тренировку →",
week: "Эта неделя", week_count: "3 / 5 дней", week_goal: "Недельная цель", week_done: "1 / 3",
d_mo: "Пн", d_tu: "Вт", d_we: "Ср", d_th: "Чт", d_fr: "Пт", d_sa: "Сб", d_su: "Вс",
recent: "Последняя активность", all: "Все тренировки →",
a1: "Кардио без прыжков", a1s: "Вчера · 18 мин · Выполнено",
a2: "Растяжка всего тела", a2s: "Вторник · 12 мин · Выполнено",
cTA_t: "Твой цифровой напарник", cTA_p: "Подскажу, как начать, подскажу технику упражнений и подберу тренировку под твой ритм, настроение и свободное время.",
tr_t: "Тренировки", tr_s: "Программа на неделю — выбирай, что подходит сегодня.", tr_chip: "4 программы",
h_t: "Здоровье", h_s: "Записывай показатели и наблюдай за изменениями со временем.",
hm_t: "Вес и параметры тела", w_lbl: "Вес, кг", h_lbl: "Рост, см", waist_lbl: "Обхват талии, см (необязательно)",
local: "Данные — только в этом браузере.", hm_save: "Сохранить",
bmi_note: "ИМТ — лишь один из ориентиров и не учитывает состав тела, возраст и другие особенности. Обсудите личные показатели со специалистом.",
bmi_lbl: "ИМТ · ориентировочный показатель, не диагноз",
wt_t: "Изменение веса", wt_s: "Твои записи за последние недели.",
lb_t: "Анализы и самочувствие", metric: "Показатель", testo: "Тестостерон", ferritin: "Ферритин", vitd: "Витамин D", other: "Другой",
val: "Результат", unit: "Единицы измерения", labdate: "Дата анализа", lb_add: "Добавить результат",
lb_h: "Ваши записи", empty: "Пока нет записей — добавьте первый результат.",
lb_note: "Референсные значения зависят от лаборатории, метода, пола, возраста и времени сдачи. Мы не диагностируем гормональные нарушения. Сверь результат с бланком и обсуди его с врачом.",
ai_t: "ИИ помощник", ai_s: "Расскажи, что тебе нужно — подберём идею для следующего шага.", ai_online: "Онлайн",
ai_on: "Отвечает мгновенно", hi_msg: "Привет! Я помогу выбрать тренировку, подскажу технику и составлю план на неделю. Как ты себя сегодня чувствуешь?",
s1: "15-минутная тренировка", s2: "Войти в ритм", s3: "Лёгкая растяжка", ph: "Напиши сообщение…",
ai_disc: "ИИ может ошибаться. Ответы не заменяют консультацию врача или тренера. При боли остановись; при недомогании обратись к специалисту.",
ex_start: "Начать →", ex_prog: "Упражнение {n} из {total}", ex_title: "{name}", ex_pause: "Пауза", ex_resume: "Продолжить", ex_next: "Далее →",
toast_done: "Поздравляем! Вы отработали {name}.", toast_pause: "Пауза", toast_save: "Сохранено", toast_add: "Добавлено",
ex_done: "Упражнение выполнено", ex_end: "Завершить тренировку",
workouts: [
["Сила ног и ягодиц", "Начальный · 15 мин", "Мягко включаем мышцы нижней части тела.", ["Приседания у стула", "Ягодичный мост", "Подъёмы на носки"]],
["Всё тело за 15 минут", "Средний · 15 мин", "Короткий круг, когда день расписан по минутам.", ["Отжимания от стены", "Обратные выпады", "Планка на коленях"]],
["Кардио без прыжков", "Лёгкий · 12 мин", "Поддержим пульс, не нагружая суставы.", ["Шаги на месте", "Бокс без прыжков", "Шаг в сторону"]],
["Вечерняя растяжка", "Мобильность · 10 мин", "Расслабимся и мягко потянёмся перед сном.", ["Кошка-корова", "Поворот лёжа", "Растяжка бёдер"]]
]
},
kz: {
menu: "МӘЗІР", nav_home: "Шолу", nav_train: "Жаттығулар", nav_health: "Денсаулық", nav_coach: "AI көмекші",
cTA: "AI-дан сұрау", profile_name: "Ә", profile_name_lbl: "Менің профилім", profile_sub: "Тегін жоспар",
hi: "Сәлем, Әйгерім", sub: "Бүгінгі кішкентай қадам — ертеңгі жақсы әдет.", date: "25 қыркүйек",
kpi_streak: "Серия", streak_unit: "күн", kpi_streak_d: "өткен аптамен салыстырғанда",
kpi_min: "Белсенділік", min_unit: "мин", kpi_min_d: "осы аптада",
kpi_water: "Су", water_add: "+ бір стақан қосу",
kpi_weight: "Соңғы салмақ", kg: "кг", kpi_weight_d: "бүгін жаңартылды",
wk_eb: "Бүгін · 22 мин · 6 жаттығу", wk_title: "Мықты аяқтар",
wk_desc: "Бірқалыпты қарқын және өзіңе қамқорлық. Жабдықсыз, ыңғайлы ритмде.",
wk_min: "22 мин", wk_ex: "6 жаттығу", wk_lvl: "Орташа", start: "Бастау →",
week: "Осы апта", week_count: "3 / 5 күн", week_goal: "Апталық мақсат", week_done: "1 / 3",
d_mo: "Дс", d_tu: "Сс", d_we: "Ср", d_th: "Бс", d_fr: "Жм", d_sa: "Сб", d_su: "Жс",
recent: "Соңғы белсенділік", all: "Барлық жаттығулар →",
a1: "Секірусіз кардио", a1s: "Кеше · 18 мин · Аяқталды",
a2: "Бүкіл денені созу", a2s: "Сейсенбі · 12 мин · Аяқталды",
cTA_t: "Сенің цифрлық көмекшің", cTA_p: "Қалай бастарыңды айтып, техника көрсетіп, жаттығу ұсынамын — ритміңе, көңіл күйіңе және бос уақытыңа сай.",
tr_t: "Жаттығулар", tr_s: "Апталық бағдарлама — бүгін өзіңе сай келетінін таңда.", tr_chip: "4 бағдарлама",
h_t: "Денсаулық", h_s: "Көрсеткіштерді жазып, өзгерістерді бақыла.",
hm_t: "Салмақ және дене өлшемдері", w_lbl: "Салмақ, кг", h_lbl: "Бой, см", waist_lbl: "Бел айналымы, см (міндетті емес)",
local: "Деректер — тек осы браузерде.", hm_save: "Сақтау",
bmi_note: "ДСИ — тек бір бағдар, ол дене құрамын, жасты және басқа ерекшеліктерді ескермейді. Жеке көрсеткіштерді маманмен талқыла.",
bmi_lbl: "ДСИ · шамамен көрсеткіш, диагноз емес",
wt_t: "Салмақ өзгерісі", wt_s: "Соңғы аптадағы жазбаларың.",
lb_t: "Талдаулар мен көңіл күй", metric: "Көрсеткіш", testo: "Тестостерон", ferritin: "Ферритин", vitd: "D дәрумені", other: "Басқа",
val: "Нәтиже", unit: "Өлшем бірлігі", labdate: "Талдау күні", lb_add: "Нәтижені қосу",
lb_h: "Сенің жазбаларың", empty: "Алдымен жазба жоқ — бірінші нәтижені қос.",
lb_note: "Қалыпты аралық зертханаға, әдіске, жынысқа, жасқа және тапсыру уақытына байланысты. Біз гормондық бұзылыстарды анықтамаймыз. Нәтижені зертхана парағымен салыстырып, дәрігермен талқыла.",
ai_t: "AI көмекші", ai_s: "Не қажет екенін айт — келесі қадамға идея тауып беремін.", ai_online: "Онлайн",
ai_on: "Бірден жауап береді", hi_msg: "Сәлем! Жаттығу таңдауға, техника түсіндіруге немесе апталық жоспар құруға көмектесемін. Бүгін өзіңді қалай сезініп тұрсың?",
s1: "15-минуттық жаттығу", s2: "Ритмге кіру", s3: "Жеңіл созылу", ph: "Хабарлама жаз…",
ai_disc: "AI қателесуі мүмкін. Жауаптар дәрігердің немесе білікті жаттықтырушының кеңесін алмастырмайды. Ауырса тоқта; жайсыздық сезінсең маманға жүгін.",
ex_start: "Бастау →", ex_prog: "Жаттығу {n} / {total}", ex_title: "{name}", ex_pause: "Үзіліс", ex_resume: "Жалғастыру", ex_next: "Келесі →",
toast_done: "Құттықтаймыз! {name} орындалды.", toast_pause: "Үзіліс", toast_save: "Сақталды", toast_add: "Қосылды",
ex_done: "Жаттығу аяқталды", ex_end: "Жаттығуды аяқтау",
workouts: [
["Аяқ пен бөксе", "Бастаушы · 15 мин", "Төменгі дене бұлшықеттерін біртіндеп белсендіреміз.", ["Орындыққа отырып-тұру", "Жамбас көтеру", "Башпайға көтерілу"]],
["15 минутта бүкіл дене", "Орташа · 15 мин", "Күн қарбалас кездегі қысқа жаттығу.", ["Қабырғадан итерілу", "Кері адым", "Тіземен планка"]],
["Секірусіз кардио", "Жеңіл · 12 мин", "Жүрек соғуын жиілетеміз, буынға жүктеме қоймай.", ["Орнында жүру", "Секірмей бокс", "Жанға қадам"]],
["Кешкі созылу", "Икемділік · 10 мин", "Ұйқы алдында босаңсып, денеңді соз.", ["Мысық-сиыр", "Жатып бұрылу", "Жамбас созу"]]
]
},
en: {
menu: "MENU", nav_home: "Overview", nav_train: "Workouts", nav_health: "Health", nav_coach: "AI coach",
cTA: "Ask the AI", profile_name: "A", profile_name_lbl: "My profile", profile_sub: "Free plan",
hi: "Hi, Aigerim", sub: "A small step today builds a strong habit tomorrow.", date: "Sep 25",
kpi_streak: "Streak", streak_unit: "days", kpi_streak_d: "from last week",
kpi_min: "Activity", min_unit: "min", kpi_min_d: "this week",
kpi_water: "Water", water_add: "+ add a glass",
kpi_weight: "Latest weight", kg: "kg", kpi_weight_d: "updated today",
wk_eb: "Today · 22 min · 6 exercises", wk_title: "Strong legs",
wk_desc: "Steady pace and care for your body. No equipment, at a comfortable rhythm.",
wk_min: "22 min", wk_ex: "6 exercises", wk_lvl: "Medium", start: "Start workout →",
week: "This week", week_count: "3 / 5 days", week_goal: "Weekly goal", week_done: "1 / 3",
d_mo: "Mo", d_tu: "Tu", d_we: "We", d_th: "Th", d_fr: "Fr", d_sa: "Sa", d_su: "Su",
recent: "Recent activity", all: "All workouts →",
a1: "Low-impact cardio", a1s: "Yesterday · 18 min · Complete",
a2: "Full-body stretch", a2s: "Tuesday · 12 min · Complete",
cTA_t: "Your digital partner", cTA_p: "I'll help you start, show exercise technique and suggest a workout for your pace, mood and schedule.",
tr_t: "Workouts", tr_s: "A weekly program — pick what suits you today.", tr_chip: "4 programs",
h_t: "Health", h_s: "Log your measurements and watch how they change over time.",
hm_t: "Weight and body measurements", w_lbl: "Weight, kg", h_lbl: "Height, cm", waist_lbl: "Waist, cm (optional)",
local: "Your data stays in this browser only.", hm_save: "Save",
bmi_note: "BMI is one rough screening measure and does not account for body composition, age or individual factors. Discuss personal measurements with a health professional.",
bmi_lbl: "BMI · an estimate, not a diagnosis",
wt_t: "Weight trend", wt_s: "Your entries from recent weeks.",
lb_t: "Lab results and wellbeing", metric: "Marker", testo: "Testosterone", ferritin: "Ferritin", vitd: "Vitamin D", other: "Other",
val: "Result", unit: "Units", labdate: "Test date", lb_add: "Add result",
lb_h: "Your records", empty: "No records yet — add your first result.",
lb_note: "Reference ranges vary by lab, method, sex, age and time of day. We do not diagnose hormone conditions. Compare your result with the lab report and discuss with a clinician.",
ai_t: "AI coach", ai_s: "Tell me what you need — I'll find an idea for your next step.", ai_online: "Online",
ai_on: "Replies instantly", hi_msg: "Hi! I can help pick a workout, show the technique and build a weekly plan. How are you feeling today?",
s1: "15-minute workout", s2: "Build a routine", s3: "Gentle stretching", ph: "Write a message…",
ai_disc: "AI can make mistakes. Answers don't replace a clinician's or qualified trainer's advice. Stop if you feel pain; speak to a professional if you feel unwell.",
ex_start: "Start →", ex_prog: "Exercise {n} of {total}", ex_title: "{name}", ex_pause: "Pause", ex_resume: "Resume", ex_next: "Next →",
toast_done: "Nice work! You completed {name}.", toast_pause: "Paused", toast_save: "Saved", toast_add: "Added",
ex_done: "Exercise complete", ex_end: "Finish workout",
workouts: [
["Legs & glutes", "Beginner · 15 min", "Ease your lower-body muscles into movement.", ["Chair squats", "Glute bridge", "Calf raises"]],
["Full body in 15", "Medium · 15 min", "A short circuit for your busiest days.", ["Wall push-ups", "Reverse lunges", "Knee plank"]],
["Low-impact cardio", "Easy · 12 min", "Keep the pulse up without loading the joints.", ["March in place", "No-jump boxing", "Side steps"]],
["Evening stretch", "Mobility · 10 min", "Unwind and stretch gently before bed.", ["Cat-cow", "Supine twist", "Hip stretch"]]
]
}
};
var EX_NAMES = {
ru: ["Приседания у стула", "Ягодичный мост", "Подъёмы на носки", "Отжимания от стены", "Обратные выпады", "Планка на коленях", "Шаги на месте", "Бокс без прыжков", "Шаг в сторону", "Кошка-корова", "Поворот лёжа", "Растяжка бёдер"],
kz: ["Орындыққа отырып-тұру", "Жамбас көтеру", "Башпайға көтерілу", "Қабырғадан итерілу", "Кері адым", "Тіземен планка", "Орнында жүру", "Секірмей бокс", "Жанға қадам", "Мысық-сиыр", "Жатып бұрылу", "Жамбас созу"],
en: ["Chair squats", "Glute bridge", "Calf raises", "Wall push-ups", "Reverse lunges", "Knee plank", "March in place", "No-jump boxing", "Side steps", "Cat-cow", "Supine twist", "Hip stretch"]
};
var CHAT = {
ru: {
plan: "Отличный выбор — начнём с короткой тренировки. Попробуй круг: 30 сек приседаний у стула, 30 сек шагов на месте, 30 сек отжиманий от стены, 30 сек ягодичного моста. Сделай 2–3 круга в комфортном темпе.",
stretch: "Мягкая растяжка без боли: кошка-корова, наклоны таза лёжа и спокойное дыхание. Держи 20–30 секунд. Остановись, если почувствуешь боль.",
routine: "Для начала — 3 короткие тренировки в неделю с днями отдыха между ними. Чередуй лёгкое кардио, упражнения на всё тело и растяжку. Начни с 10–15 минут и добавляй время постепенно.",
default: "Хорошая идея — начать в комфортном темпе. Могу предложить короткую тренировку без оборудования, план на неделю или упражнения на растяжку. Что тебе ближе?"
},
kz: {
plan: "Жақсы таңдау — қысқа жаттығудан бастайық. Шеңбер жаса: 30 сек орындыққа отырып-тұру, 30 сек орнында жүру, 30 сек қабырғадан итерілу, 30 сек жамбас көтеру. Өзіңе ыңғайлы темпте 2–3 айналым.",
stretch: "Ауырсынусыз жеңіл созылу: мысық-сиыр, жатып жамбас қозғау және баяу тыныс алу. 20–30 секунд ұста. Ауырса тоқта.",
routine: "Бастау үшін аптасына 3 қысқа жаттығу және арасына демалыс күндерін қос. Жеңіл кардио, бүкіл дене және созылуды кезектестір. 10–15 минуттан баста.",
default: "Жақсы идея — ыңғайлы темптен бастау. Жабдықсыз жаттығу, апталық жоспар немесе созылуды ұсынамын. Қайсысы керек?"
},
en: {
plan: "Great — let's start with a short circuit: 30s chair squats, 30s marching in place, 30s wall push-ups, 30s glute bridges. Repeat for 2–3 rounds at a comfortable pace.",
stretch: "Gentle, pain-free mobility: cat-cow, lying pelvic tilts and slow breathing. Hold 20–30s each. Stop if you feel pain.",
routine: "For a start: three short workouts a week with rest days between. Alternate light cardio, full-body strength and stretching. Begin with 10–15 minutes and build up gradually.",
default: "A good place to start is a comfortable pace. I can suggest an equipment-free workout, a weekly plan or some mobility exercises. Which sounds useful?"
}
};
// ---------- state ----------
var lang = localStorage.getItem("formaLang") || "ru";
var t = function (key, data) {
var s = (I18N[lang] && I18N[lang][key]) || I18N.ru[key] || key;
if (data) Object.keys(data).forEach(function (k) { s = s.replace("{" + k + "}", data[k]); });
return s;
};
// ---------- toast ----------
var toastStack = $("#toasts");
function toast(msg, tone) {
if (!toastStack) return;
var el = document.createElement("div");
el.className = "kt-ai-toast";
if (tone) el.setAttribute("data-tone", tone);
el.textContent = msg;
toastStack.appendChild(el);
setTimeout(function () { el.remove(); }, 2600);
}
// ---------- navigation ----------
var current = "home";
function go(page) {
if (!page) return;
current = page;
$$(".kt-ai-page").forEach(function (p) { p.style.display = (p.getAttribute("data-page") === page) ? "block" : "none"; });
$$(".kt-ai-nav-item").forEach(function (b) { b.setAttribute("data-active", b.getAttribute("data-page") === page ? "true" : "false"); });
var crumb = $("#crumb");
if (crumb) crumb.textContent = navLabel(page);
if (page === "home") renderHomeExercise();
if (page === "health") { calcBmi(); renderLabs(); drawChart(); }
window.scrollTo(0, 0);
}
function navLabel(page) {
return t("nav_" + page);
}
$$("[data-page]").forEach(function (el) {
el.addEventListener("click", function (e) {
var p = el.getAttribute("data-page");
if (el.classList.contains("kt-ai-nav-item") || el.classList.contains("kt-ai-link") || el.tagName === "BUTTON") go(p);
});
});
// ---------- language ----------
var langSel = $("#lang");
if (langSel) langSel.addEventListener("change", function (e) {
lang = e.target.value;
localStorage.setItem("formaLang", lang);
applyLanguage();
});
function applyLanguage() {
document.documentElement.lang = (lang === "kz") ? "kk" : lang;
if (langSel) langSel.value = lang;
$$("[data-i]").forEach(function (el) {
var key = el.getAttribute("data-i");
var val = I18N[lang][key];
if (val != null) el.innerHTML = val;
});
var ph = $("#chatText"); if (ph) ph.placeholder = t("ph");
renderTrainGrid();
renderHomeExercise();
renderLabs();
drawChart();
var crumb = $("#crumb"); if (crumb) crumb.textContent = navLabel(current);
}
// ---------- home today-exercise card ----------
function renderHomeExercise() {
var w = I18N[lang].workouts[0];
var card = $("#homeExCard"); if (!card) return;
$("#homeExBadgeTx").textContent = w[1];
$("#homeExTitle").textContent = w[0];
$("#homeExDesc").textContent = w[2];
var list = $("#homeExList");
list.innerHTML = "";
w[3].forEach(function (name, i) {
var d = document.createElement("div");
d.className = "form-ex";
d.innerHTML = "<div class=\"form-ex-row\"><span>" + (i + 1) + ". " + name + "</span><span>× " + (i === 2 ? 12 : 10) + "</span></div>";
list.appendChild(d);
});
}
// ---------- train grid ----------
function renderTrainGrid() {
var grid = $("#trainGrid"); if (!grid) return;
grid.innerHTML = "";
I18N[lang].workouts.forEach(function (w, i) {
var card = document.createElement("article");
card.className = "kt-ai-card form-exercise-card";
card.style.cursor = "pointer";
var lines = w[3].map(function (n, j) {
return '<div class="form-ex"><div class="form-ex-row"><span>' + (j + 1) + '. ' + n + "</span><span>× " + (j === 2 ? 12 : 10) + "</span></div></div>";
}).join("");
card.innerHTML =
'<span class="form-badge">' + w[1] + "</span>" +
'<h2>' + w[0] + "</h2>" +
"<p>" + w[2] + "</p>" +
"<div>" + lines + "</div>" +
'<button class="kt-ai-btn" data-variant="ghost" data-ex="' + i + '">' + t("ex_start") + "</button>";
grid.appendChild(card);
card.addEventListener("click", function (e) {
if (e.target.closest("[data-ex]")) openExercise(i, 0);
});
});
}
// ---------- exercise overlay (timer) ----------
var exTimer = null, exRemaining = 40, exIndex = 0, exWorkout = 0, exRunning = false;
var exNames = function () { return EX_NAMES[lang] || EX_NAMES.ru; };
function openExercise(workout, startIdx) {
exWorkout = workout;
exIndex = startIdx || 0;
exRemaining = 40;
exRunning = true;
var overlay = $("#exOverlay");
if (overlay) overlay.hidden = false;
renderExercise();
startTick();
}
function closeExercise() {
exRunning = false;
if (exTimer) clearInterval(exTimer);
var overlay = $("#exOverlay");
if (overlay) overlay.hidden = true;
}
function renderExercise() {
var w = I18N[lang].workouts[exWorkout];
var names = w[3];
var name = names[exIndex % names.length];
var total = names.length;
$("#exProgress").textContent = t("ex_prog", { n: exIndex + 1, total: total });
$("#exTitle2").textContent = name;
updateCount();
$("#exPause").textContent = t(exRunning ? "ex_pause" : "ex_resume");
}
function updateCount() {
$("#exCount").textContent = "00:" + String(exRemaining).padStart(2, "0");
}
function startTick() {
if (exTimer) clearInterval(exTimer);
exRunning = true;
exTimer = setInterval(function () {
if (!exRunning) return;
exRemaining--;
if (exRemaining <= 0) {
exRemaining = 40;
exIndex++;
toast(t("ex_done"), "ok");
renderExercise();
}
updateCount();
}, 1000);
}
if (typeof document !== "undefined") {
(function () {
// wire exercise overlay + start buttons after DOM (script at end of body)
document.addEventListener("click", function (e) {
var b = e.target.closest("[data-ex]");
if (b) {
var v = b.getAttribute("data-ex");
var idx = (v === "today") ? 0 : (parseInt(v, 10) || 0);
openExercise(idx, 0);
return;
}
if (e.target.closest("#exPause")) {
exRunning = !exRunning;
if (exRunning) { renderExercise(); startTick(); }
else { if (exTimer) clearInterval(exTimer); $("#exPause").textContent = t("ex_resume"); }
return;
}
if (e.target.closest("#exNext")) { exIndex++; exRemaining = 40; renderExercise(); startTick(); return; }
if (e.target.closest("#exClose")) { closeExercise(); toast(t("toast_done", { name: I18N[lang].workouts[exWorkout][0] }), "ok"); return; }
if (e.target.closest("#waterAdd")) { addWater(); return; }
});
// chat form
var chatForm = $("#chatForm");
if (chatForm) chatForm.addEventListener("submit", function (e) { e.preventDefault(); ask(); });
$$(".form-chat-suggest button").forEach(function (b) {
b.addEventListener("click", function () {
var text = b.getAttribute("data-prompt") || "";
sendChat(text, false);
});
});
// body form
var bodyForm = $("#bodyForm");
if (bodyForm) bodyForm.addEventListener("submit", function (e) { e.preventDefault(); saveBody(); });
// lab form
var labForm = $("#labForm");
if (labForm) labForm.addEventListener("submit", function (e) { e.preventDefault(); saveLab(); });
// weight/height live BMI
["weight", "height"].forEach(function (id) { var el = document.getElementById(id); if (el) el.addEventListener("input", calcBmi); });
})();
}
// ---------- chat ----------
var chatMsgs = $("#chatMessages");
function addMsg(text, isMe) {
var d = document.createElement("div");
d.className = "form-msg " + (isMe ? "form-msg--me" : "form-msg--bot");
d.textContent = text;
chatMsgs.appendChild(d);
chatMsgs.scrollTop = chatMsgs.scrollHeight;
return d;
}
function pickReply(lower) {
var pool = CHAT[lang] || CHAT.ru;
if (/15|коротк|short|15-мин|қысқа/.test(lower)) return pool.plan;
if (/растяж|созыл|stretch|растя/.test(lower)) return pool.stretch;
if (/ритм|жоспар|план|plan|routine/.test(lower)) return pool.routine;
return pool.default;
}
function ask() {
var input = $("#chatText");
var val = input.value;
if (!val.trim()) return;
sendChat(val, true);
input.value = "";
}
function sendChat(text, scrollInput) {
if (!text) return;
addMsg(text, true);
setTimeout(function () {
addMsg(pickReply(text.toLowerCase()), false);
}, 350);
}
// ---------- water / body / labs ----------
function loadBody() {
try {
var b = JSON.parse(localStorage.getItem("formaBody") || "null");
if (b) {
$("#weight").value = b.weight;
$("#height").value = b.height;
$("#waist").value = b.waist || "";
$("#weightStat").textContent = b.weight;
}
} catch (e) {}
var wv = localStorage.getItem("formaWater");
if (wv != null) $("#waterVal").textContent = wv;
}
function addWater() {
var cur = parseInt($("#waterVal").textContent, 10) || 0;
cur = Math.min(8, cur + 1);
$("#waterVal").textContent = cur;
localStorage.setItem("formaWater", cur);
toast(t("water_add").replace("+ ", "") + " · " + cur + "/8", "info");
}
function saveBody() {
var body = {
weight: parseFloat($("#weight").value) || 0,
height: parseFloat($("#height").value) || 0,
waist: $("#waist").value
};
localStorage.setItem("formaBody", JSON.stringify(body));
var hist = JSON.parse(localStorage.getItem("formaWeights") || "[]");
hist.push({ w: body.weight, d: new Date().toLocaleDateString(lang === "en" ? "en" : "ru", { day: "2-digit", month: "short" }) });
localStorage.setItem("formaWeights", JSON.stringify(hist.slice(-7)));
$("#weightStat").textContent = body.weight;
calcBmi();
drawChart();
toast(t("toast_save"), "ok");
}
function calcBmi() {
var h = (parseFloat($("#height").value) || 0) / 100;
var w = parseFloat($("#weight").value) || 0;
var box = $("#bmiBox");
if (h && w) {
$("#bmiVal").textContent = (w / (h * h)).toFixed(1);
box.hidden = false;
} else {
box.hidden = true;
}
}
function drawChart() {
var host = $("#weightChart"); if (!host) return;
var data = JSON.parse(localStorage.getItem("formaWeights") || "[]");
if (data.length < 2) {
var val = parseFloat($("#weight").value) || 62.4;
data = [
{ w: val + 0.8, d: sep(8) }, { w: val + 0.5, d: sep(12) }, { w: val + 0.6, d: sep(16) },
{ w: val + 0.1, d: sep(20) }, { w: val, d: (lang === "en" ? "Today" : lang === "kz" ? "Бүгін" : "Сегодня") }
];
}
var vals = data.slice(-7).map(function (x) { return x.w; });
var lo = Math.min.apply(null, vals) - 0.5, hi = Math.max.apply(null, vals) + 0.5;
host.innerHTML = "";
data.slice(-7).forEach(function (x, i) {
var isLast = i === data.slice(-7).length - 1;
var pct = ((x.w - lo) / (hi - lo)) * 100;
host.innerHTML += '<div class="form-chart-col"><span>' + x.w.toFixed(1) + '</span><div class="form-chart-bar' + (isLast ? " is-last" : "") + '" style="height:' + Math.max(20, pct) + '%"></div><span>' + x.d + "</span></div>";
});
}
function sep(day) {
var m = { ru: "сент", kz: "қырк", en: "Sep" }[lang];
return m ? (m + " " + day) : day;
}
function renderLabs() {
var hist = JSON.parse(localStorage.getItem("formaLabs") || "[]");
var list = $("#labHistory"), empty = $("#labEmpty");
list.innerHTML = "";
if (!hist.length) { empty.style.display = "block"; return; }
empty.style.display = "none";
hist.forEach(function (x) {
var li = document.createElement("li");
li.innerHTML = '<span class="form-avatar"><svg class="kt-icon" aria-hidden="true"><use href="#chart"></use></svg></span>' +
"<div><strong>" + x.name + "</strong><small>" + x.date + "</small></div>" +
'<span class="form-list-xp">' + x.value + (x.unit ? " " + x.unit : "") + "</span>";
list.appendChild(li);
});
}
function saveLab() {
var value = $("#labValue").value;
if (!value) { $("#labValue").focus(); return; }
var name = $("#labName").selectedOptions[0].textContent || "—";
var labs = JSON.parse(localStorage.getItem("formaLabs") || "[]");
labs.unshift({ name: name, value: value, unit: $("#labUnit").value, date: $("#labDate").value || new Date().toISOString().slice(0, 10) });
localStorage.setItem("formaLabs", JSON.stringify(labs.slice(0, 5)));
$("#labValue").value = ""; $("#labUnit").value = ""; $("#labDate").value = "";
renderLabs();
toast(t("toast_add"), "ok");
}
// ---------- boot ----------
loadBody();
applyLanguage();
})();