// Shared helpers: API access, DOM building, formatting import { accessToken, redirectToLogin, refresh } from './auth.js'; const API = '/api/v1'; /** What a training without a trainer is called */ export const SOLO = 'соло'; async function send(path, options, token) { const headers = { ...options.headers, Authorization: `Bearer ${token}` }; return fetch(API + path, { ...options, headers }); } /** JSON request to the API with the access token. Without a session the browser is sent to * the login page; an expired token is refreshed and the request repeated once. */ export async function api(path, options = {}) { const token = await accessToken(); if (!token) { return redirectToLogin(); } let response = await send(path, options, token); if (response.status === 401) { if (!(await refresh())) { return redirectToLogin(); } response = await send(path, options, await accessToken()); } 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}`; } /** "First Last" of a user, the email when the name is empty. Trainers in a list come with * a ready `name`. */ export function userName(user) { return user.name || `${user.first_name} ${user.last_name}`.trim() || user.email; } /** "2026-10-23T17:27:00+00:00" -> "23.10.2026 17:27" in the viewer's timezone */ export function formatDateTime(iso) { return new Date(iso).toLocaleString('ru-RU', { dateStyle: 'short', timeStyle: 'short' }).replace(',', ''); } /** 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); }