// Shared helpers: API access, DOM building, formatting const API = '/api/v1'; export async function api(path, options) { const response = await fetch(API + path, options); if (!response.ok) { const error = new Error(`HTTP ${response.status}`); error.status = response.status; throw error; } return response.json(); } /** h('div', {class: 'x'}, 'text', childNode) -> element; strings become text nodes */ export function h(tag, attrs = {}, ...children) { const element = document.createElement(tag); for (const [name, value] of Object.entries(attrs)) { if (value !== null && value !== undefined && value !== false) { element.setAttribute(name, value === true ? '' : value); } } element.append(...children.filter((child) => child !== null && child !== undefined)); return element; } export function alert(kind, ...children) { return h('div', { class: `alert alert-${kind}` }, ...children); } /** "2026-10-21" -> "21.10.2026" (no Date: a timezone must not shift the day) */ export function formatDate(iso) { const [year, month, day] = iso.split('-'); return `${day}.${month}.${year}`; } /** Rounded to one decimal without trailing zeros: 22.5 -> "22.5", 20 -> "20" */ export function num(value) { return String(Math.round(value * 10) / 10); } /** Same with an explicit sign: "+12", "-3.5", "+0" */ export function signed(value) { const rounded = Math.round(value * 10) / 10; return (rounded > 0 ? '+' : '') + rounded; } /** Approaches as "weight × reps" chips */ export function approachChips(approaches) { return approaches.map((approach) => h('span', { class: 'approach' }, `${approach.weight ? num(approach.weight) + ' кг' : 'без веса'} × ${approach.reps}`) ); } export function perArmBadge() { return h('span', { class: 'badge text-bg-light border', title: 'Вес указан на каждую руку (гантель)' }, 'на руку'); } export function tip(text, hint) { return h('span', { class: 'has-tip', title: hint }, text); }