From 9527c253ed50e43a580a8b96b8e2619bef8045c6 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Sat, 11 Apr 2026 15:01:30 +0200 Subject: [PATCH] feat: add template library for browsing and adding defaults Replace auto-seed with a browsable template library. Users can selectively add built-in templates to their collection via a BookOpen icon or the empty-state prompt. Each library template tracks its origin via libraryId to prevent duplicates. - Extract default templates to shared $lib/data/defaultTemplates.ts - Add GET/POST /api/fitness/templates/library endpoint - Add library modal with add/added state per template - Keep seed endpoint as fallback (imports from shared data) --- package.json | 2 +- src/lib/data/defaultTemplates.ts | 91 ++++ src/lib/js/fitnessI18n.ts | 6 +- src/models/WorkoutTemplate.ts | 5 + .../api/fitness/templates/library/+server.ts | 58 +++ .../api/fitness/templates/seed/+server.ts | 406 +----------------- .../[workout=fitnessWorkout]/+page.svelte | 183 +++++++- 7 files changed, 333 insertions(+), 418 deletions(-) create mode 100644 src/lib/data/defaultTemplates.ts create mode 100644 src/routes/api/fitness/templates/library/+server.ts diff --git a/package.json b/package.json index 1e4e8d2..fa510b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.24.1", + "version": "1.25.0", "private": true, "type": "module", "scripts": { diff --git a/src/lib/data/defaultTemplates.ts b/src/lib/data/defaultTemplates.ts new file mode 100644 index 0000000..f86936d --- /dev/null +++ b/src/lib/data/defaultTemplates.ts @@ -0,0 +1,91 @@ +/** + * Built-in workout template library. + * Each template has a stable `id` for dedup (so users can't add the same one twice). + */ +export const defaultTemplates = [ + { + id: 'default-pull', + name: 'Day 1 - Pull', + description: 'Back, biceps, and shoulders pull day', + exercises: [ + { exerciseId: 'bent-over-row-barbell', sets: [{ reps: 10, weight: 60, rpe: 7 }, { reps: 10, weight: 60, rpe: 8 }, { reps: 10, weight: 60, rpe: 9 }], restTime: 120 }, + { exerciseId: 'pull-up', sets: [{ reps: 6, rpe: 8 }, { reps: 6, rpe: 8 }, { reps: 6, rpe: 9 }], restTime: 120 }, + { exerciseId: 'incline-row-dumbbell', sets: [{ reps: 12, weight: 16, rpe: 7 }, { reps: 12, weight: 16, rpe: 8 }], restTime: 90 }, + { exerciseId: 'upright-row-barbell', sets: [{ reps: 12, weight: 30, rpe: 7 }, { reps: 12, weight: 30, rpe: 8 }], restTime: 90 }, + { exerciseId: 'decline-crunch', sets: [{ reps: 15, rpe: 7 }, { reps: 15, rpe: 8 }], restTime: 60 }, + { exerciseId: 'lateral-raise-dumbbell', sets: [{ reps: 15, weight: 10, rpe: 7 }, { reps: 15, weight: 10, rpe: 8 }], restTime: 90 }, + { exerciseId: 'front-raise-dumbbell', sets: [{ reps: 10, weight: 10, rpe: 7 }, { reps: 10, weight: 10, rpe: 8 }], restTime: 90 }, + ] + }, + { + id: 'default-push', + name: 'Day 2 - Push', + description: 'Chest, triceps, and biceps push day', + exercises: [ + { exerciseId: 'bench-press-barbell', sets: [{ reps: 8, weight: 80, rpe: 7 }, { reps: 8, weight: 80, rpe: 8 }, { reps: 8, weight: 80, rpe: 9 }], restTime: 120 }, + { exerciseId: 'incline-bench-press-barbell', sets: [{ reps: 10, weight: 60, rpe: 7 }, { reps: 10, weight: 60, rpe: 8 }], restTime: 120 }, + { exerciseId: 'skullcrusher-dumbbell', sets: [{ reps: 15, weight: 15, rpe: 7 }, { reps: 15, weight: 15, rpe: 8 }], restTime: 90 }, + { exerciseId: 'bench-press-close-grip-barbell', sets: [{ reps: 10, weight: 60, rpe: 7 }, { reps: 10, weight: 60, rpe: 8 }], restTime: 120 }, + { exerciseId: 'hammer-curl-dumbbell', sets: [{ reps: 15, weight: 12, rpe: 7 }, { reps: 15, weight: 12, rpe: 8 }], restTime: 90 }, + { exerciseId: 'bicep-curl-dumbbell', sets: [{ reps: 15, weight: 10, rpe: 7 }, { reps: 15, weight: 10, rpe: 8 }], restTime: 90 }, + ] + }, + { + id: 'default-legs', + name: 'Day 3 - Legs', + description: 'Quad, hamstring, and calf focused leg day', + exercises: [ + { exerciseId: 'squat-barbell', sets: [{ reps: 8, weight: 80, rpe: 7 }, { reps: 8, weight: 80, rpe: 8 }, { reps: 8, weight: 80, rpe: 9 }], restTime: 150 }, + { exerciseId: 'romanian-deadlift-barbell', sets: [{ reps: 10, weight: 70, rpe: 7 }, { reps: 10, weight: 70, rpe: 8 }, { reps: 10, weight: 70, rpe: 9 }], restTime: 120 }, + { exerciseId: 'bulgarian-split-squat-dumbbell', sets: [{ reps: 10, weight: 16, rpe: 7 }, { reps: 10, weight: 16, rpe: 8 }], restTime: 120 }, + { exerciseId: 'calf-raise-standing', sets: [{ reps: 15, rpe: 7 }, { reps: 15, rpe: 8 }, { reps: 15, rpe: 9 }], restTime: 60 }, + ] + }, + { + id: 'default-upper', + name: 'Day 4 - Upper', + description: 'Full upper body day — shoulders, chest, back, and core', + exercises: [ + { exerciseId: 'overhead-press-barbell', sets: [{ reps: 8, weight: 40, rpe: 7 }, { reps: 8, weight: 40, rpe: 8 }], restTime: 120 }, + { exerciseId: 'bench-press-dumbbell', sets: [{ reps: 10, weight: 28, rpe: 7 }, { reps: 10, weight: 28, rpe: 8 }], restTime: 120 }, + { exerciseId: 'chin-up', sets: [{ reps: 6, rpe: 8 }, { reps: 6, rpe: 9 }], restTime: 120 }, + { exerciseId: 'bench-press-close-grip-barbell', sets: [{ reps: 10, weight: 60, rpe: 7 }, { reps: 10, weight: 60, rpe: 8 }], restTime: 120 }, + { exerciseId: 'decline-crunch', sets: [{ reps: 15, rpe: 7 }, { reps: 15, rpe: 8 }], restTime: 60 }, + { exerciseId: 'flat-leg-raise', sets: [{ reps: 15, rpe: 7 }, { reps: 15, rpe: 8 }], restTime: 60 }, + ] + }, + { + id: 'default-lower', + name: 'Day 5 - Lower', + description: 'Glute, quad, and hamstring focused lower day', + exercises: [ + { exerciseId: 'front-squat-barbell', sets: [{ reps: 8, weight: 60, rpe: 7 }, { reps: 8, weight: 60, rpe: 8 }, { reps: 8, weight: 60, rpe: 9 }], restTime: 150 }, + { exerciseId: 'romanian-deadlift-dumbbell', sets: [{ reps: 10, weight: 24, rpe: 7 }, { reps: 10, weight: 24, rpe: 8 }], restTime: 120 }, + { exerciseId: 'hip-thrust-barbell', sets: [{ reps: 10, weight: 60, rpe: 7 }, { reps: 10, weight: 60, rpe: 8 }], restTime: 120 }, + { exerciseId: 'goblet-squat-dumbbell', sets: [{ reps: 12, weight: 20, rpe: 7 }, { reps: 12, weight: 20, rpe: 8 }], restTime: 90 }, + { exerciseId: 'calf-raise-standing', sets: [{ reps: 15, rpe: 7 }, { reps: 15, rpe: 8 }], restTime: 60 }, + ] + }, + { + id: 'default-stretching', + name: 'Day 6 - Stretching', + description: 'Full-body stretching — all muscle groups, no equipment (~30 min)', + exercises: [ + { exerciseId: 'neck-circle-stretch', sets: [{ duration: 1.5 }, { duration: 1.5 }], restTime: 15 }, + { exerciseId: 'side-push-neck-stretch', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'seated-shoulder-flexor-stretch-bent-knee', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'shoulder-stretch-behind-back', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'elbows-back-stretch', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'back-pec-stretch', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'cow-stretch', sets: [{ duration: 1.5 }, { duration: 1.5 }], restTime: 15 }, + { exerciseId: 'thoracic-bridge', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'butterfly-yoga-pose', sets: [{ duration: 1.5 }, { duration: 1.5 }], restTime: 15 }, + { exerciseId: 'seated-single-leg-hamstring-stretch', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'kneeling-toe-up-hamstring-stretch', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'side-lunge-stretch', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'lying-lower-back-stretch', sets: [{ duration: 1.5 }, { duration: 1.5 }], restTime: 15 }, + { exerciseId: 'calf-stretch-wall', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + { exerciseId: 'elbow-flexor-stretch', sets: [{ duration: 1 }, { duration: 1 }], restTime: 15 }, + ] + } +]; diff --git a/src/lib/js/fitnessI18n.ts b/src/lib/js/fitnessI18n.ts index 8457698..abec5ce 100644 --- a/src/lib/js/fitnessI18n.ts +++ b/src/lib/js/fitnessI18n.ts @@ -115,7 +115,11 @@ const translations: Translations = { templates: { en: 'Templates', de: 'Vorlagen' }, schedule: { en: 'Schedule', de: 'Zeitplan' }, my_templates: { en: 'My Templates', de: 'Meine Vorlagen' }, - no_templates_yet: { en: 'No templates yet. Create one or start an empty workout.', de: 'Noch keine Vorlagen. Erstelle eine oder starte ein leeres Training.' }, + no_templates_yet: { en: 'No templates yet. Browse the library or create your own.', de: 'Noch keine Vorlagen. Stöbere in der Bibliothek oder erstelle deine eigene.' }, + template_library: { en: 'Template Library', de: 'Vorlagen-Bibliothek' }, + browse_library: { en: 'Browse Library', de: 'Bibliothek durchsuchen' }, + template_added: { en: 'Template added', de: 'Vorlage hinzugefügt' }, + loading: { en: 'Loading', de: 'Laden' }, edit_template: { en: 'Edit Template', de: 'Vorlage bearbeiten' }, new_template: { en: 'New Template', de: 'Neue Vorlage' }, template_name_placeholder: { en: 'Template name', de: 'Vorlagenname' }, diff --git a/src/models/WorkoutTemplate.ts b/src/models/WorkoutTemplate.ts index ec0fecd..bf2a8a3 100644 --- a/src/models/WorkoutTemplate.ts +++ b/src/models/WorkoutTemplate.ts @@ -25,6 +25,7 @@ export interface IWorkoutTemplate { exercises: IExercise[]; createdBy: string; // username/nickname of the person who created the template isPublic?: boolean; // whether other users can see/use this template + libraryId?: string; // tracks which built-in library template this was created from createdAt?: Date; updatedAt?: Date; } @@ -126,6 +127,10 @@ const WorkoutTemplateSchema = new mongoose.Schema( isPublic: { type: Boolean, default: false + }, + libraryId: { + type: String, + default: undefined } }, { diff --git a/src/routes/api/fitness/templates/library/+server.ts b/src/routes/api/fitness/templates/library/+server.ts new file mode 100644 index 0000000..0b62adf --- /dev/null +++ b/src/routes/api/fitness/templates/library/+server.ts @@ -0,0 +1,58 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { requireAuth } from '$lib/server/middleware/auth'; +import { dbConnect } from '$utils/db'; +import { WorkoutTemplate } from '$models/WorkoutTemplate'; +import { defaultTemplates } from '$lib/data/defaultTemplates'; + +// GET /api/fitness/templates/library — list available library templates +// Returns each template with an `added` flag if user already has it +export const GET: RequestHandler = async ({ locals }) => { + const user = await requireAuth(locals); + await dbConnect(); + + // Find which library templates the user already saved (by libraryId) + const existing = await WorkoutTemplate.find( + { createdBy: user.nickname, libraryId: { $exists: true, $ne: null } }, + { libraryId: 1 } + ).lean(); + const addedIds = new Set(existing.map((t: any) => t.libraryId)); + + const library = defaultTemplates.map((t) => ({ + ...t, + added: addedIds.has(t.id), + })); + + return json({ templates: library }); +}; + +// POST /api/fitness/templates/library — add a library template to user's collection +export const POST: RequestHandler = async ({ request, locals }) => { + const user = await requireAuth(locals); + await dbConnect(); + + const { id } = await request.json(); + const libTemplate = defaultTemplates.find((t) => t.id === id); + if (!libTemplate) { + return json({ error: 'Template not found in library' }, { status: 404 }); + } + + // Prevent duplicates + const exists = await WorkoutTemplate.exists({ + createdBy: user.nickname, + libraryId: id + }); + if (exists) { + return json({ error: 'Template already added' }, { status: 409 }); + } + + const template = await WorkoutTemplate.create({ + name: libTemplate.name, + description: libTemplate.description, + exercises: libTemplate.exercises, + libraryId: libTemplate.id, + createdBy: user.nickname, + }); + + return json({ template }, { status: 201 }); +}; diff --git a/src/routes/api/fitness/templates/seed/+server.ts b/src/routes/api/fitness/templates/seed/+server.ts index 3f89b5d..0cf34c5 100644 --- a/src/routes/api/fitness/templates/seed/+server.ts +++ b/src/routes/api/fitness/templates/seed/+server.ts @@ -3,411 +3,7 @@ import type { RequestHandler } from './$types'; import { requireAuth } from '$lib/server/middleware/auth'; import { dbConnect } from '$utils/db'; import { WorkoutTemplate } from '$models/WorkoutTemplate'; - -const defaultTemplates = [ - { - name: 'Day 1 - Pull', - description: 'Back, biceps, and shoulders pull day', - exercises: [ - { - exerciseId: 'bent-over-row-barbell', - sets: [ - { reps: 10, weight: 60, rpe: 7 }, - { reps: 10, weight: 60, rpe: 8 }, - { reps: 10, weight: 60, rpe: 9 } - ], - restTime: 120 - }, - { - exerciseId: 'pull-up', - sets: [ - { reps: 6, rpe: 8 }, - { reps: 6, rpe: 8 }, - { reps: 6, rpe: 9 } - ], - restTime: 120 - }, - { - exerciseId: 'incline-row-dumbbell', - sets: [ - { reps: 12, weight: 16, rpe: 7 }, - { reps: 12, weight: 16, rpe: 8 } - ], - restTime: 90 - }, - { - exerciseId: 'upright-row-barbell', - sets: [ - { reps: 12, weight: 30, rpe: 7 }, - { reps: 12, weight: 30, rpe: 8 } - ], - restTime: 90 - }, - { - exerciseId: 'decline-crunch', - sets: [ - { reps: 15, rpe: 7 }, - { reps: 15, rpe: 8 } - ], - restTime: 60 - }, - { - exerciseId: 'lateral-raise-dumbbell', - sets: [ - { reps: 15, weight: 10, rpe: 7 }, - { reps: 15, weight: 10, rpe: 8 } - ], - restTime: 90 - }, - { - exerciseId: 'front-raise-dumbbell', - sets: [ - { reps: 10, weight: 10, rpe: 7 }, - { reps: 10, weight: 10, rpe: 8 } - ], - restTime: 90 - } - ] - }, - { - name: 'Day 2 - Push', - description: 'Chest, triceps, and biceps push day', - exercises: [ - { - exerciseId: 'bench-press-barbell', - sets: [ - { reps: 8, weight: 80, rpe: 7 }, - { reps: 8, weight: 80, rpe: 8 }, - { reps: 8, weight: 80, rpe: 9 } - ], - restTime: 120 - }, - { - exerciseId: 'incline-bench-press-barbell', - sets: [ - { reps: 10, weight: 60, rpe: 7 }, - { reps: 10, weight: 60, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'skullcrusher-dumbbell', - sets: [ - { reps: 15, weight: 15, rpe: 7 }, - { reps: 15, weight: 15, rpe: 8 } - ], - restTime: 90 - }, - { - exerciseId: 'bench-press-close-grip-barbell', - sets: [ - { reps: 10, weight: 60, rpe: 7 }, - { reps: 10, weight: 60, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'hammer-curl-dumbbell', - sets: [ - { reps: 15, weight: 12, rpe: 7 }, - { reps: 15, weight: 12, rpe: 8 } - ], - restTime: 90 - }, - { - exerciseId: 'bicep-curl-dumbbell', - sets: [ - { reps: 15, weight: 10, rpe: 7 }, - { reps: 15, weight: 10, rpe: 8 } - ], - restTime: 90 - } - ] - }, - { - name: 'Day 3 - Legs', - description: 'Quad, hamstring, and calf focused leg day', - exercises: [ - { - exerciseId: 'squat-barbell', - sets: [ - { reps: 8, weight: 80, rpe: 7 }, - { reps: 8, weight: 80, rpe: 8 }, - { reps: 8, weight: 80, rpe: 9 } - ], - restTime: 150 - }, - { - exerciseId: 'romanian-deadlift-barbell', - sets: [ - { reps: 10, weight: 70, rpe: 7 }, - { reps: 10, weight: 70, rpe: 8 }, - { reps: 10, weight: 70, rpe: 9 } - ], - restTime: 120 - }, - { - exerciseId: 'bulgarian-split-squat-dumbbell', - sets: [ - { reps: 10, weight: 16, rpe: 7 }, - { reps: 10, weight: 16, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'calf-raise-standing', - sets: [ - { reps: 15, rpe: 7 }, - { reps: 15, rpe: 8 }, - { reps: 15, rpe: 9 } - ], - restTime: 60 - } - ] - }, - { - name: 'Day 4 - Upper', - description: 'Full upper body day — shoulders, chest, back, and core', - exercises: [ - { - exerciseId: 'overhead-press-barbell', - sets: [ - { reps: 8, weight: 40, rpe: 7 }, - { reps: 8, weight: 40, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'bench-press-dumbbell', - sets: [ - { reps: 10, weight: 28, rpe: 7 }, - { reps: 10, weight: 28, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'chin-up', - sets: [ - { reps: 6, rpe: 8 }, - { reps: 6, rpe: 9 } - ], - restTime: 120 - }, - { - exerciseId: 'bench-press-close-grip-barbell', - sets: [ - { reps: 10, weight: 60, rpe: 7 }, - { reps: 10, weight: 60, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'decline-crunch', - sets: [ - { reps: 15, rpe: 7 }, - { reps: 15, rpe: 8 } - ], - restTime: 60 - }, - { - exerciseId: 'flat-leg-raise', - sets: [ - { reps: 15, rpe: 7 }, - { reps: 15, rpe: 8 } - ], - restTime: 60 - } - ] - }, - { - name: 'Day 5 - Lower', - description: 'Glute, quad, and hamstring focused lower day', - exercises: [ - { - exerciseId: 'front-squat-barbell', - sets: [ - { reps: 8, weight: 60, rpe: 7 }, - { reps: 8, weight: 60, rpe: 8 }, - { reps: 8, weight: 60, rpe: 9 } - ], - restTime: 150 - }, - { - exerciseId: 'romanian-deadlift-dumbbell', - sets: [ - { reps: 10, weight: 24, rpe: 7 }, - { reps: 10, weight: 24, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'hip-thrust-barbell', - sets: [ - { reps: 10, weight: 60, rpe: 7 }, - { reps: 10, weight: 60, rpe: 8 } - ], - restTime: 120 - }, - { - exerciseId: 'goblet-squat-dumbbell', - sets: [ - { reps: 12, weight: 20, rpe: 7 }, - { reps: 12, weight: 20, rpe: 8 } - ], - restTime: 90 - }, - { - exerciseId: 'calf-raise-standing', - sets: [ - { reps: 15, rpe: 7 }, - { reps: 15, rpe: 8 } - ], - restTime: 60 - } - ] - }, - { - name: 'Day 6 - Stretching', - description: 'Full-body stretching — all muscle groups, no equipment (~30 min)', - exercises: [ - { - // Neck: sternocleidomastoid, levator scapulae, trapezius - exerciseId: 'neck-circle-stretch', - sets: [ - { duration: 1.5 }, - { duration: 1.5 } - ], - restTime: 15 - }, - { - // Neck/traps: lateral neck flexion - exerciseId: 'side-push-neck-stretch', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Chest & front delts - exerciseId: 'seated-shoulder-flexor-stretch-bent-knee', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Rear delts, triceps, traps - exerciseId: 'shoulder-stretch-behind-back', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Triceps - exerciseId: 'elbows-back-stretch', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Lats & back - exerciseId: 'back-pec-stretch', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Spine, erector spinae, levator scapulae - exerciseId: 'cow-stretch', - sets: [ - { duration: 1.5 }, - { duration: 1.5 } - ], - restTime: 15 - }, - { - // Thoracic spine, shoulders, glutes, calves (multi-target bridge) - exerciseId: 'thoracic-bridge', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Hip adductors & hip flexors - exerciseId: 'butterfly-yoga-pose', - sets: [ - { duration: 1.5 }, - { duration: 1.5 } - ], - restTime: 15 - }, - { - // Hamstrings - exerciseId: 'seated-single-leg-hamstring-stretch', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Hamstrings (toe-up variation) - exerciseId: 'kneeling-toe-up-hamstring-stretch', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Quads, glutes, hip flexors, calves - exerciseId: 'side-lunge-stretch', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Lower back & glutes - exerciseId: 'lying-lower-back-stretch', - sets: [ - { duration: 1.5 }, - { duration: 1.5 } - ], - restTime: 15 - }, - { - // Calves (soleus & gastrocnemius) - exerciseId: 'calf-stretch-wall', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - }, - { - // Biceps & forearm flexors - exerciseId: 'elbow-flexor-stretch', - sets: [ - { duration: 1 }, - { duration: 1 } - ], - restTime: 15 - } - ] - } -]; +import { defaultTemplates } from '$lib/data/defaultTemplates'; export const POST: RequestHandler = async ({ locals }) => { const user = await requireAuth(locals); diff --git a/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte b/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte index bc7b893..ec3cb76 100644 --- a/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte +++ b/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { onMount } from 'svelte'; - import { Plus, Trash2, Play, Pencil, X, Save, CalendarClock, ChevronUp, ChevronDown, ArrowRight, MapPin, Dumbbell, Timer } from '@lucide/svelte'; + import { Plus, Trash2, Play, Pencil, X, Save, CalendarClock, ChevronUp, ChevronDown, ArrowRight, MapPin, Dumbbell, Timer, BookOpen, Check } from '@lucide/svelte'; import { getWorkout } from '$lib/js/workout.svelte'; import { getWorkoutSync } from '$lib/js/workoutSync.svelte'; import { detectFitnessLang, fitnessSlugs, t } from '$lib/js/fitnessI18n'; @@ -20,7 +20,11 @@ const sync = getWorkoutSync(); // svelte-ignore state_referenced_locally let templates = $state(data.templates?.templates ? [...data.templates.templates] : []); - let seeded = $state(false); + // Library browser + let showLibrary = $state(false); + /** @type {any[]} */ + let libraryTemplates = $state([]); + let libraryLoading = $state(false); // Schedule state /** @type {string[]} */ @@ -84,15 +88,8 @@ return; } - if (templates.length === 0 && !seeded) { - seeded = true; - fetch('/api/fitness/templates/seed', { method: 'POST' }).then(async (res) => { - if (res.ok) { - const refreshRes = await fetch('/api/fitness/templates'); - const refreshData = await refreshRes.json(); - templates = refreshData.templates ?? []; - } - }); + if (templates.length === 0 && !showLibrary) { + openLibrary(); } }); @@ -135,6 +132,41 @@ await startFromTemplate(nextTemplate); } + async function openLibrary() { + showLibrary = true; + if (libraryTemplates.length > 0) return; + libraryLoading = true; + try { + const res = await fetch('/api/fitness/templates/library'); + if (res.ok) { + const data = await res.json(); + libraryTemplates = data.templates ?? []; + } + } catch {} + libraryLoading = false; + } + + /** @param {any} libTemplate */ + async function addFromLibrary(libTemplate) { + try { + const res = await fetch('/api/fitness/templates/library', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: libTemplate.id }) + }); + if (res.ok) { + const { template } = await res.json(); + templates = [...templates, template]; + libTemplate.added = true; + libraryTemplates = [...libraryTemplates]; + toast.success(t('template_added', lang)); + } else if (res.status === 409) { + libTemplate.added = true; + libraryTemplates = [...libraryTemplates]; + } + } catch {} + } + function openCreateTemplate() { editingTemplate = null; editorName = ''; @@ -425,6 +457,9 @@ + {/if} @@ -702,6 +741,49 @@ {/if} + +{#if showLibrary} + + +{/if} +