f1tness_parser/frontend/js/edit-form.js

139 lines
7.2 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.

// Form for editing one training: date, trainee, trainer, exercises and their approaches
import { alert, api, h, SOLO, userName } from './common.js';
const jsonRequest = (method, body) => ({
method,
headers: { 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
function field(label, input, className = '') {
return h('div', { class: className }, h('label', { class: 'form-label small text-muted mb-1' }, label), input);
}
function approachRow(approach) {
const row = h('div', { class: 'row g-2 align-items-end approach-row mb-2' },
h('div', { class: 'col-5 col-md-3' },
h('input', { type: 'number', class: 'form-control form-control-sm', name: 'weight', min: 0, step: 'any', required: true, value: approach.weight, 'aria-label': 'Вес, кг' })),
h('div', { class: 'col-auto pb-1' }, '×'),
h('div', { class: 'col-4 col-md-2' },
h('input', { type: 'number', class: 'form-control form-control-sm', name: 'reps', min: 1, step: 1, required: true, value: approach.reps, 'aria-label': 'Повторения' })),
h('div', { class: 'col-auto' },
h('button', { type: 'button', class: 'btn btn-sm btn-outline-danger', title: 'Убрать подход' }, '×')));
row.querySelector('button').addEventListener('click', () => row.remove());
return row;
}
function exerciseCard(exercise) {
const approaches = h('div', {}, ...exercise.approaches.map(approachRow));
const addApproach = h('button', { type: 'button', class: 'btn btn-sm btn-outline-secondary' }, '+ подход');
addApproach.addEventListener('click', () => {
// a new approach starts as a copy of the last one: sets usually repeat
const last = approaches.lastElementChild;
approaches.append(approachRow({
weight: last ? last.querySelector('[name=weight]').value : 0,
reps: last ? last.querySelector('[name=reps]').value : 10,
}));
});
const remove = h('button', { type: 'button', class: 'btn btn-sm btn-outline-danger', title: 'Убрать упражнение' }, 'Убрать упражнение');
const card = h('div', { class: 'card exercise-card' },
h('div', { class: 'card-body' },
h('div', { class: 'row g-2 align-items-end mb-3' },
field('Упражнение', h('input', { type: 'text', class: 'form-control', name: 'name', list: 'edit-exercise-names', maxlength: 255, required: true, value: exercise.name }), 'col-12 col-md-6'),
h('div', { class: 'col-auto form-check ms-2 mb-2' },
h('input', { type: 'checkbox', class: 'form-check-input', name: 'splitted_weight', id: `per-arm-${Math.random().toString(36).slice(2)}`, checked: exercise.splitted_weight }),
h('label', { class: 'form-check-label' }, 'Вес на каждую руку')),
h('div', { class: 'col-auto ms-md-auto' }, remove)),
h('div', { class: 'small text-muted mb-1' }, 'Подходы: вес, кг (0 — без веса) × повторения'),
approaches,
addApproach));
// the checkbox label must point to its checkbox
const checkbox = card.querySelector('[name=splitted_weight]');
card.querySelector('.form-check-label').setAttribute('for', checkbox.id);
remove.addEventListener('click', () => card.remove());
return card;
}
function readForm(form) {
return {
date: form.elements.date.value,
trainee_id: Number(form.elements.trainee_id.value),
trainer_id: form.elements.trainer_id.value ? Number(form.elements.trainer_id.value) : null,
exercises: [...form.querySelectorAll('.exercise-card')].map((card) => ({
name: card.querySelector('[name=name]').value,
splitted_weight: card.querySelector('[name=splitted_weight]').checked,
approaches: [...card.querySelectorAll('.approach-row')].map((row) => ({
weight: Number(row.querySelector('[name=weight]').value),
reps: Number(row.querySelector('[name=reps]').value),
})),
})),
};
}
function userSelect(name, users, selectedId, emptyTitle) {
return h('select', { class: 'form-select', name, required: !emptyTitle },
emptyTitle ? h('option', { value: '', selected: selectedId === null }, emptyTitle) : null,
...users.map((user) => h('option', { value: user.id, selected: user.id === selectedId }, userName(user))));
}
/**
* trainees / trainers: users to choose from. onDone() is called after the training was saved
* or deleted, onCancel() on "Отмена".
*/
export function editForm(training, exerciseNames, { trainees, trainers }, { onDone, onCancel }) {
const exercisesBox = h('div', {}, ...training.exercises.map(exerciseCard));
const addExercise = h('button', { type: 'button', class: 'btn btn-outline-secondary mb-3' }, '+ упражнение');
addExercise.addEventListener('click', () => {
const card = exerciseCard({ name: '', splitted_weight: false, approaches: [{ weight: 0, reps: 10 }] });
exercisesBox.append(card);
card.querySelector('[name=name]').focus();
});
const status = h('div', {});
const save = h('button', { type: 'submit', class: 'btn btn-primary' }, 'Сохранить');
const cancel = h('button', { type: 'button', class: 'btn btn-outline-secondary' }, 'Отмена');
cancel.addEventListener('click', onCancel);
const remove = h('button', { type: 'button', class: 'btn btn-outline-danger ms-auto' }, 'Удалить тренировку');
const form = h('form', {},
h('datalist', { id: 'edit-exercise-names' }, ...exerciseNames.map((item) => h('option', { value: item.name }))),
h('div', { class: 'row g-3 mb-3' },
field('Дата', h('input', { type: 'date', class: 'form-control', name: 'date', required: true, value: training.date }), 'col-12 col-md-3'),
field('Тренирующийся', userSelect('trainee_id', trainees, training.trainee_id), 'col-12 col-md-3'),
field('Тренер', userSelect('trainer_id', trainers, training.trainer_id, `${SOLO} (без тренера)`), 'col-12 col-md-3')),
exercisesBox,
addExercise,
h('div', { class: 'd-flex gap-2 align-items-center' },
save,
cancel,
remove),
status);
form.addEventListener('submit', async (event) => {
event.preventDefault();
save.disabled = true;
try {
await api(`/trainings/${training.id}/`, jsonRequest('PUT', readForm(form)));
onDone();
} catch (error) {
status.replaceChildren(alert('danger', `Не удалось сохранить: ${error.message}`));
save.disabled = false;
}
});
remove.addEventListener('click', async () => {
if (!confirm('Удалить тренировку вместе с упражнениями и подходами?')) {
return;
}
try {
await api(`/trainings/${training.id}/`, jsonRequest('DELETE'));
onDone();
} catch (error) {
status.replaceChildren(alert('danger', `Не удалось удалить: ${error.message}`));
}
});
return form;
}