Преглед на файлове

Create page for new workout creation.

Lee Morgan преди 4 дни
родител
ревизия
5d3839a3ba
променени са 3 файла, в които са добавени 381 реда и са изтрити 2 реда
  1. 35 2
      src/routes/workouts/+page.svelte
  2. 7 0
      src/routes/workouts/new/+page.server.js
  3. 339 0
      src/routes/workouts/new/+page.svelte

+ 35 - 2
src/routes/workouts/+page.svelte

@@ -19,8 +19,13 @@
 
 
 	<main>
 	<main>
 		<section class="panel">
 		<section class="panel">
-			<h1>Workouts</h1>
-			<p class="lede">Signed in as {data.user.name}. Your sessions will show up here.</p>
+			<div class="panel-head">
+				<div class="panel-copy">
+					<h1>Workouts</h1>
+					<p class="lede">Signed in as {data.user.name}. Your sessions will show up here.</p>
+				</div>
+				<a class="btn btn-primary new-workout" href="/workouts/new">New workout</a>
+			</div>
 		</section>
 		</section>
 	</main>
 	</main>
 </div>
 </div>
@@ -74,6 +79,22 @@
 		padding: 24px;
 		padding: 24px;
 	}
 	}
 
 
+	.panel-head {
+		display: flex;
+		flex-direction: column;
+		align-items: flex-start;
+		gap: 16px;
+	}
+
+	.panel-copy {
+		min-width: 0;
+		width: 100%;
+	}
+
+	.new-workout {
+		flex-shrink: 0;
+	}
+
 	h1 {
 	h1 {
 		margin: 0;
 		margin: 0;
 		font-size: 1.6rem;
 		font-size: 1.6rem;
@@ -95,5 +116,17 @@
 		main {
 		main {
 			padding: 48px 28px 80px;
 			padding: 48px 28px 80px;
 		}
 		}
+
+		.panel-head {
+			flex-direction: row;
+			align-items: center;
+			justify-content: space-between;
+			gap: 20px;
+		}
+
+		.panel-copy {
+			width: auto;
+			flex: 1;
+		}
 	}
 	}
 </style>
 </style>

+ 7 - 0
src/routes/workouts/new/+page.server.js

@@ -0,0 +1,7 @@
+import { requireUser } from '$lib/server/auth.js';
+
+/** @type {import('./$types').PageServerLoad} */
+export async function load(event) {
+	const user = await requireUser(event);
+	return { user };
+}

+ 339 - 0
src/routes/workouts/new/+page.svelte

@@ -0,0 +1,339 @@
+<script>
+	import logoMark from '$lib/images/logo_white.svg';
+
+	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;
+
+	/** @returns {{ key: number, name: string, type: string }} */
+	function emptyExercise() {
+		return { key: nextKey++, name: '', type: 'weights' };
+	}
+
+	let title = $state('');
+	let exercises = $state([emptyExercise()]);
+
+	function addExercise() {
+		exercises = [...exercises, emptyExercise()];
+	}
+
+	/**
+	 * @param {number} key
+	 */
+	function removeExercise(key) {
+		if (exercises.length <= 1) return;
+		exercises = exercises.filter((exercise) => exercise.key !== key);
+	}
+
+	/**
+	 * @param {SubmitEvent} event
+	 */
+	function handleSubmit(event) {
+		event.preventDefault();
+	}
+</script>
+
+<svelte:head>
+	<title>New workout — 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="/workouts">Back</a>
+			<a class="btn btn-ghost" href="/logout">Log out</a>
+		</div>
+	</header>
+
+	<main>
+		<section class="panel" aria-labelledby="new-workout-title">
+			<div class="panel-head">
+				<h1 id="new-workout-title">New workout</h1>
+				<p class="lede">Name the session, then add as many exercises as you need.</p>
+			</div>
+
+			<form class="form" method="POST" onsubmit={handleSubmit}>
+				<label class="field">
+					<span class="label">Workout name</span>
+					<input
+						class="input"
+						type="text"
+						name="title"
+						required
+						placeholder="Push A"
+						bind:value={title}
+					/>
+				</label>
+
+				<div class="exercises" aria-label="Exercises">
+					<div class="exercises-head">
+						<h2>Exercises</h2>
+						<button type="button" class="btn btn-ghost" 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"
+											name="exerciseName"
+											required
+											placeholder="Bench press"
+											bind:value={exercise.name}
+										/>
+									</label>
+
+									<label class="field field-type">
+										<span class="label">Type</span>
+										<select class="input select" name="exerciseType" bind:value={exercise.type}>
+											{#each EXERCISE_TYPES as option}
+												<option value={option.value}>{option.label}</option>
+											{/each}
+										</select>
+									</label>
+
+									<button
+										type="button"
+										class="btn btn-ghost remove"
+										disabled={exercises.length <= 1}
+										onclick={() => removeExercise(exercise.key)}
+									>
+										Remove
+									</button>
+								</div>
+							</li>
+						{/each}
+					</ul>
+				</div>
+
+				<div class="form-actions">
+					<a class="btn btn-ghost" href="/workouts">Cancel</a>
+					<button class="btn btn-primary" type="submit">Create workout</button>
+				</div>
+			</form>
+		</section>
+	</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;
+	}
+
+	.panel {
+		background: var(--surface);
+		border: 1px solid var(--border);
+		border-radius: var(--radius-lg);
+		padding: 24px;
+	}
+
+	.panel-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;
+	}
+
+	.form {
+		display: grid;
+		gap: 24px;
+	}
+
+	.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));
+	}
+
+	.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: grid;
+		gap: 12px;
+	}
+
+	.exercise-row {
+		background: var(--surface-2);
+		border: 1px solid var(--border);
+		border-radius: var(--radius);
+		padding: 14px;
+	}
+
+	.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;
+	}
+
+	.remove {
+		justify-self: start;
+	}
+
+	.remove:disabled {
+		opacity: 0.45;
+		cursor: not-allowed;
+	}
+
+	.form-actions {
+		display: flex;
+		flex-wrap: wrap;
+		justify-content: flex-end;
+		gap: 10px;
+		padding-top: 4px;
+	}
+
+	@media (min-width: 720px) {
+		.top {
+			padding: 16px 28px;
+		}
+
+		main {
+			padding: 48px 28px 80px;
+		}
+
+		.panel {
+			padding: 28px;
+		}
+
+		.exercise-fields {
+			grid-template-columns: minmax(0, 1fr) 180px auto;
+			align-items: end;
+		}
+
+		.remove {
+			justify-self: stretch;
+		}
+	}
+</style>