Files
gym-bro/app.js
T

865 lines
35 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.
// ============================================================
// STATE
// ============================================================
const STORAGE_KEY = 'gym_tracker_v4';
const App = {
state: {
workouts: [],
exerciseRef: [], // [{id, name, workoutType}]
bodyMetrics: [],
expandedWorkoutId: null,
expandedExerciseId: null,
nextId: 1,
},
init() {
this.load();
this.render();
},
load() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
try {
this.state = JSON.parse(saved);
if (!this.state.exerciseRef) this.state.exerciseRef = [];
if (!this.state.bodyMetrics) this.state.bodyMetrics = [];
} catch (e) {
console.error('Failed to load state', e);
}
}
},
save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state));
this.updateSyncStatus();
},
genId() {
return (this.state.nextId++).toString();
},
// ============================================================
// SYNC
// ============================================================
getDirtyWorkouts() {
return this.state.workouts.filter(w => !w.synced);
},
updateSyncStatus() {
const dirty = this.getDirtyWorkouts();
const el = document.getElementById('sync-status');
if (dirty.length === 0) {
el.textContent = '● Всё синхронизировано';
el.className = 'sync-status synced';
} else {
el.textContent = `● ${dirty.length} не сохран.`;
el.className = 'sync-status pending';
}
},
async sync() {
const dirty = this.getDirtyWorkouts();
if (dirty.length === 0) {
alert('Нет данных для синхронизации');
return;
}
console.log('Syncing:', JSON.stringify(dirty, null, 2));
dirty.forEach(w => w.synced = true);
this.save();
this.render();
alert(`Отправлено тренировок: ${dirty.length}`);
},
// ============================================================
// WORKOUT CRUD
// ============================================================
startWorkout() {
const w = {
id: this.genId(),
startTime: new Date().toISOString(),
endTime: null,
type: '',
exercises: [],
cardio: [],
notes: '',
synced: false,
};
this.state.workouts.unshift(w);
this.state.expandedWorkoutId = w.id;
this.save();
this.render();
},
finishWorkout(workoutId) {
if (!confirm('Завершить тренировку?')) return;
const w = this.getWorkout(workoutId);
w.endTime = new Date().toISOString();
this.save();
this.render();
},
deleteWorkout(workoutId) {
if (!confirm('Удалить тренировку полностью?')) return;
this.state.workouts = this.state.workouts.filter(w => w.id !== workoutId);
this.save();
this.render();
},
toggleWorkout(workoutId) {
this.state.expandedWorkoutId =
this.state.expandedWorkoutId === workoutId ? null : workoutId;
this.render();
},
getWorkout(id) {
return this.state.workouts.find(w => w.id === id);
},
getActiveWorkout() {
return this.state.workouts.find(w => !w.endTime);
},
editWorkoutDate(workoutId) {
const w = this.getWorkout(workoutId);
if (!w) return;
Modal.showEditWorkoutDateDialog(w, (newDates) => {
if (!newDates) return;
w.startTime = newDates.startTime;
w.endTime = newDates.endTime;
this.save();
this.render();
});
},
// ============================================================
// EXERCISE CRUD
// ============================================================
addExercise(workoutId) {
const w = this.getWorkout(workoutId);
Modal.showExerciseDialog(w.type, (name) => {
if (!name) return;
const ex = {
id: this.genId(),
name: name,
sets: [],
};
w.exercises.push(ex);
this._addToExerciseRef(name, w.type);
this.state.expandedExerciseId = ex.id;
this.save();
this.render();
});
},
_addToExerciseRef(name, workoutType = '') {
const normalizedName = name.trim().toLowerCase();
const exists = this.state.exerciseRef.find(
e => e.name.trim().toLowerCase() === normalizedName
);
if (!exists) {
this.state.exerciseRef.push({
id: this.genId(),
name: name.trim(),
workoutType: workoutType,
});
} else if (!exists.workoutType && workoutType) {
// Обновляем тип, если он был пуст
exists.workoutType = workoutType;
}
},
toggleExercise(exerciseId) {
this.state.expandedExerciseId =
this.state.expandedExerciseId === exerciseId ? null : exerciseId;
this.render();
},
deleteExercise(workoutId, exerciseId) {
if (!confirm('Удалить упражнение?')) return;
const w = this.getWorkout(workoutId);
w.exercises = w.exercises.filter(e => e.id !== exerciseId);
this.save();
this.render();
},
getExerciseHistory(exerciseName) {
const normalizedName = exerciseName.trim().toLowerCase();
const history = [];
for (const w of this.state.workouts) {
for (const ex of w.exercises) {
if (ex.name.trim().toLowerCase() === normalizedName && ex.sets.length > 0) {
history.push({
date: w.startTime,
sets: ex.sets,
});
}
}
}
history.sort((a, b) => new Date(b.date) - new Date(a.date));
return history.slice(0, 3);
},
// ============================================================
// SET CRUD
// ============================================================
addSet(workoutId, exerciseId) {
const w = this.getWorkout(workoutId);
const ex = w.exercises.find(e => e.id === exerciseId);
const lastSet = ex.sets[ex.sets.length - 1];
const history = this.getExerciseHistory(ex.name);
Modal.showSetDialog(lastSet, history, (setData) => {
if (!setData) return;
setData.id = this.genId();
setData.timestamp = new Date().toISOString();
ex.sets.push(setData);
this.save();
this.render();
});
},
editSet(workoutId, exerciseId, setId) {
const w = this.getWorkout(workoutId);
const ex = w.exercises.find(e => e.id === exerciseId);
const set = ex.sets.find(s => s.id === setId);
const history = this.getExerciseHistory(ex.name);
Modal.showSetDialog(set, history, (setData) => {
if (!setData) return;
Object.assign(set, setData);
this.save();
this.render();
});
},
deleteSet(workoutId, exerciseId, setId) {
if (!confirm('Удалить подход?')) return;
const w = this.getWorkout(workoutId);
const ex = w.exercises.find(e => e.id === exerciseId);
ex.sets = ex.sets.filter(s => s.id !== setId);
this.save();
this.render();
},
// ============================================================
// CARDIO CRUD
// ============================================================
addCardio(workoutId) {
Modal.showCardioDialog(null, (cardio) => {
if (!cardio) return;
const w = this.getWorkout(workoutId);
cardio.id = this.genId();
w.cardio.push(cardio);
this.save();
this.render();
});
},
editCardio(workoutId, cardioId) {
const w = this.getWorkout(workoutId);
const cardio = w.cardio.find(c => c.id === cardioId);
Modal.showCardioDialog(cardio, (newData) => {
if (!newData) return;
Object.assign(cardio, newData);
this.save();
this.render();
});
},
deleteCardio(workoutId, cardioId) {
if (!confirm('Удалить кардио?')) return;
const w = this.getWorkout(workoutId);
w.cardio = w.cardio.filter(c => c.id !== cardioId);
this.save();
this.render();
},
// ============================================================
// BODY METRICS
// ============================================================
showBodyMetrics() {
Modal.showBodyMetricsDialog(this.state.bodyMetrics, (newMetric, isEdit) => {
if (!newMetric) return;
if (isEdit) {
// При редактировании — ищем по временной метке исходного замера
// (передаём через замыкание)
return;
}
newMetric.id = this.genId();
this.state.bodyMetrics.unshift(newMetric);
// Сортируем по дате (новые сверху)
this.state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date));
this.save();
this.render();
alert('Замеры сохранены!');
});
},
editBodyMetric(metricId) {
const metric = this.state.bodyMetrics.find(m => m.id === metricId);
if (!metric) return;
Modal.showBodyMetricsDialog(this.state.bodyMetrics, (updatedData, isEdit) => {
if (!updatedData) return;
Object.assign(metric, updatedData);
this.state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date));
this.save();
this._renderBodyMetricsList();
}, metric); // передаём редактируемый замер
},
// ============================================================
// SETTINGS
// ============================================================
showSettings() {
const app = document.getElementById('app');
app.innerHTML = '';
const screen = document.createElement('div');
screen.className = 'settings-screen';
screen.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2>⚙️ Настройки</h2>
<button class="btn btn-small btn-secondary" onclick="App.render()">← Назад</button>
</div>
<div class="settings-section">
<h3>📋 Справочник упражнений</h3>
<p class="text-muted text-small mb-10">
Управляйте списком упражнений и привязывайте их к типам тренировок.
</p>
<div class="field-group">
<div style="display:flex; gap:6px;">
<input type="text" id="new-ref-exercise" placeholder="Новое упражнение...">
<select id="new-ref-type" style="width: auto; min-width: 120px;">
<option value="">тип...</option>
<option value="грудь-руки">грудь-руки</option>
<option value="ноги">ноги</option>
<option value="спина-плечи">спина-плечи</option>
<option value="круговая">круговая</option>
<option value="силовая">силовая</option>
<option value="многоповторная">многоповторная</option>
</select>
<button class="btn btn-small" style="width:auto; padding: 0 20px;"
onclick="App.addRefExercise()">+</button>
</div>
</div>
<div id="ref-exercises-list"></div>
</div>
<div class="settings-section">
<h3>📏 Замеры тела</h3>
<div id="body-metrics-list"></div>
</div>
<div class="settings-section">
<h3>🗑 Данные</h3>
<button class="btn btn-danger" onclick="App.clearAllData()">Очистить все данные</button>
</div>
`;
app.appendChild(screen);
this._renderRefExercises();
this._renderBodyMetricsList();
},
addRefExercise() {
const input = document.getElementById('new-ref-exercise');
const typeSelect = document.getElementById('new-ref-type');
const name = input.value.trim();
if (!name) return;
this._addToExerciseRef(name, typeSelect.value);
this.save();
input.value = '';
typeSelect.value = '';
this._renderRefExercises();
},
deleteRefExercise(id) {
if (!confirm('Удалить из справочника? (в тренировках останется)')) return;
this.state.exerciseRef = this.state.exerciseRef.filter(e => e.id !== id);
this.save();
this._renderRefExercises();
},
updateRefExerciseType(id, newType) {
const ex = this.state.exerciseRef.find(e => e.id === id);
if (ex) {
ex.workoutType = newType;
this.save();
}
},
editRefExercise(id) {
const ex = this.state.exerciseRef.find(e => e.id === id);
if (!ex) return;
Modal.showEditExerciseDialog(ex, (updatedData) => {
if (!updatedData) return;
Object.assign(ex, updatedData);
this.save();
this._renderRefExercises();
});
},
_renderRefExercises() {
const list = document.getElementById('ref-exercises-list');
if (!list) return;
if (this.state.exerciseRef.length === 0) {
list.innerHTML = '<p class="text-muted text-small">Справочник пуст</p>';
return;
}
const sorted = [...this.state.exerciseRef].sort((a, b) => {
if (a.workoutType !== b.workoutType) {
return (a.workoutType || '').localeCompare(b.workoutType || '');
}
return a.name.localeCompare(b.name);
});
list.innerHTML = sorted.map(ex => {
let count = 0;
this.state.workouts.forEach(w => {
w.exercises.forEach(e => {
if (e.name.trim().toLowerCase() === ex.name.trim().toLowerCase()) {
count++;
}
});
});
return `
<div class="exercise-ref-item">
<div style="flex:1;">
<span class="name">${ex.name}</span>
<span class="count">(${count}×)</span>
${ex.workoutType ? `<span class="type-badge">${ex.workoutType}</span>` : ''}
</div>
<div style="display:flex; gap:4px; align-items:center;">
<select onchange="App.updateRefExerciseType('${ex.id}', this.value)"
style="width:auto; min-width:100px; padding:6px; font-size:0.8rem;">
<option value="">тип...</option>
<option value="грудь-руки" ${ex.workoutType==='грудь-руки'?'selected':''}>грудь-руки</option>
<option value="ноги" ${ex.workoutType==='ноги'?'selected':''}>ноги</option>
<option value="спина-плечи" ${ex.workoutType==='спина-плечи'?'selected':''}>спина-плечи</option>
<option value="круговая" ${ex.workoutType==='круговая'?'selected':''}>круговая</option>
<option value="силовая" ${ex.workoutType==='силовая'?'selected':''}>силовая</option>
<option value="многоповторная" ${ex.workoutType==='многоповторная'?'selected':''}>многоповторная</option>
</select>
<button class="icon-btn" onclick="App.editRefExercise('${ex.id}')">✎</button>
<button class="icon-btn danger" onclick="App.deleteRefExercise('${ex.id}')">✕</button>
</div>
</div>
`;
}).join('');
},
_renderBodyMetricsList() {
const list = document.getElementById('body-metrics-list');
if (!list) return;
if (this.state.bodyMetrics.length === 0) {
list.innerHTML = '<p class="text-muted text-small">Нет замеров</p>';
return;
}
list.innerHTML = this.state.bodyMetrics.map(m => {
const date = new Date(m.date);
const dateStr = date.toLocaleDateString('ru-RU') + ' ' + date.toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit'});
const parts = [];
if (m.weight) parts.push(`⚖️ ${m.weight} кг`);
if (m.chest) parts.push(`📏 ${m.chest} см`);
if (m.waist) parts.push(`📏 ${m.waist} см`);
if (m.hips) parts.push(`📏 ${m.hips} см`);
if (m.arms) parts.push(`💪 ${m.arms} см`);
if (m.legs) parts.push(`🦵 ${m.legs} см`);
return `
<div class="exercise-ref-item">
<div>
<div style="font-weight:500;">${dateStr}</div>
<div class="text-small text-muted">${parts.join(' • ') || '—'}</div>
</div>
<div style="display:flex; gap:4px;">
<button class="icon-btn" onclick="App.editBodyMetric('${m.id}')">✎</button>
<button class="icon-btn danger" onclick="App.deleteMetric('${m.id}')">✕</button>
</div>
</div>
`;
}).join('');
},
deleteMetric(id) {
if (!confirm('Удалить замер?')) return;
this.state.bodyMetrics = this.state.bodyMetrics.filter(m => m.id !== id);
this.save();
this._renderBodyMetricsList();
},
clearAllData() {
if (!confirm('Удалить ВСЕ данные? Это действие необратимо!')) return;
if (!confirm('Точно уверены?')) return;
localStorage.removeItem(STORAGE_KEY);
location.reload();
},
// ============================================================
// RENDER
// ============================================================
render() {
const app = document.getElementById('app');
app.innerHTML = '';
if (this.state.workouts.length === 0) {
app.innerHTML = `
<div class="empty-state">
<h3>🏋️ Нет тренировок</h3>
<p>Начните первую тренировку</p>
<button class="btn" onclick="App.startWorkout()">▶ Начать тренировку</button>
</div>
`;
return;
}
const sorted = [...this.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);
});
if (!this.getActiveWorkout()) {
const btn = document.createElement('button');
btn.className = 'btn mb-10';
btn.textContent = '▶ Начать новую тренировку';
btn.onclick = () => this.startWorkout();
app.appendChild(btn);
}
sorted.forEach(w => {
app.appendChild(this.renderWorkoutItem(w));
});
this.updateSyncStatus();
},
renderWorkoutItem(w) {
const isActive = !w.endTime;
const isExpanded = this.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'} ${isReadOnly ? 'read-only-overlay' : ''}`;
const start = new Date(w.startTime);
const dateStr = start.toLocaleDateString('ru-RU') + ' ' + start.toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit'});
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(); App.editWorkoutDate('${w.id}')">
<span class="edit-icon">✎</span>
</span>`
: '';
div.innerHTML = `
<div class="${headerClass}" onclick="App.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(this.renderWorkoutDetails(w));
}
div.appendChild(body);
return div;
},
renderWorkoutDetails(w) {
const wrap = document.createElement('div');
const isActive = !w.endTime;
const isReadOnly = w.synced && w.endTime;
// Баннер read-only
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.save();" ${isReadOnly ? 'disabled' : ''}>
<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);
// Кнопки добавления (только для активной и не read-only)
if (isActive && !isReadOnly) {
const addSection = document.createElement('div');
addSection.className = 'add-section';
addSection.innerHTML = `
<button class="btn btn-secondary" onclick="App.addExercise('${w.id}')">+ Упражнение</button>
<button class="btn btn-secondary" onclick="App.addCardio('${w.id}')">+ Кардио</button>
`;
wrap.appendChild(addSection);
}
// === Сортировка кардио: warmup/separate сверху, cooldown внизу ===
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(this.renderCardioItem(w.id, c, isReadOnly));
});
}
// Упражнения
const sortedExercises = [...w.exercises].reverse();
sortedExercises.forEach(ex => {
wrap.appendChild(this.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(this.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;
this.save();
};
}
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="App.finishWorkout('${w.id}')">⏹ Закончить тренировку</button>
`;
} else {
finishBlock.innerHTML = `
<h3>Тренировка завершена</h3>
<p class="text-muted text-small">До синхронизации можно редактировать</p>
<button class="btn btn-danger" onclick="App.deleteWorkout('${w.id}')">🗑 Удалить</button>
`;
}
wrap.appendChild(finishBlock);
return wrap;
},
renderExercise(w, ex, isReadOnly = false) {
const isExpanded = this.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="App.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(this.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 = () => this.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();
this.deleteExercise(w.id, ex.id);
};
body.appendChild(delBtn);
}
}
card.appendChild(body);
return card;
},
renderSetRow(workoutId, exerciseId, set, index, isReadOnly = false) {
const row = document.createElement('div');
const actionsHtml = isReadOnly ? '' : `
<div class="set-actions">
<button class="icon-btn" onclick="App.editSet('${workoutId}', '${exerciseId}', '${set.id}')">✎</button>
<button class="icon-btn danger" onclick="App.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;
},
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="App.editCardio('${workoutId}', '${c.id}')">✎</button>
<button class="icon-btn danger" onclick="App.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;
},
};
App.init();