fitness: GPS workout templates with interval pre-selection

Enable creating templates for GPS-tracked workouts with activity type
and optional interval training. GPS templates show activity/interval
info instead of exercise lists in cards, modals, and schedule. Starting
a GPS template pre-selects the interval and jumps to the map screen.
This commit is contained in:
2026-03-30 13:24:52 +02:00
parent bdaae6d7dc
commit f649b94e9d
8 changed files with 382 additions and 52 deletions
+23 -11
View File
@@ -1,6 +1,6 @@
<script>
import { getExerciseById } from '$lib/data/exercises';
import { EllipsisVertical } from 'lucide-svelte';
import { EllipsisVertical, MapPin } from 'lucide-svelte';
import { page } from '$app/stores';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
@@ -8,7 +8,7 @@
/**
* @type {{
* template: { _id: string, name: string, exercises: Array<{ exerciseId: string, sets: any[] }> },
* template: { _id: string, name: string, mode?: string, activityType?: string, exercises: Array<{ exerciseId: string, sets: any[] }> },
* lastUsed?: string | null,
* onStart?: (() => void) | null,
* onMenu?: ((e: MouseEvent) => void) | null
@@ -42,15 +42,19 @@
</button>
{/if}
</div>
<ul class="exercise-preview">
{#each template.exercises.slice(0, 4) as ex}
{@const exercise = getExerciseById(ex.exerciseId, lang)}
<li>{ex.sets.length} &times; {exercise?.localName ?? ex.exerciseId}</li>
{/each}
{#if template.exercises.length > 4}
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>
{/if}
</ul>
{#if template.mode === 'gps'}
<div class="gps-badge"><MapPin size={12} /> {template.activityType?.[0]?.toUpperCase()}{template.activityType?.slice(1) ?? 'Running'}</div>
{:else}
<ul class="exercise-preview">
{#each template.exercises.slice(0, 4) as ex}
{@const exercise = getExerciseById(ex.exerciseId, lang)}
<li>{ex.sets.length} &times; {exercise?.localName ?? ex.exerciseId}</li>
{/each}
{#if template.exercises.length > 4}
<li class="more">+{template.exercises.length - 4} {t('more', lang)}</li>
{/if}
</ul>
{/if}
{#if lastUsed}
<p class="last-used">{t('last_performed', lang)} {formatDate(lastUsed)}</p>
{/if}
@@ -115,6 +119,14 @@
color: var(--color-primary);
font-style: italic;
}
.gps-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.8rem;
color: var(--nord10);
font-weight: 600;
}
.last-used {
margin: 0.5rem 0 0;
font-size: 0.75rem;
+28
View File
@@ -43,6 +43,7 @@ export interface StoredState {
activityType: GpsActivityType | null;
name: string;
templateId: string | null;
intervalTemplateId: string | null;
exercises: WorkoutExercise[];
elapsed: number; // total elapsed seconds at time of save
savedAt: number; // Date.now() at time of save
@@ -57,6 +58,7 @@ export interface RemoteState {
mode: WorkoutMode;
activityType: GpsActivityType | null;
templateId: string | null;
intervalTemplateId: string | null;
exercises: WorkoutExercise[];
paused: boolean;
elapsed: number;
@@ -100,6 +102,7 @@ export function createWorkout() {
let activityType = $state<GpsActivityType | null>(null);
let name = $state('');
let templateId: string | null = $state(null);
let intervalTemplateId: string | null = $state(null);
let exercises = $state<WorkoutExercise[]>([]);
let startTime: Date | null = $state(null);
let _pausedElapsed = $state(0); // seconds accumulated before current run
@@ -128,6 +131,7 @@ export function createWorkout() {
activityType,
name,
templateId,
intervalTemplateId,
exercises: JSON.parse(JSON.stringify(exercises)),
elapsed: _elapsed,
savedAt: Date.now(),
@@ -197,6 +201,7 @@ export function createWorkout() {
activityType = stored.activityType ?? null;
name = stored.name;
templateId = stored.templateId;
intervalTemplateId = stored.intervalTemplateId ?? null;
exercises = stored.exercises;
if (stored.paused) {
@@ -233,6 +238,7 @@ export function createWorkout() {
function startFromTemplate(template: TemplateData) {
name = template.name;
templateId = template._id;
intervalTemplateId = null;
mode = 'manual';
exercises = template.exercises.map((e) => ({
exerciseId: e.exerciseId,
@@ -260,6 +266,7 @@ export function createWorkout() {
function startEmpty() {
name = 'Quick Workout';
templateId = null;
intervalTemplateId = null;
mode = 'manual';
exercises = [];
startTime = new Date();
@@ -280,6 +287,7 @@ export function createWorkout() {
};
name = labels[activity];
templateId = null;
intervalTemplateId = null;
mode = 'gps';
activityType = activity;
exercises = [];
@@ -291,6 +299,21 @@ export function createWorkout() {
_persist();
}
function startFromGpsTemplate(template: { _id: string; name: string; activityType?: string; intervalTemplateId?: string }) {
name = template.name;
templateId = template._id;
intervalTemplateId = template.intervalTemplateId ?? null;
mode = 'gps';
activityType = (template.activityType as GpsActivityType) ?? 'running';
exercises = [];
startTime = null;
_pausedElapsed = 0;
_elapsed = 0;
paused = true;
active = true;
_persist();
}
function pauseTimer() {
if (!active || paused) return;
_computeElapsed();
@@ -450,6 +473,7 @@ export function createWorkout() {
activityType = null;
name = '';
templateId = null;
intervalTemplateId = null;
exercises = [];
startTime = null;
_pausedElapsed = 0;
@@ -469,6 +493,7 @@ export function createWorkout() {
mode = remote.mode ?? 'manual';
activityType = remote.activityType ?? null;
templateId = remote.templateId;
intervalTemplateId = remote.intervalTemplateId ?? null;
exercises = remote.exercises;
if (remote.paused) {
@@ -515,6 +540,7 @@ export function createWorkout() {
activityType,
name,
templateId,
intervalTemplateId,
exercises: JSON.parse(JSON.stringify(exercises)),
elapsed: _elapsed,
savedAt: Date.now(),
@@ -544,6 +570,7 @@ export function createWorkout() {
get name() { return name; },
set name(v: string) { name = v; _persist(); },
get templateId() { return templateId; },
get intervalTemplateId() { return intervalTemplateId; },
get exercises() { return exercises; },
get startTime() { return startTime; },
get elapsedSeconds() { return _elapsed; },
@@ -557,6 +584,7 @@ export function createWorkout() {
startFromTemplate,
startEmpty,
startGpsWorkout,
startFromGpsTemplate,
pauseTimer,
resumeTimer,
addExercise,
+4
View File
@@ -17,6 +17,7 @@ interface ServerWorkout {
mode: WorkoutMode;
activityType: GpsActivityType | null;
templateId: string | null;
intervalTemplateId: string | null;
exercises: WorkoutExercise[];
paused: boolean;
elapsed: number;
@@ -47,6 +48,7 @@ export function createWorkoutSync() {
mode: workout.mode,
activityType: workout.activityType,
templateId: workout.templateId,
intervalTemplateId: workout.intervalTemplateId,
exercises: JSON.parse(JSON.stringify(workout.exercises)),
paused: workout.paused,
elapsed,
@@ -114,6 +116,7 @@ export function createWorkoutSync() {
mode: doc.mode ?? 'manual',
activityType: doc.activityType ?? null,
templateId: doc.templateId,
intervalTemplateId: doc.intervalTemplateId ?? null,
exercises: doc.exercises,
paused: doc.paused,
elapsed: doc.elapsed,
@@ -234,6 +237,7 @@ export function createWorkoutSync() {
mode: serverDoc.mode ?? 'manual',
activityType: serverDoc.activityType ?? null,
templateId: serverDoc.templateId,
intervalTemplateId: serverDoc.intervalTemplateId ?? null,
exercises: serverDoc.exercises,
paused: serverDoc.paused,
elapsed: serverDoc.elapsed,