UI
This commit is contained in:
@@ -18,22 +18,27 @@
|
||||
.tp-day-body { padding: 14px 16px 16px; }
|
||||
.tp-day-actions { display: flex; gap: 6px; justify-content: flex-end; align-items: center; flex-wrap: wrap; }
|
||||
.tp-textareas { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 14px; }
|
||||
.tp-exercise-row { display: grid; grid-template-columns: minmax(180px, 2fr) 72px minmax(80px, 1fr) 86px minmax(110px, 1fr) auto; gap: 8px; align-items: end; padding: 9px 0; border-bottom: 1px solid var(--bt-line-soft); }
|
||||
.tp-exercise-row { display: grid; grid-template-columns: minmax(220px, 1.2fr) minmax(180px, 1fr) auto; gap: 8px; align-items: end; padding: 9px 0; border-bottom: 1px solid var(--bt-line-soft); }
|
||||
.tp-exercise-row:last-child { border-bottom: none; }
|
||||
.tp-exercise-actions { display: flex; gap: 5px; justify-content: flex-end; }
|
||||
.tp-addline { display: grid; grid-template-columns: minmax(200px, 1fr) auto; gap: 8px; align-items: end; margin-top: 12px; }
|
||||
.tp-table-input { min-width: 130px; }
|
||||
.tp-library { display: grid; gap: 12px; }
|
||||
.tp-library-row { border-bottom: 1px solid var(--bt-line); padding: 14px 16px; }
|
||||
.tp-library-row:last-child { border-bottom: none; }
|
||||
.tp-library-main { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(220px, 1fr) auto; gap: 8px; align-items: end; }
|
||||
.tp-progressions { display: grid; gap: 8px; margin-top: 10px; }
|
||||
.tp-progression-row { display: grid; grid-template-columns: 34px minmax(180px, 1fr) auto; gap: 8px; align-items: center; }
|
||||
.tp-progression-number { color: var(--bt-muted); font-size: 12px; text-align: center; }
|
||||
.tp-addline, .tp-formline { display: grid; grid-template-columns: minmax(220px, 1fr) auto; gap: 8px; align-items: end; margin-top: 12px; }
|
||||
.tp-table-input { width: 100%; min-width: 0; }
|
||||
.tp-code { min-height: 520px; font-family: var(--bt-mono); font-size: 12.5px; }
|
||||
.tp-warn { color: var(--bt-warn); }
|
||||
@media (max-width: 980px) {
|
||||
.tp-top, .tp-layout, .tp-day-head, .tp-textareas, .tp-addline { grid-template-columns: 1fr; }
|
||||
.tp-top, .tp-layout, .tp-day-head, .tp-textareas, .tp-addline, .tp-formline { grid-template-columns: 1fr; }
|
||||
.tp-side { position: static; }
|
||||
.tp-actions, .tp-day-actions { justify-content: flex-start; }
|
||||
.tp-exercise-row { grid-template-columns: 1fr 86px 1fr 86px; }
|
||||
.tp-exercise-row label:first-child, .tp-exercise-row label:nth-child(5), .tp-exercise-actions { grid-column: 1 / -1; }
|
||||
}
|
||||
@media (max-width: 620px) {
|
||||
.tp-exercise-row { grid-template-columns: 1fr; }
|
||||
.tp-exercise-row, .tp-library-main, .tp-progression-row { grid-template-columns: 1fr; }
|
||||
.tp-exercise-actions { justify-content: flex-start; }
|
||||
.tp-progression-number { text-align: left; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -133,6 +138,20 @@ function exerciseName(id) {
|
||||
return (state.cfg?.exercises || []).find((exercise) => exercise.id === id)?.name || id || "Übung";
|
||||
}
|
||||
|
||||
function currentProgressionName(exerciseId) {
|
||||
const exercise = (state.cfg?.exercises || []).find((item) => item.id === exerciseId);
|
||||
if (!exercise) return "";
|
||||
const progressions = exercise.progressions || [];
|
||||
const selected = progressions.find((item) => item.id === exercise.current_progression_id) || progressions[0];
|
||||
return selected?.name || "";
|
||||
}
|
||||
|
||||
function progressionOptions(exercise, selected = "") {
|
||||
return (exercise.progressions || []).map((progression) =>
|
||||
`<option value="${esc(progression.id)}"${progression.id === selected ? " selected" : ""}>${esc(progression.name || progression.id)}</option>`
|
||||
).join("");
|
||||
}
|
||||
|
||||
function optionList(selected = "") {
|
||||
return (state.cfg?.exercises || []).map((exercise) =>
|
||||
`<option value="${esc(exercise.id)}"${exercise.id === selected ? " selected" : ""}>${esc(exercise.name || exercise.id)}</option>`
|
||||
@@ -177,7 +196,7 @@ function renderStatus() {
|
||||
? `${state.activePlanId} · Bearbeitung R${wrapper.revision || 1} · veröffentlicht R${wrapper.published_revision || 1}`
|
||||
: "Kein Trainingsplan gefunden.";
|
||||
$("statusPills").innerHTML = [
|
||||
`<span class="bt-badge">Schema ${cfg.schema_version || 4}/${cfg.contract_version || 3}</span>`,
|
||||
`<span class="bt-badge">Schema ${cfg.schema_version || 5}/${cfg.contract_version || 4}</span>`,
|
||||
`<span class="bt-badge">${(cfg.days || []).length} Tage</span>`,
|
||||
`<span class="bt-badge">${(cfg.exercises || []).length} Übungen</span>`,
|
||||
state.dirty || wrapper.has_unpublished_changes
|
||||
@@ -231,10 +250,7 @@ function dayHtml(day, index) {
|
||||
const items = (day.exercises || []).map((item, itemIndex) => `
|
||||
<div class="tp-exercise-row" data-day-item="${esc(item.id)}">
|
||||
<label>Übung<select data-day-field="exercise_id">${optionList(item.exercise_id)}</select></label>
|
||||
<label>Sätze<input type="number" min="1" max="40" data-day-field="sets" value="${esc(item.sets || "")}"></label>
|
||||
<label>Wdh.<input type="text" data-day-field="reps" value="${esc(item.reps || "")}"></label>
|
||||
<label>Sek.<input type="number" min="1" data-day-field="duration_seconds" value="${esc(item.duration_seconds || "")}"></label>
|
||||
<label>Notiz<input type="text" data-day-field="notes" value="${esc(item.notes || "")}"></label>
|
||||
<label>Aktuell<input type="text" value="${esc(currentProgressionName(item.exercise_id) || "Keine Progression")}" readonly></label>
|
||||
<div class="tp-exercise-actions">
|
||||
<button class="mini ghost" data-move-day-item="-1" title="Nach oben">↑</button>
|
||||
<button class="mini ghost" data-move-day-item="1" title="Nach unten">↓</button>
|
||||
@@ -271,32 +287,44 @@ function dayHtml(day, index) {
|
||||
|
||||
function renderExercises() {
|
||||
const rows = (state.cfg.exercises || []).map((exercise) => `
|
||||
<tr data-exercise-id="${esc(exercise.id)}">
|
||||
<td><input class="tp-table-input" data-exercise-field="name" value="${esc(exercise.name || "")}"></td>
|
||||
<td><input class="tp-table-input" data-exercise-field="cue" value="${esc(exercise.cue || "")}"></td>
|
||||
<td><input class="tp-table-input" data-exercise-field="equipment" value="${esc(exercise.equipment || "")}"></td>
|
||||
<td><input class="tp-table-input" data-exercise-field="description" value="${esc(exercise.description || "")}"></td>
|
||||
<td class="bt-mono bt-muted">${esc(exercise.id)}</td>
|
||||
<td><button class="mini danger" data-remove-exercise>×</button></td>
|
||||
</tr>
|
||||
<div class="tp-library-row" data-exercise-id="${esc(exercise.id)}">
|
||||
<div class="tp-library-main">
|
||||
<label>Name<input class="tp-table-input" data-exercise-field="name" value="${esc(exercise.name || "")}"></label>
|
||||
<label>Aktuelle Progression<select data-exercise-field="current_progression_id">
|
||||
<option value="">Keine Progression</option>
|
||||
${progressionOptions(exercise, exercise.current_progression_id)}
|
||||
</select></label>
|
||||
<div class="tp-exercise-actions">
|
||||
<button class="mini ghost" data-add-progression>Progression</button>
|
||||
<button class="mini danger" data-remove-exercise>×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tp-progressions">
|
||||
${(exercise.progressions || []).map((progression, index) => `
|
||||
<div class="tp-progression-row" data-progression-id="${esc(progression.id)}">
|
||||
<div class="tp-progression-number">${index + 1}</div>
|
||||
<label>Progression<input class="tp-table-input" data-progression-field="name" value="${esc(progression.name || "")}"></label>
|
||||
<div class="tp-exercise-actions">
|
||||
<button class="mini ghost" data-move-progression="-1" title="Nach oben">↑</button>
|
||||
<button class="mini ghost" data-move-progression="1" title="Nach unten">↓</button>
|
||||
<button class="mini danger" data-remove-progression title="Entfernen">×</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="bt-empty">Keine Progressionen hinterlegt.</div>`}
|
||||
</div>
|
||||
<div class="bt-mono bt-muted" style="margin-top:8px">${esc(exercise.id)}</div>
|
||||
</div>
|
||||
`).join("");
|
||||
$("tab-uebungen").innerHTML = `
|
||||
<div class="bt-card">
|
||||
<h2>Übung anlegen</h2>
|
||||
<div class="bt-grid4">
|
||||
<div class="tp-formline">
|
||||
<label>Name<input id="newExerciseName" type="text"></label>
|
||||
<label>Cue<input id="newExerciseCue" type="text"></label>
|
||||
<label>Equipment<input id="newExerciseEquipment" type="text"></label>
|
||||
<div style="display:flex;align-items:end"><button class="primary" data-add-exercise>Hinzufügen</button></div>
|
||||
<button class="primary" data-add-exercise>Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bt-card bt-card-flush">
|
||||
${rows ? `
|
||||
<table class="bt-table">
|
||||
<thead><tr><th>Name</th><th>Cue</th><th>Equipment</th><th>Beschreibung</th><th>ID</th><th></th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
` : `<div class="bt-empty" style="margin:16px">Noch keine Übungen.</div>`}
|
||||
${rows ? `<div class="tp-library">${rows}</div>` : `<div class="bt-empty" style="margin:16px">Noch keine Übungen.</div>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -322,8 +350,8 @@ function addExerciseFromForm() {
|
||||
state.cfg.exercises.push({
|
||||
id: uid("exercise"),
|
||||
name,
|
||||
cue: $("newExerciseCue")?.value.trim() || "",
|
||||
equipment: $("newExerciseEquipment")?.value.trim() || "",
|
||||
progressions: [],
|
||||
current_progression_id: "",
|
||||
});
|
||||
markDirty();
|
||||
renderAll();
|
||||
@@ -345,8 +373,56 @@ function move(list, index, delta) {
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function syncFormStateFromDom() {
|
||||
if (!state.cfg) return;
|
||||
state.cfg.meta = state.cfg.meta || {};
|
||||
state.cfg.meta.title = $("metaTitle").value;
|
||||
if ($("metaSubtitle").value.trim()) state.cfg.meta.subtitle = $("metaSubtitle").value;
|
||||
else delete state.cfg.meta.subtitle;
|
||||
|
||||
document.querySelectorAll("[data-exercise-id]").forEach((row) => {
|
||||
const exercise = state.cfg.exercises.find((item) => item.id === row.dataset.exerciseId);
|
||||
if (!exercise) return;
|
||||
row.querySelectorAll("[data-exercise-field]").forEach((input) => {
|
||||
const key = input.dataset.exerciseField;
|
||||
if (key === "name") exercise.name = input.value;
|
||||
else if (input.value.trim()) exercise[key] = input.value;
|
||||
else delete exercise[key];
|
||||
});
|
||||
row.querySelectorAll("[data-progression-id]").forEach((progressionRow) => {
|
||||
const progression = (exercise.progressions || []).find((item) => item.id === progressionRow.dataset.progressionId);
|
||||
const input = progressionRow.querySelector('[data-progression-field="name"]');
|
||||
if (progression && input) progression.name = input.value;
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-day-id]").forEach((shell) => {
|
||||
const day = state.cfg.days.find((item) => item.id === shell.dataset.dayId);
|
||||
if (!day) return;
|
||||
const num = shell.querySelector('[data-day-prop="num"]');
|
||||
const name = shell.querySelector('[data-day-prop="name"]');
|
||||
const warmup = shell.querySelector('[data-day-text="warmup"]');
|
||||
const cooldown = shell.querySelector('[data-day-text="cooldown"]');
|
||||
if (num) day.num = Number(num.value || 1);
|
||||
if (name) {
|
||||
day.name = name.value;
|
||||
delete day.focus;
|
||||
}
|
||||
if (warmup) day.warmup = {items: warmup.value.split("\n").map((item) => item.trim()).filter(Boolean)};
|
||||
if (cooldown) day.cooldown = {items: cooldown.value.split("\n").map((item) => item.trim()).filter(Boolean)};
|
||||
|
||||
shell.querySelectorAll("[data-day-item]").forEach((row) => {
|
||||
const item = (day.exercises || []).find((entry) => entry.id === row.dataset.dayItem);
|
||||
if (!item) return;
|
||||
const exercise = row.querySelector('[data-day-field="exercise_id"]');
|
||||
if (exercise) item.exercise_id = exercise.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function savePlan() {
|
||||
if (!state.activePlanId || !state.cfg) return;
|
||||
syncFormStateFromDom();
|
||||
const saved = await api(`/api/plans/${encodeURIComponent(state.activePlanId)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({expected_revision: state.wrapper.revision, config: state.cfg}),
|
||||
@@ -362,6 +438,7 @@ async function savePlan() {
|
||||
|
||||
async function publishPlan() {
|
||||
if (!state.activePlanId) return;
|
||||
syncFormStateFromDom();
|
||||
if (state.dirty) await savePlan();
|
||||
const saved = await api(`/api/plans/${encodeURIComponent(state.activePlanId)}/publish`, {
|
||||
method: "POST",
|
||||
@@ -382,7 +459,7 @@ $("tabs").addEventListener("click", (event) => {
|
||||
if (state.tab === "json") renderJson();
|
||||
});
|
||||
|
||||
document.addEventListener("input", (event) => {
|
||||
function handleFormEdit(event) {
|
||||
const target = event.target;
|
||||
if (!state.cfg) return;
|
||||
if (target.id === "metaTitle") {
|
||||
@@ -404,6 +481,14 @@ document.addEventListener("input", (event) => {
|
||||
else delete exercise[key];
|
||||
markDirty();
|
||||
}
|
||||
const progressionRow = target.closest("[data-progression-id]");
|
||||
if (exerciseRow && progressionRow && target.dataset.progressionField) {
|
||||
const exercise = state.cfg.exercises.find((item) => item.id === exerciseRow.dataset.exerciseId);
|
||||
const progression = (exercise?.progressions || []).find((item) => item.id === progressionRow.dataset.progressionId);
|
||||
if (!progression) return;
|
||||
progression[target.dataset.progressionField] = target.value;
|
||||
markDirty();
|
||||
}
|
||||
const dayShell = target.closest("[data-day-id]");
|
||||
if (dayShell) {
|
||||
const day = state.cfg.days.find((item) => item.id === dayShell.dataset.dayId);
|
||||
@@ -418,16 +503,15 @@ document.addEventListener("input", (event) => {
|
||||
const item = (day.exercises || []).find((entry) => entry.id === itemRow.dataset.dayItem);
|
||||
if (!item) return;
|
||||
const key = target.dataset.dayField;
|
||||
if (key === "sets" || key === "duration_seconds") {
|
||||
const value = Number(target.value || 0);
|
||||
if (value > 0) item[key] = value;
|
||||
else delete item[key];
|
||||
} else if (target.value.trim()) item[key] = target.value;
|
||||
if (target.value.trim()) item[key] = target.value;
|
||||
else delete item[key];
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("input", handleFormEdit);
|
||||
document.addEventListener("change", handleFormEdit);
|
||||
|
||||
document.addEventListener("click", async (event) => {
|
||||
const button = event.target.closest("button");
|
||||
@@ -498,7 +582,16 @@ document.addEventListener("click", async (event) => {
|
||||
}
|
||||
|
||||
const exerciseRow = button.closest("[data-exercise-id]");
|
||||
if (exerciseRow && button.dataset.removeExercise !== undefined) {
|
||||
if (exerciseRow && button.dataset.addProgression !== undefined) {
|
||||
const exercise = state.cfg.exercises.find((item) => item.id === exerciseRow.dataset.exerciseId);
|
||||
if (!exercise) return;
|
||||
exercise.progressions = exercise.progressions || [];
|
||||
const progression = {id: uid("progression"), name: "Neue Progression"};
|
||||
exercise.progressions.push(progression);
|
||||
if (!exercise.current_progression_id) exercise.current_progression_id = progression.id;
|
||||
markDirty();
|
||||
renderAll();
|
||||
} else if (exerciseRow && button.dataset.removeExercise !== undefined) {
|
||||
const id = exerciseRow.dataset.exerciseId;
|
||||
const used = state.cfg.days.some((day) => (day.exercises || []).some((item) => item.exercise_id === id));
|
||||
if (used && !confirm("Übung ist in Tagen hinterlegt. Trotzdem entfernen?")) return;
|
||||
@@ -509,6 +602,24 @@ document.addEventListener("click", async (event) => {
|
||||
markDirty();
|
||||
renderAll();
|
||||
}
|
||||
if (exerciseRow) {
|
||||
const exercise = state.cfg.exercises.find((item) => item.id === exerciseRow.dataset.exerciseId);
|
||||
const progressionRow = button.closest("[data-progression-id]");
|
||||
if (exercise && progressionRow) {
|
||||
exercise.progressions = exercise.progressions || [];
|
||||
const progressionIndex = exercise.progressions.findIndex((item) => item.id === progressionRow.dataset.progressionId);
|
||||
if (button.dataset.moveProgression) {
|
||||
move(exercise.progressions, progressionIndex, Number(button.dataset.moveProgression));
|
||||
} else if (button.dataset.removeProgression !== undefined && progressionIndex >= 0) {
|
||||
const [removed] = exercise.progressions.splice(progressionIndex, 1);
|
||||
if (removed?.id === exercise.current_progression_id) {
|
||||
exercise.current_progression_id = exercise.progressions[0]?.id || "";
|
||||
}
|
||||
markDirty();
|
||||
renderAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toast(err.message, "err");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user