fitness: shorter strings for main activity buttons, add missing pages
All checks were successful
CI / update (push) Successful in 2m12s
All checks were successful
CI / update (push) Successful in 2m12s
This commit is contained in:
@@ -108,7 +108,7 @@ const translations: Translations = {
|
|||||||
|
|
||||||
// Workout templates page
|
// Workout templates page
|
||||||
next_in_schedule: { en: 'Next in schedule', de: 'Nächstes im Plan' },
|
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' },
|
templates: { en: 'Templates', de: 'Vorlagen' },
|
||||||
schedule: { en: 'Schedule', de: 'Zeitplan' },
|
schedule: { en: 'Schedule', de: 'Zeitplan' },
|
||||||
my_templates: { en: 'My Templates', de: 'Meine Vorlagen' },
|
my_templates: { en: 'My Templates', de: 'Meine Vorlagen' },
|
||||||
|
|||||||
70
src/models/IntervalTemplate.ts
Normal file
70
src/models/IntervalTemplate.ts
Normal file
@@ -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<IIntervalTemplate>("IntervalTemplate", IntervalTemplateSchema);
|
||||||
58
src/routes/api/fitness/intervals/+server.ts
Normal file
58
src/routes/api/fitness/intervals/+server.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
103
src/routes/api/fitness/intervals/[id]/+server.ts
Normal file
103
src/routes/api/fitness/intervals/[id]/+server.ts
Normal file
@@ -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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -342,16 +342,16 @@
|
|||||||
|
|
||||||
<section class="quick-start">
|
<section class="quick-start">
|
||||||
<div class="quick-start-row">
|
<div class="quick-start-row">
|
||||||
|
<button class="start-choice-btn" onclick={startEmpty}>
|
||||||
|
{#if isApp}<Dumbbell size={18} />{/if}
|
||||||
|
<span>{t('start_empty_workout', lang)}</span>
|
||||||
|
</button>
|
||||||
{#if isApp}
|
{#if isApp}
|
||||||
<button class="start-choice-btn" onclick={startGps}>
|
<button class="start-choice-btn" onclick={startGps}>
|
||||||
<MapPin size={18} />
|
<MapPin size={18} />
|
||||||
<span>GPS Workout</span>
|
<span>GPS Workout</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="start-choice-btn" onclick={startEmpty}>
|
|
||||||
{#if isApp}<Dumbbell size={18} />{/if}
|
|
||||||
<span>{t('start_empty_workout', lang)}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user