495 lines
33 KiB
JavaScript
495 lines
33 KiB
JavaScript
/* 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();
|
||
})();
|