diff --git a/src/lib/js/fitnessI18n.ts b/src/lib/js/fitnessI18n.ts index 7a1110a..be29fd7 100644 --- a/src/lib/js/fitnessI18n.ts +++ b/src/lib/js/fitnessI18n.ts @@ -108,7 +108,7 @@ const translations: Translations = { // Workout templates page next_in_schedule: { en: 'Next in schedule', de: 'Nächstes im Plan' }, - start_empty_workout: { en: 'START AN EMPTY WORKOUT', de: 'LEERES TRAINING STARTEN' }, + start_empty_workout: { en: 'Empty Workout', de: 'leeres Training' }, templates: { en: 'Templates', de: 'Vorlagen' }, schedule: { en: 'Schedule', de: 'Zeitplan' }, my_templates: { en: 'My Templates', de: 'Meine Vorlagen' }, diff --git a/src/models/IntervalTemplate.ts b/src/models/IntervalTemplate.ts new file mode 100644 index 0000000..699f543 --- /dev/null +++ b/src/models/IntervalTemplate.ts @@ -0,0 +1,70 @@ +import mongoose from 'mongoose'; + +export interface IIntervalStep { + label: string; + durationType: 'distance' | 'time'; + durationValue: number; // meters (distance) or seconds (time) +} + +export interface IIntervalTemplate { + _id?: string; + name: string; + steps: IIntervalStep[]; + createdBy: string; + createdAt?: Date; + updatedAt?: Date; +} + +const IntervalStepSchema = new mongoose.Schema({ + label: { + type: String, + required: true, + trim: true, + maxlength: 50 + }, + durationType: { + type: String, + required: true, + enum: ['distance', 'time'] + }, + durationValue: { + type: Number, + required: true, + min: 1 + } +}); + +const IntervalTemplateSchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + trim: true, + maxlength: 100 + }, + steps: { + type: [IntervalStepSchema], + required: true, + validate: { + validator: function(steps: IIntervalStep[]) { + return steps.length > 0; + }, + message: 'An interval template must have at least one step' + } + }, + createdBy: { + type: String, + required: true, + trim: true + } + }, + { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } + } +); + +IntervalTemplateSchema.index({ createdBy: 1 }); + +export const IntervalTemplate = mongoose.model("IntervalTemplate", IntervalTemplateSchema); diff --git a/src/routes/api/fitness/intervals/+server.ts b/src/routes/api/fitness/intervals/+server.ts new file mode 100644 index 0000000..c8879a7 --- /dev/null +++ b/src/routes/api/fitness/intervals/+server.ts @@ -0,0 +1,58 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { dbConnect } from '$utils/db'; +import { IntervalTemplate } from '$models/IntervalTemplate'; + +export const GET: RequestHandler = async ({ locals }) => { + const session = await locals.auth(); + if (!session || !session.user?.nickname) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + await dbConnect(); + const templates = await IntervalTemplate.find({ createdBy: session.user.nickname }).sort({ updatedAt: -1 }); + return json({ templates }); + } catch (error) { + console.error('Error fetching interval templates:', error); + return json({ error: 'Failed to fetch interval templates' }, { status: 500 }); + } +}; + +export const POST: RequestHandler = async ({ request, locals }) => { + const session = await locals.auth(); + if (!session || !session.user?.nickname) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + await dbConnect(); + const data = await request.json(); + const { name, steps } = data; + + if (!name || !steps || !Array.isArray(steps) || steps.length === 0) { + return json({ error: 'Name and at least one step are required' }, { status: 400 }); + } + + for (const step of steps) { + if (!step.label || !step.durationType || !step.durationValue) { + return json({ error: 'Each step must have a label, durationType, and durationValue' }, { status: 400 }); + } + if (!['distance', 'time'].includes(step.durationType)) { + return json({ error: 'durationType must be "distance" or "time"' }, { status: 400 }); + } + } + + const template = new IntervalTemplate({ + name, + steps, + createdBy: session.user.nickname + }); + + await template.save(); + return json({ template }, { status: 201 }); + } catch (error) { + console.error('Error creating interval template:', error); + return json({ error: 'Failed to create interval template' }, { status: 500 }); + } +}; diff --git a/src/routes/api/fitness/intervals/[id]/+server.ts b/src/routes/api/fitness/intervals/[id]/+server.ts new file mode 100644 index 0000000..a198630 --- /dev/null +++ b/src/routes/api/fitness/intervals/[id]/+server.ts @@ -0,0 +1,103 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { dbConnect } from '$utils/db'; +import { IntervalTemplate } from '$models/IntervalTemplate'; +import mongoose from 'mongoose'; + +export const GET: RequestHandler = async ({ params, locals }) => { + const session = await locals.auth(); + if (!session || !session.user?.nickname) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + await dbConnect(); + if (!mongoose.Types.ObjectId.isValid(params.id)) { + return json({ error: 'Invalid template ID' }, { status: 400 }); + } + + const template = await IntervalTemplate.findOne({ + _id: params.id, + createdBy: session.user.nickname + }); + + if (!template) { + return json({ error: 'Template not found' }, { status: 404 }); + } + + return json({ template }); + } catch (error) { + console.error('Error fetching interval template:', error); + return json({ error: 'Failed to fetch interval template' }, { status: 500 }); + } +}; + +export const PUT: RequestHandler = async ({ params, request, locals }) => { + const session = await locals.auth(); + if (!session || !session.user?.nickname) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + await dbConnect(); + if (!mongoose.Types.ObjectId.isValid(params.id)) { + return json({ error: 'Invalid template ID' }, { status: 400 }); + } + + const data = await request.json(); + const { name, steps } = data; + + if (!name || !steps || !Array.isArray(steps) || steps.length === 0) { + return json({ error: 'Name and at least one step are required' }, { status: 400 }); + } + + for (const step of steps) { + if (!step.label || !step.durationType || !step.durationValue) { + return json({ error: 'Each step must have a label, durationType, and durationValue' }, { status: 400 }); + } + } + + const template = await IntervalTemplate.findOneAndUpdate( + { _id: params.id, createdBy: session.user.nickname }, + { name, steps }, + { new: true } + ); + + if (!template) { + return json({ error: 'Template not found or unauthorized' }, { status: 404 }); + } + + return json({ template }); + } catch (error) { + console.error('Error updating interval template:', error); + return json({ error: 'Failed to update interval template' }, { status: 500 }); + } +}; + +export const DELETE: RequestHandler = async ({ params, locals }) => { + const session = await locals.auth(); + if (!session || !session.user?.nickname) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + await dbConnect(); + if (!mongoose.Types.ObjectId.isValid(params.id)) { + return json({ error: 'Invalid template ID' }, { status: 400 }); + } + + const template = await IntervalTemplate.findOneAndDelete({ + _id: params.id, + createdBy: session.user.nickname + }); + + if (!template) { + return json({ error: 'Template not found or unauthorized' }, { status: 404 }); + } + + return json({ message: 'Template deleted successfully' }); + } catch (error) { + console.error('Error deleting interval template:', error); + return json({ error: 'Failed to delete interval template' }, { status: 500 }); + } +}; diff --git a/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte b/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte index 0604a3d..25e4a31 100644 --- a/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte +++ b/src/routes/fitness/[workout=fitnessWorkout]/+page.svelte @@ -342,16 +342,16 @@
+ {#if isApp} {/if} -