Forráskód Böngészése

Create page for editing workouts

Lee Morgan 4 napja
szülő
commit
0fb547d33f

+ 19 - 0
src/routes/workout/[workoutId]/edit/+page.server.js

@@ -0,0 +1,19 @@
+import { error } from '@sveltejs/kit';
+import { requireUser } from '$lib/server/auth.js';
+import { getWorkoutForUser } from '$lib/server/workouts.js';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load(event) {
+	const user = await requireUser(event);
+	const workout = await getWorkoutForUser(
+		event.locals.db,
+		user.id,
+		event.params.workoutId
+	);
+
+	if (!workout) {
+		error(404, 'Workout not found');
+	}
+
+	return { user, workout };
+}

+ 327 - 0
src/routes/workout/[workoutId]/edit/+page.svelte

@@ -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>