massive frontend refactoring (works good)

This commit is contained in:
2026-09-30 01:54:17 +03:00
parent 9203884f53
commit 93ca76c6e1
36 changed files with 1608 additions and 2069 deletions
+30
View File
@@ -0,0 +1,30 @@
export function renderCardioItem(workoutId, c, isReadOnly = false) {
const card = document.createElement('div');
card.className = 'card';
const purposeLabels = { warmup: '🔥 Разминка', cooldown: '🧘 Заминка', separate: '➕ Отдельная' };
const metrics = [];
if (c.duration) metrics.push(`${c.duration} мин`);
if (c.distance) metrics.push(`${c.distance} км`);
if (c.speed) metrics.push(`${c.speed} км/ч`);
if (c.incline) metrics.push(`наклон ${c.incline}`);
if (c.resistance) metrics.push(`сопр. ${c.resistance}`);
const actionsHtml = isReadOnly ? '' : `
<div class="set-actions">
<button class="icon-btn" onclick="CL.editCardio('${workoutId}', '${c.id}')">✎</button>
<button class="icon-btn danger" onclick="CL.deleteCardio('${workoutId}', '${c.id}')">✕</button>
</div>
`;
card.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div>
<div style="font-weight:600;">${c.type || '—'} ${purposeLabels[c.purpose] || ''}</div>
<div class="text-small text-muted">${metrics.join(' • ') || '—'}</div>
${c.notes ? `<div class="text-small" style="margin-top:4px;">"${c.notes}"</div>` : ''}
</div>
${actionsHtml}
</div>
`;
return card;
}
+60
View File
@@ -0,0 +1,60 @@
import { state } from '../state.js';
import { renderSetRow } from './set-row.js';
export function renderExercise(w, ex, isReadOnly = false) {
const isExpanded = state.expandedExerciseId === ex.id;
const totalSets = ex.sets.length;
const summary = totalSets > 0 ? `${totalSets} подх.` : '';
const card = document.createElement('div');
card.className = 'exercise-card';
card.innerHTML = `
<div class="exercise-header ${isExpanded ? 'expanded' : ''}" onclick="EL.toggleExercise('${ex.id}')">
<div>
<span class="exercise-name">${ex.name}</span>
<span class="exercise-summary">${summary}</span>
</div>
<span class="chevron ${isExpanded ? 'open' : ''}">›</span>
</div>
`;
const body = document.createElement('div');
body.className = `exercise-body ${isExpanded ? 'expanded' : ''}`;
if (isExpanded) {
if (ex.sets.length > 0) {
const table = document.createElement('div');
table.className = 'sets-table';
[...ex.sets].reverse().forEach((set, idx) => {
const realIdx = ex.sets.length - 1 - idx;
table.appendChild(renderSetRow(w.id, ex.id, set, realIdx, isReadOnly));
});
body.appendChild(table);
} else {
body.innerHTML += `<p class="text-muted text-small">Пока нет подходов</p>`;
}
if (!isReadOnly) {
if (!w.endTime) {
const addBtn = document.createElement('button');
addBtn.className = 'btn mt-10';
addBtn.textContent = '+ Добавить подход';
addBtn.onclick = () => window.SL.addSet(w.id, ex.id);
body.appendChild(addBtn);
}
const delBtn = document.createElement('button');
delBtn.className = 'btn btn-ghost mt-10';
delBtn.style.width = '100%';
delBtn.textContent = 'Удалить упражнение';
delBtn.onclick = (e) => {
e.stopPropagation();
window.EL.deleteExercise(w.id, ex.id);
};
body.appendChild(delBtn);
}
}
card.appendChild(body);
return card;
}
+43
View File
@@ -0,0 +1,43 @@
// Рендеринг главного экрана: список тренировок.
// Получает данные из state, возвращает DOM.
import { state } from '../state.js';
import { renderWorkoutCard } from './workout-card.js';
import { startWorkout } from '../logic/workout.js';
export function renderMainScreen() {
const app = document.getElementById('app');
app.innerHTML = '';
if (state.workouts.length === 0) {
app.innerHTML = `
<div class="empty-state">
<h3>🏋️ Нет тренировок</h3>
<p>Начните первую тренировку</p>
<button class="btn" id="btn-start-first">▶ Начать тренировку</button>
</div>
`;
document.getElementById('btn-start-first').onclick = startWorkout;
return;
}
// Сортировка: активные сверху, завершённые снизу
const sorted = [...state.workouts].sort((a, b) => {
if (!a.endTime && b.endTime) return -1;
if (a.endTime && !b.endTime) return 1;
return new Date(b.startTime) - new Date(a.startTime);
});
const hasActive = sorted.some(w => !w.endTime);
if (!hasActive) {
const btn = document.createElement('button');
btn.className = 'btn mb-10';
btn.textContent = '▶ Начать новую тренировку';
btn.onclick = startWorkout;
app.appendChild(btn);
}
sorted.forEach(w => {
app.appendChild(renderWorkoutCard(w));
});
}
+35
View File
@@ -0,0 +1,35 @@
export function renderSetRow(workoutId, exerciseId, set, index, isReadOnly = false) {
const row = document.createElement('div');
const actionsHtml = isReadOnly ? '' : `
<div class="set-actions">
<button class="icon-btn" onclick="SL.editSet('${workoutId}', '${exerciseId}', '${set.id}')">✎</button>
<button class="icon-btn danger" onclick="SL.deleteSet('${workoutId}', '${exerciseId}', '${set.id}')">✕</button>
</div>
`;
if (set.isDropset && set.subSets && set.subSets.length > 0) {
row.className = 'dropset-group';
const subsText = set.subSets.map(s => `${s.weight}×${s.reps}`).join(' → ');
const ratingStars = '★'.repeat(set.rating || 3);
row.innerHTML = `
<div class="set-row" style="grid-template-columns: 30px 1fr auto ${isReadOnly ? '0' : 'auto'};">
<div class="set-number">${index + 1}</div>
<div class="set-cell dropset">🔥 ${subsText}</div>
<div class="set-rating">${ratingStars}</div>
${actionsHtml}
</div>
`;
} else {
row.innerHTML = `
<div class="set-row">
<div class="set-number">${index + 1}</div>
<div class="set-cell">${set.weight || '—'} кг</div>
<div class="set-cell">${set.reps || '—'}</div>
<div class="set-rating">${'★'.repeat(set.rating || 3)}</div>
${actionsHtml}
</div>
`;
}
return row;
}
+169
View File
@@ -0,0 +1,169 @@
import { state } from '../state.js';
import { renderExercise } from './exercise-card.js';
import { renderCardioItem } from './cardio-card.js';
import { formatDate } from '../utils.js';
export function renderWorkoutCard(w) {
const isActive = !w.endTime;
const isExpanded = state.expandedWorkoutId === w.id;
const isReadOnly = w.synced && w.endTime;
const div = document.createElement('div');
div.className = 'workout-accordion';
const headerClass = `workout-header ${isActive ? 'active' : 'completed'}`;
const dateStr = formatDate(w.startTime);
const typeStr = w.type || 'тип не выбран';
const exCount = w.exercises.length;
const cardioCount = w.cardio.length;
let statusBadge;
if (isReadOnly) statusBadge = '<span class="workout-badge synced-badge">✓ архив</span>';
else if (w.synced) statusBadge = '<span class="workout-badge synced-badge">✓</span>';
else statusBadge = '<span class="workout-badge dirty">● не сохранено</span>';
const editDateBtn = !isReadOnly
? `<span class="editable-date" onclick="event.stopPropagation(); WL.editWorkoutDate('${w.id}')">
<span class="edit-icon">✎</span>
</span>`
: '';
div.innerHTML = `
<div class="${headerClass}" onclick="WL.toggleWorkout('${w.id}')">
<div>
<div style="font-weight:600;">${isActive ? '🔥 Тренировка' : '✅ Завершена'}</div>
<div class="workout-meta">${dateStr} ${editDateBtn} • ${typeStr}</div>
<div class="workout-meta">Упр: ${exCount} • Кардио: ${cardioCount}</div>
</div>
<div>${statusBadge} <span class="chevron ${isExpanded ? 'open' : ''}">›</span></div>
</div>
`;
const body = document.createElement('div');
body.className = `workout-body ${isExpanded ? 'expanded' : ''}`;
if (isExpanded) {
body.appendChild(renderWorkoutDetails(w));
}
div.appendChild(body);
return div;
}
export function renderWorkoutDetails(w) {
const wrap = document.createElement('div');
const isActive = !w.endTime;
const isReadOnly = w.synced && w.endTime;
if (isReadOnly) {
const banner = document.createElement('div');
banner.className = 'read-only-banner';
banner.textContent = '🔒 Тренировка в архиве — только просмотр';
wrap.appendChild(banner);
}
// Тип тренировки
const typeCard = document.createElement('div');
typeCard.className = 'card';
if (isReadOnly) {
typeCard.innerHTML = `
<div class="field-group" style="margin:0;">
<span class="text-muted text-small">Тип тренировки</span>
<div style="font-weight:600; margin-top:4px;">${w.type || '—'}</div>
</div>`;
} else {
typeCard.innerHTML = `
<label class="field-group" style="margin:0;">
<span>Тип тренировки</span>
<select onchange="App.getWorkout('${w.id}').type = this.value; App.saveAndRender();">
<option value="">— выберите —</option>
<option ${w.type==='грудь-руки'?'selected':''}>грудь-руки</option>
<option ${w.type==='ноги'?'selected':''}>ноги</option>
<option ${w.type==='спина-плечи'?'selected':''}>спина-плечи</option>
<option ${w.type==='круговая'?'selected':''}>круговая</option>
<option ${w.type==='силовая'?'selected':''}>силовая</option>
<option ${w.type==='многоповторная'?'selected':''}>многоповторная</option>
</select>
</label>`;
}
wrap.appendChild(typeCard);
// Кнопки добавления
if (isActive && !isReadOnly) {
const addSection = document.createElement('div');
addSection.className = 'add-section';
addSection.innerHTML = `
<button class="btn btn-secondary" onclick="EL.addExercise('${w.id}')">+ Упражнение</button>
<button class="btn btn-secondary" onclick="CL.addCardio('${w.id}')">+ Кардио</button>
`;
wrap.appendChild(addSection);
}
// Кардио: разминка сверху
const cardioWarmup = w.cardio.filter(c => c.purpose === 'warmup' || c.purpose === 'separate');
const cardioCooldown = w.cardio.filter(c => c.purpose === 'cooldown');
if (cardioWarmup.length > 0) {
const title = document.createElement('h4');
title.style.margin = '16px 0 8px';
title.textContent = '🔥 Разминка / Кардио';
wrap.appendChild(title);
cardioWarmup.forEach(c => wrap.appendChild(renderCardioItem(w.id, c, isReadOnly)));
}
// Упражнения
[...w.exercises].reverse().forEach(ex => {
wrap.appendChild(renderExercise(w, ex, isReadOnly));
});
// Заминка внизу
if (cardioCooldown.length > 0) {
const title = document.createElement('h4');
title.style.margin = '16px 0 8px';
title.textContent = '🧘 Заминка';
wrap.appendChild(title);
cardioCooldown.forEach(c => wrap.appendChild(renderCardioItem(w.id, c, isReadOnly)));
}
// Комментарий
const notesCard = document.createElement('div');
notesCard.className = 'card mt-10';
if (isReadOnly) {
notesCard.innerHTML = `
<div class="field-group" style="margin:0;">
<span class="text-muted text-small">Комментарий</span>
<div style="margin-top:4px; white-space:pre-wrap;">${w.notes || '—'}</div>
</div>`;
} else {
notesCard.innerHTML = `
<label class="field-group" style="margin:0;">
<span>Комментарий к тренировке</span>
<textarea placeholder="Как прошла тренировка...">${w.notes || ''}</textarea>
</label>`;
notesCard.querySelector('textarea').oninput = (e) => {
w.notes = e.target.value;
window.App.saveAndRender();
};
}
wrap.appendChild(notesCard);
// Блок завершения
const finishBlock = document.createElement('div');
finishBlock.className = 'finish-block';
if (isReadOnly) {
finishBlock.innerHTML = `
<h3>🔒 Архив</h3>
<p class="text-muted text-small">Тренировка синхронизирована и завершена. Редактирование недоступно.</p>`;
} else if (isActive) {
finishBlock.innerHTML = `
<h3>Завершение тренировки</h3>
<p class="text-muted text-small">После завершения данные останутся локально до синхронизации</p>
<button class="btn btn-danger" onclick="WL.finishWorkout('${w.id}')">⏹ Закончить тренировку</button>`;
} else {
finishBlock.innerHTML = `
<h3>Тренировка завершена</h3>
<p class="text-muted text-small">До синхронизации можно редактировать</p>
<button class="btn btn-danger" onclick="WL.deleteWorkout('${w.id}')">🗑 Удалить</button>`;
}
wrap.appendChild(finishBlock);
return wrap;
}