|
@@ -0,0 +1,327 @@
|
|
|
|
|
+<script>
|
|
|
|
|
+ import logoMark from '$lib/images/logo_white.svg';
|
|
|
|
|
+
|
|
|
|
|
+ let { data } = $props();
|
|
|
|
|
+
|
|
|
|
|
+ const EXERCISE_TYPES = [
|
|
|
|
|
+ { value: 'weights', label: 'Weights' },
|
|
|
|
|
+ { value: 'bodyweight', label: 'Bodyweight' },
|
|
|
|
|
+ { value: 'timed', label: 'Timed' },
|
|
|
|
|
+ { value: 'distance', label: 'Distance' },
|
|
|
|
|
+ { value: 'stretch', label: 'Stretch' }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ let nextKey = 1;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param {{ name?: string, type?: string }} [exercise]
|
|
|
|
|
+ */
|
|
|
|
|
+ function toRow(exercise = {}) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ key: nextKey++,
|
|
|
|
|
+ name: String(exercise.name ?? ''),
|
|
|
|
|
+ type: String(exercise.type ?? 'weights')
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let exercises = $state(data.workout.exercises.map((exercise) => toRow(exercise)));
|
|
|
|
|
+ if (exercises.length === 0) {
|
|
|
|
|
+ exercises = [toRow()];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function addExercise() {
|
|
|
|
|
+ exercises = [...exercises, toRow()];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param {number} key
|
|
|
|
|
+ */
|
|
|
|
|
+ function removeExercise(key) {
|
|
|
|
|
+ if (exercises.length <= 1) return;
|
|
|
|
|
+ exercises = exercises.filter((exercise) => exercise.key !== key);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @param {number} index
|
|
|
|
|
+ * @param {-1 | 1} direction
|
|
|
|
|
+ */
|
|
|
|
|
+ function moveExercise(index, direction) {
|
|
|
|
|
+ const target = index + direction;
|
|
|
|
|
+ if (target < 0 || target >= exercises.length) return;
|
|
|
|
|
+
|
|
|
|
|
+ const next = [...exercises];
|
|
|
|
|
+ const [item] = next.splice(index, 1);
|
|
|
|
|
+ next.splice(target, 0, item);
|
|
|
|
|
+ exercises = next;
|
|
|
|
|
+ }
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<svelte:head>
|
|
|
|
|
+ <title>Edit {data.workout.title} — Torus</title>
|
|
|
|
|
+</svelte:head>
|
|
|
|
|
+
|
|
|
|
|
+<div class="page">
|
|
|
|
|
+ <header class="top">
|
|
|
|
|
+ <a class="brand" href="/">
|
|
|
|
|
+ <img class="brand-mark" src={logoMark} alt="" width="28" height="28" />
|
|
|
|
|
+ <span class="brand-name">Torus</span>
|
|
|
|
|
+ </a>
|
|
|
|
|
+ <div class="top-actions">
|
|
|
|
|
+ <a class="btn btn-ghost" href="/workout/{data.workout.id}">Back</a>
|
|
|
|
|
+ <a class="btn btn-ghost" href="/logout">Log out</a>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+
|
|
|
|
|
+ <main>
|
|
|
|
|
+ <div class="page-head">
|
|
|
|
|
+ <h1>Edit workout</h1>
|
|
|
|
|
+ <p class="lede">{data.workout.title}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="exercises-head">
|
|
|
|
|
+ <h2>Exercises</h2>
|
|
|
|
|
+ <button type="button" class="btn btn-primary" onclick={addExercise}>Add exercise</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <ul class="exercise-list" role="list">
|
|
|
|
|
+ {#each exercises as exercise, index (exercise.key)}
|
|
|
|
|
+ <li class="exercise-row">
|
|
|
|
|
+ <p class="exercise-index">Exercise {index + 1}</p>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="exercise-fields">
|
|
|
|
|
+ <label class="field field-name">
|
|
|
|
|
+ <span class="label">Name</span>
|
|
|
|
|
+ <input
|
|
|
|
|
+ class="input"
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ required
|
|
|
|
|
+ placeholder="Bench press"
|
|
|
|
|
+ bind:value={exercise.name}
|
|
|
|
|
+ />
|
|
|
|
|
+ </label>
|
|
|
|
|
+
|
|
|
|
|
+ <label class="field field-type">
|
|
|
|
|
+ <span class="label">Type</span>
|
|
|
|
|
+ <select class="input select" bind:value={exercise.type}>
|
|
|
|
|
+ {#each EXERCISE_TYPES as option}
|
|
|
|
|
+ <option value={option.value}>{option.label}</option>
|
|
|
|
|
+ {/each}
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="exercise-actions">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="btn btn-ghost"
|
|
|
|
|
+ disabled={index === 0}
|
|
|
|
|
+ onclick={() => moveExercise(index, -1)}
|
|
|
|
|
+ >
|
|
|
|
|
+ Move up
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="btn btn-ghost"
|
|
|
|
|
+ disabled={index === exercises.length - 1}
|
|
|
|
|
+ onclick={() => moveExercise(index, 1)}
|
|
|
|
|
+ >
|
|
|
|
|
+ Move down
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="btn btn-ghost"
|
|
|
|
|
+ disabled={exercises.length <= 1}
|
|
|
|
|
+ onclick={() => removeExercise(exercise.key)}
|
|
|
|
|
+ >
|
|
|
|
|
+ Delete
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ {/each}
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </main>
|
|
|
|
|
+</div>
|
|
|
|
|
+
|
|
|
|
|
+<style>
|
|
|
|
|
+ .page {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .top {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .top-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .brand {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: -0.02em;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .brand-mark {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .brand-name {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ main {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ width: min(720px, 100%);
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ padding: 40px 20px 64px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .page-head {
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ h1 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 1.6rem;
|
|
|
|
|
+ letter-spacing: -0.03em;
|
|
|
|
|
+ font-weight: 750;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ h2 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-size: 1rem;
|
|
|
|
|
+ letter-spacing: -0.01em;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .lede {
|
|
|
|
|
+ margin: 8px 0 0;
|
|
|
|
|
+ color: var(--text-muted);
|
|
|
|
|
+ line-height: 1.45;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercises-head {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercise-list {
|
|
|
|
|
+ list-style: none;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercise-row {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ padding: 14px;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: var(--radius);
|
|
|
|
|
+ background: var(--surface-2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercise-index {
|
|
|
|
|
+ margin: 0 0 10px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 0.04em;
|
|
|
|
|
+ text-transform: uppercase;
|
|
|
|
|
+ color: var(--text-muted);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercise-fields {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .field {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .label {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 650;
|
|
|
|
|
+ color: var(--text-muted);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .input {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding: 11px 12px;
|
|
|
|
|
+ border: 1px solid var(--border);
|
|
|
|
|
+ border-radius: var(--radius-sm);
|
|
|
|
|
+ background: var(--surface-3);
|
|
|
|
|
+ color: var(--text);
|
|
|
|
|
+ font: inherit;
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .select {
|
|
|
|
|
+ appearance: none;
|
|
|
|
|
+ background-image: linear-gradient(45deg, transparent 50%, var(--ash) 50%),
|
|
|
|
|
+ linear-gradient(135deg, var(--ash) 50%, transparent 50%);
|
|
|
|
|
+ background-position:
|
|
|
|
|
+ calc(100% - 16px) calc(50% - 3px),
|
|
|
|
|
+ calc(100% - 11px) calc(50% - 3px);
|
|
|
|
|
+ background-size: 5px 5px;
|
|
|
|
|
+ background-repeat: no-repeat;
|
|
|
|
|
+ padding-right: 32px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .input:hover {
|
|
|
|
|
+ border-color: color-mix(in srgb, var(--border) 70%, var(--ash));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .input:focus {
|
|
|
|
|
+ outline: 2px solid var(--focus);
|
|
|
|
|
+ outline-offset: 2px;
|
|
|
|
|
+ border-color: color-mix(in srgb, var(--ember) 45%, var(--border));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercise-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercise-actions .btn:disabled {
|
|
|
|
|
+ opacity: 0.45;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @media (min-width: 720px) {
|
|
|
|
|
+ .top {
|
|
|
|
|
+ padding: 16px 28px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ main {
|
|
|
|
|
+ padding: 48px 28px 80px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .exercise-fields {
|
|
|
|
|
+ grid-template-columns: minmax(0, 1fr) 180px;
|
|
|
|
|
+ align-items: end;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+</style>
|