f1tness_parser/frontend/js/common.js

95 lines
3.3 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.

// 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);
}