massive frontend refactoring (works good)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { showModal, closeModal, stepperHtml } from './modal-base.js';
|
||||
|
||||
let _cb = null;
|
||||
|
||||
export function showCardioDialog(existing, callback) {
|
||||
_cb = callback;
|
||||
const e = existing || {};
|
||||
showModal(`<div class="modal-content">
|
||||
<h3 class="modal-title">${existing ? 'Редактировать' : 'Новое'} кардио</h3>
|
||||
<div class="field-row">
|
||||
<div class="field-group"><label>Тип</label><select id="m-ct">
|
||||
<option value="вело" ${e.type==='вело'?'selected':''}>🚴 Вело</option>
|
||||
<option value="ходьба" ${e.type==='ходьба'?'selected':''}>🚶 Ходьба</option>
|
||||
<option value="бег" ${e.type==='бег'?'selected':''}>🏃 Бег</option>
|
||||
<option value="эллипс" ${e.type==='эллипс'?'selected':''}>⚡ Эллипс</option>
|
||||
<option value="гребля" ${e.type==='гребля'?'selected':''}>🚣 Гребля</option>
|
||||
</select></div>
|
||||
<div class="field-group"><label>Назначение</label><select id="m-cp">
|
||||
<option value="warmup" ${e.purpose==='warmup'?'selected':''}>🔥 Разминка</option>
|
||||
<option value="cooldown" ${e.purpose==='cooldown'?'selected':''}>🧘 Заминка</option>
|
||||
<option value="separate" ${e.purpose==='separate'?'selected':''}>➕ Отдельная</option>
|
||||
</select></div></div>
|
||||
<div class="field-row-3">
|
||||
<div class="field-group"><label>Минут</label>${stepperHtml('m-cd', e.duration??'', 1, 'мин', 'numeric')}</div>
|
||||
<div class="field-group"><label>Дист.</label>${stepperHtml('m-cdi', e.distance??'', 0.1, 'км')}</div>
|
||||
<div class="field-group"><label>Скор.</label>${stepperHtml('m-cs', e.speed??'', 0.5, 'км/ч')}</div></div>
|
||||
<div class="field-row">
|
||||
<div class="field-group"><label>Наклон</label>${stepperHtml('m-ci', e.incline??'', 0.5, '—')}</div>
|
||||
<div class="field-group"><label>Сопр.</label>${stepperHtml('m-cr', e.resistance??'', 1, '—', 'numeric')}</div></div>
|
||||
<div class="field-group"><label>Заметка</label><input type="text" id="m-cn" value="${e.notes||''}"></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-secondary" onclick="ModalBase.closeOnBg({target:document.querySelector('.modal-overlay')})">Отмена</button>
|
||||
<button class="btn btn-success" onclick="CardioModal.submit()">Сохранить</button></div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
export function submit() {
|
||||
// 1. Сначала считываем все значения, пока модалка ещё в DOM
|
||||
const result = {
|
||||
type: document.getElementById('m-ct').value,
|
||||
purpose: document.getElementById('m-cp').value,
|
||||
duration: parseInt(document.getElementById('m-cd').value) || 0,
|
||||
distance: parseFloat(document.getElementById('m-cdi').value) || 0,
|
||||
speed: parseFloat(document.getElementById('m-cs').value) || 0,
|
||||
incline: parseFloat(document.getElementById('m-ci').value) || 0,
|
||||
resistance: parseInt(document.getElementById('m-cr').value) || 0,
|
||||
notes: document.getElementById('m-cn').value.trim(),
|
||||
};
|
||||
|
||||
// 2. Только теперь закрываем модалку
|
||||
closeModal();
|
||||
|
||||
// 3. Отдаём результат
|
||||
_cb?.(result);
|
||||
}
|
||||
|
||||
window.CardioModal = { submit };
|
||||
@@ -0,0 +1,42 @@
|
||||
import { showModal, closeModal } from './modal-base.js';
|
||||
|
||||
let _cb = null;
|
||||
|
||||
export function showEditExerciseDialog(exercise, callback) {
|
||||
_cb = callback;
|
||||
showModal(`
|
||||
<div class="modal-content">
|
||||
<h3 class="modal-title">Редактировать упражнение</h3>
|
||||
<div class="field-group">
|
||||
<label>Название</label>
|
||||
<input type="text" id="m-edit-ex-name" value="${exercise.name}">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>Тип тренировки</label>
|
||||
<select id="m-edit-ex-type">
|
||||
<option value="">тип не указан</option>
|
||||
<option value="грудь-руки" ${exercise.workoutType==='грудь-руки'?'selected':''}>грудь-руки</option>
|
||||
<option value="ноги" ${exercise.workoutType==='ноги'?'selected':''}>ноги</option>
|
||||
<option value="спина-плечи" ${exercise.workoutType==='спина-плечи'?'selected':''}>спина-плечи</option>
|
||||
<option value="круговая" ${exercise.workoutType==='круговая'?'selected':''}>круговая</option>
|
||||
<option value="силовая" ${exercise.workoutType==='силовая'?'selected':''}>силовая</option>
|
||||
<option value="многоповторная" ${exercise.workoutType==='многоповторная'?'selected':''}>многоповторная</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-secondary" onclick="ModalBase.close()">Отмена</button>
|
||||
<button class="btn btn-success" onclick="EditExModal.submit()">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
export function submit() {
|
||||
const name = document.getElementById('m-edit-ex-name').value.trim();
|
||||
const type = document.getElementById('m-edit-ex-type').value;
|
||||
if (!name) { alert('Название не может быть пустым'); return; }
|
||||
closeModal();
|
||||
if (_cb) _cb({ name, workoutType: type });
|
||||
}
|
||||
|
||||
window.EditExModal = { submit };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { showModal, closeModal } from './modal-base.js';
|
||||
import { toDatetimeLocal } from '../utils.js';
|
||||
|
||||
let _cb = null;
|
||||
|
||||
export function showEditWorkoutDateDialog(workout, callback) {
|
||||
_cb = callback;
|
||||
const startValue = toDatetimeLocal(workout.startTime);
|
||||
const endValue = workout.endTime ? toDatetimeLocal(workout.endTime) : '';
|
||||
|
||||
showModal(`
|
||||
<div class="modal-content">
|
||||
<h3 class="modal-title">📅 Дата тренировки</h3>
|
||||
<div class="field-group">
|
||||
<label>Начало</label>
|
||||
<input type="datetime-local" id="m-w-start" value="${startValue}">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>Окончание</label>
|
||||
<input type="datetime-local" id="m-w-end" value="${endValue}">
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-secondary" onclick="ModalBase.close()">Отмена</button>
|
||||
<button class="btn btn-success" onclick="EditDateModal.submit()">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
export function submit() {
|
||||
const startStr = document.getElementById('m-w-start').value;
|
||||
const endStr = document.getElementById('m-w-end').value;
|
||||
if (!startStr) { alert('Укажите дату начала'); return; }
|
||||
closeModal();
|
||||
if (_cb) _cb({
|
||||
startTime: new Date(startStr).toISOString(),
|
||||
endTime: endStr ? new Date(endStr).toISOString() : null,
|
||||
});
|
||||
}
|
||||
|
||||
window.EditDateModal = { submit };
|
||||
@@ -0,0 +1,60 @@
|
||||
import { showModal, closeModal } from './modal-base.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
let _cb = null;
|
||||
let _names = [];
|
||||
|
||||
export function showExerciseDialog(workoutType, callback) {
|
||||
_cb = callback;
|
||||
const nameCount = {};
|
||||
state.exerciseRef.forEach(ex => {
|
||||
const bonus = (workoutType && ex.workoutType === workoutType) ? 100 : 10;
|
||||
nameCount[ex.name] = (nameCount[ex.name] || 0) + bonus;
|
||||
});
|
||||
state.workouts.forEach(w => w.exercises.forEach(ex => {
|
||||
nameCount[ex.name] = (nameCount[ex.name] || 0) + 1;
|
||||
}));
|
||||
_names = Object.keys(nameCount).sort((a, b) => {
|
||||
const d = nameCount[b] - nameCount[a];
|
||||
return d !== 0 ? d : a.localeCompare(b);
|
||||
});
|
||||
|
||||
showModal(`<div class="modal-content">
|
||||
<h3 class="modal-title">Добавить упражнение</h3>
|
||||
<div class="field-group">
|
||||
<label>Название</label>
|
||||
<input type="text" id="m-ex-name" placeholder="Начните вводить..." oninput="ExerciseModal.filter()" autocomplete="off">
|
||||
<div id="m-ex-autocomplete" class="autocomplete-list"></div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-secondary" onclick="ModalBase.closeOnBg({target:document.querySelector('.modal-overlay')})">Отмена</button>
|
||||
<button class="btn" onclick="ExerciseModal.submit()">Добавить</button>
|
||||
</div>
|
||||
</div>`);
|
||||
setTimeout(() => { document.getElementById('m-ex-name').focus(); filter(); }, 50);
|
||||
}
|
||||
|
||||
export function filter() {
|
||||
const q = document.getElementById('m-ex-name').value.trim().toLowerCase();
|
||||
const list = document.getElementById('m-ex-autocomplete');
|
||||
let f = _names;
|
||||
if (q) f = f.filter(n => n.toLowerCase().includes(q));
|
||||
if (!f.length) { list.innerHTML = '<div class="autocomplete-item text-muted">Нет совпадений</div>'; return; }
|
||||
list.innerHTML = f.slice(0, 15).map(name => {
|
||||
const count = state.workouts.reduce((s, w) => s + w.exercises.filter(e => e.name === name).length, 0);
|
||||
return `<div class="autocomplete-item" onclick="ExerciseModal.select('${name.replace(/'/g, "\\'")}')">${name}${count ? `<span class="freq">${count}×</span>` : ''}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
export function select(name) {
|
||||
document.getElementById('m-ex-name').value = name;
|
||||
document.getElementById('m-ex-autocomplete').innerHTML = '';
|
||||
}
|
||||
|
||||
export function submit() {
|
||||
const name = document.getElementById('m-ex-name').value.trim();
|
||||
closeModal();
|
||||
if (name && _cb) _cb(name);
|
||||
}
|
||||
|
||||
window.ExerciseModal = { filter, select, submit };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { showModal, closeModal, stepperHtml } from './modal-base.js';
|
||||
import { toDatetimeLocal } from '../utils.js';
|
||||
|
||||
let _cb = null;
|
||||
|
||||
export function showBodyMetricsDialog(metrics, callback, editing = null) {
|
||||
_cb = callback;
|
||||
const src = editing || metrics[0] || {};
|
||||
const isEdit = !!editing;
|
||||
const dateVal = src.date ? toDatetimeLocal(src.date) : toDatetimeLocal(new Date().toISOString());
|
||||
|
||||
showModal(`<div class="modal-content">
|
||||
<h3 class="modal-title">📏 Замеры ${isEdit ? '(ред.)' : ''}</h3>
|
||||
<div class="field-group"><label>📅 Дата</label><input type="datetime-local" id="m-md" value="${dateVal}"></div>
|
||||
<div class="field-row">
|
||||
<div class="field-group"><label>⚖️ Вес</label>${stepperHtml('m-mw', src.weight??'', 0.1, 'кг')}</div>
|
||||
<div class="field-group"><label>📏 Грудь</label>${stepperHtml('m-mc', src.chest??'', 1, 'см', 'numeric')}</div></div>
|
||||
<div class="field-row">
|
||||
<div class="field-group"><label>📏 Талия</label>${stepperHtml('m-mwa', src.waist??'', 1, 'см', 'numeric')}</div>
|
||||
<div class="field-group"><label>📏 Бёдра</label>${stepperHtml('m-mh', src.hips??'', 1, 'см', 'numeric')}</div></div>
|
||||
<div class="field-row">
|
||||
<div class="field-group"><label>💪 Руки</label>${stepperHtml('m-ma', src.arms??'', 0.5, 'см')}</div>
|
||||
<div class="field-group"><label>🦵 Ноги</label>${stepperHtml('m-ml', src.legs??'', 0.5, 'см')}</div></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-secondary" onclick="ModalBase.closeOnBg({target:document.querySelector('.modal-overlay')})">Отмена</button>
|
||||
<button class="btn btn-success" onclick="MetricsModal.submit(${isEdit})">Сохранить</button></div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
export function submit(isEdit) {
|
||||
const r = {
|
||||
date: new Date(document.getElementById('m-md').value).toISOString(),
|
||||
weight: parseFloat(document.getElementById('m-mw').value)||null,
|
||||
chest: parseInt(document.getElementById('m-mc').value)||null,
|
||||
waist: parseInt(document.getElementById('m-mwa').value)||null,
|
||||
hips: parseInt(document.getElementById('m-mh').value)||null,
|
||||
arms: parseFloat(document.getElementById('m-ma').value)||null,
|
||||
legs: parseFloat(document.getElementById('m-ml').value)||null,
|
||||
};
|
||||
if (!Object.values(r).some((v,i) => i>0 && v!==null)) { alert('Заполните поле'); return; }
|
||||
closeModal(); _cb?.(r, isEdit);
|
||||
}
|
||||
|
||||
window.MetricsModal = { submit };
|
||||
@@ -0,0 +1,33 @@
|
||||
const container = () => document.getElementById('modal-container');
|
||||
|
||||
export function showModal(html) {
|
||||
container().innerHTML = `<div class="modal-overlay" onclick="ModalBase.closeOnBg(event)">${html}</div>`;
|
||||
}
|
||||
|
||||
export function closeModal() {
|
||||
container().innerHTML = '';
|
||||
}
|
||||
|
||||
export function closeOnBg(e) {
|
||||
if (e.target.classList.contains('modal-overlay')) closeModal();
|
||||
}
|
||||
|
||||
export function stepperHtml(id, value, step, placeholder, inputmode = 'decimal') {
|
||||
const v = value !== '' && value != null ? value : '';
|
||||
return `<div class="stepper-field">
|
||||
<button type="button" class="stepper-btn" onclick="ModalBase.stepValue('${id}', -${step})">−</button>
|
||||
<input type="number" id="${id}" step="${step}" value="${v}" placeholder="${placeholder}" inputmode="${inputmode}">
|
||||
<button type="button" class="stepper-btn" onclick="ModalBase.stepValue('${id}', ${step})">+</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export function stepValue(id, delta) {
|
||||
const input = document.getElementById(id);
|
||||
if (!input) return;
|
||||
const current = parseFloat(input.value) || 0;
|
||||
const step = parseFloat(input.step) || 1;
|
||||
const newVal = Math.max(0, current + delta);
|
||||
input.value = parseFloat((Math.round(newVal / step) * step).toFixed(4));
|
||||
}
|
||||
|
||||
window.ModalBase = { closeOnBg, closeModal, stepValue };
|
||||
@@ -0,0 +1,127 @@
|
||||
import { showModal, closeModal, stepperHtml } from './modal-base.js';
|
||||
import { formatShortDate, formatSet } from '../utils.js';
|
||||
|
||||
let _data = {};
|
||||
let _cb = null;
|
||||
|
||||
export function showSetDialog(existingSet, history, callback) {
|
||||
_cb = callback;
|
||||
const isEdit = !!existingSet;
|
||||
const isDropset = existingSet?.isDropset || false;
|
||||
|
||||
let historyHtml = '';
|
||||
if (history?.length) {
|
||||
const rows = history.map(h => `<div class="history-hint-row">
|
||||
<span class="history-hint-date">${formatShortDate(h.date)}</span>
|
||||
<span class="history-hint-sets">${h.sets.map(formatSet).join(', ')}</span>
|
||||
</div>`).join('');
|
||||
historyHtml = `<div class="history-hint"><div class="history-hint-title">📊 Прошлые тренировки</div>${rows}</div>`;
|
||||
}
|
||||
|
||||
showModal(`<div class="modal-content">
|
||||
<h3 class="modal-title">${isEdit ? 'Редактировать' : 'Новый'} подход</h3>
|
||||
${historyHtml}
|
||||
<div class="field-group"><label style="display:flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" id="m-set-dropset" ${isDropset ? 'checked' : ''} onchange="SetModal.toggleDropset()" style="width:auto;">
|
||||
Это дроп-сет</label></div>
|
||||
<div id="m-set-normal" class="${isDropset ? 'hidden' : ''}">
|
||||
<div class="field-row">
|
||||
<div class="field-group"><label>Вес (кг)</label>${stepperHtml('m-set-weight', existingSet?.weight ?? '', 0.5, 'Вес')}</div>
|
||||
<div class="field-group"><label>Повторения</label>${stepperHtml('m-set-reps', existingSet?.reps ?? '', 1, 'Повт.', 'numeric')}</div>
|
||||
</div></div>
|
||||
<div id="m-set-dropset-block" class="${isDropset ? '' : 'hidden'}">
|
||||
<div id="m-subsets-list"></div>
|
||||
<button class="btn btn-ghost mt-10" style="width:100%;" onclick="SetModal.addSubset()">+ Сбрасывание</button></div>
|
||||
<div class="field-group"><label>Оценка (1-5)</label>
|
||||
<div class="rating-grid" id="m-rating-grid">
|
||||
${[1,2,3,4,5].map(r => `<button class="rating-btn ${existingSet?.rating===r?'active':''}" data-rating="${r}" onclick="SetModal.rating(${r})">${r}</button>`).join('')}
|
||||
</div></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-secondary" onclick="ModalBase.closeOnBg({target:document.querySelector('.modal-overlay')})">Отмена</button>
|
||||
<button class="btn btn-success" onclick="SetModal.submit()">Сохранить</button></div>
|
||||
</div>`);
|
||||
|
||||
_data = {
|
||||
isDropset, rating: existingSet?.rating || 3,
|
||||
subSets: existingSet?.subSets ? JSON.parse(JSON.stringify(existingSet.subSets)) : [],
|
||||
};
|
||||
if (isDropset) { if (!_data.subSets.length) addSubset(); else renderSubsets(); }
|
||||
rating(_data.rating);
|
||||
}
|
||||
|
||||
export function toggleDropset() {
|
||||
_data.isDropset = document.getElementById('m-set-dropset').checked;
|
||||
document.getElementById('m-set-normal').classList.toggle('hidden', _data.isDropset);
|
||||
document.getElementById('m-set-dropset-block').classList.toggle('hidden', !_data.isDropset);
|
||||
if (_data.isDropset && !_data.subSets.length) addSubset();
|
||||
else if (_data.isDropset) renderSubsets();
|
||||
}
|
||||
|
||||
export function addSubset() {
|
||||
const last = _data.subSets[_data.subSets.length - 1];
|
||||
_data.subSets.push({ weight: last?.weight || '', reps: last?.reps || '' });
|
||||
renderSubsets();
|
||||
}
|
||||
|
||||
export function renderSubsets() {
|
||||
document.getElementById('m-subsets-list').innerHTML = _data.subSets.map((s, i) => `
|
||||
<div class="stepper-row">
|
||||
<div class="stepper-field">
|
||||
<button type="button" class="stepper-btn" onclick="SetModal.stepSubset(${i}, 'weight', -0.5)">−</button>
|
||||
<input type="number" id="m-sw-${i}" step="0.5" value="${s.weight || ''}" placeholder="Вес" inputmode="decimal" oninput="SetModal.updateSubset(${i}, 'weight', this.value)">
|
||||
<button type="button" class="stepper-btn" onclick="SetModal.stepSubset(${i}, 'weight', 0.5)">+</button>
|
||||
</div>
|
||||
<div class="stepper-field">
|
||||
<button type="button" class="stepper-btn" onclick="SetModal.stepSubset(${i}, 'reps', -1)">−</button>
|
||||
<input type="number" id="m-sr-${i}" step="1" value="${s.reps || ''}" placeholder="Повт." inputmode="numeric" oninput="SetModal.updateSubset(${i}, 'reps', this.value)">
|
||||
<button type="button" class="stepper-btn" onclick="SetModal.stepSubset(${i}, 'reps', 1)">+</button>
|
||||
</div>
|
||||
<button class="icon-btn danger" onclick="SetModal.removeSubset(${i})">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
export function updateSubset(i, field, value) {
|
||||
_data.subSets[i][field] = value;
|
||||
}
|
||||
|
||||
export function stepSubset(i, field, delta) {
|
||||
const current = parseFloat(_data.subSets[i][field]) || 0;
|
||||
const step = field === 'weight' ? 0.5 : 1;
|
||||
const newVal = Math.max(0, current + delta);
|
||||
_data.subSets[i][field] = parseFloat((Math.round(newVal / step) * step).toFixed(4));
|
||||
renderSubsets(); // Перерисовываем, чтобы обновить значение в input
|
||||
}
|
||||
|
||||
export function removeSubset(i) {
|
||||
_data.subSets.splice(i, 1);
|
||||
renderSubsets();
|
||||
}
|
||||
|
||||
export function rating(r) {
|
||||
_data.rating = r;
|
||||
document.querySelectorAll('#m-rating-grid .rating-btn').forEach(b => b.classList.toggle('active', +b.dataset.rating === r));
|
||||
}
|
||||
|
||||
export function submit() {
|
||||
if (_data.isDropset) {
|
||||
_data.subSets.forEach((s, i) => {
|
||||
const w = document.getElementById(`m-sw-${i}`);
|
||||
const r = document.getElementById(`m-sr-${i}`);
|
||||
if (w) s.weight = w.value; if (r) s.reps = r.value;
|
||||
});
|
||||
if (!_data.subSets.length) { alert('Добавьте сбрасывание'); return; }
|
||||
closeModal(); _cb?.({ isDropset: true, subSets: _data.subSets.map(s => ({ weight: parseFloat(s.weight)||0, reps: parseInt(s.reps)||0 })), rating: _data.rating });
|
||||
} else {
|
||||
const w = parseFloat(document.getElementById('m-set-weight').value);
|
||||
const r = parseInt(document.getElementById('m-set-reps').value);
|
||||
if (!w && !r) { alert('Укажите вес и повторения'); return; }
|
||||
closeModal(); _cb?.({ isDropset: false, weight: w||0, reps: r||0, rating: _data.rating });
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем экспорт в window
|
||||
window.SetModal = {
|
||||
toggleDropset, addSubset, removeSubset, rating, submit,
|
||||
updateSubset, stepSubset
|
||||
};
|
||||
Reference in New Issue
Block a user