fitness: add complete fitness tracker frontend
- 5-tab layout (Profile, History, Workout, Exercises, Measure) with shared header nav - Workout system: template CRUD, active workout on /fitness/workout/active with localStorage persistence, pause/resume timer, rest timer, RPE input - Shared workout singleton (getWorkout) so active workout state is accessible across all fitness routes - Floating workout FAB indicator on all /fitness routes when workout is active - AddActionButton component for button-based FABs (measure + template creation) - Profile page with workouts-per-week bar chart and weight line chart with SMA trend line + ±1σ confidence band - Exercise detail with history, charts, and records tabs using static exercise data - Session history with grouped-by-month list, session detail with stats/PRs - Body measurements with latest values, body part display, add form - Card styling matching rosary/prayer route patterns (accent-dark, nord5 light, box-shadow, hover lift) - FitnessChart: fix SSR hang by moving Chart.register to client-side, remove redundant $effect - Exercise API: use static in-repo data instead of empty MongoDB collection - Workout finish: include exercise name for WorkoutSession model validation
This commit is contained in:
@@ -1,30 +1,18 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { Exercise } from '$models/Exercise';
|
||||
import { getExerciseById } from '$lib/data/exercises';
|
||||
|
||||
// GET /api/fitness/exercises/[id] - Get detailed exercise information
|
||||
// GET /api/fitness/exercises/[id] - Get exercise from static data
|
||||
export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
const session = await locals.auth();
|
||||
if (!session || !session.user?.nickname) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
const session = await locals.auth();
|
||||
if (!session || !session.user?.nickname) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
await dbConnect();
|
||||
|
||||
const exercise = await Exercise.findOne({
|
||||
exerciseId: params.id,
|
||||
isActive: true
|
||||
});
|
||||
const exercise = getExerciseById(params.id);
|
||||
if (!exercise) {
|
||||
return json({ error: 'Exercise not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!exercise) {
|
||||
return json({ error: 'Exercise not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ exercise });
|
||||
} catch (error) {
|
||||
console.error('Error fetching exercise details:', error);
|
||||
return json({ error: 'Failed to fetch exercise details' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
return json({ exercise });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAuth } from '$lib/server/middleware/auth';
|
||||
import { getExerciseById } from '$lib/data/exercises';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { WorkoutSession } from '$models/WorkoutSession';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
|
||||
const exercise = getExerciseById(params.id);
|
||||
if (!exercise) {
|
||||
return json({ error: 'Exercise not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const limit = parseInt(url.searchParams.get('limit') || '20');
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0');
|
||||
|
||||
await dbConnect();
|
||||
|
||||
const sessions = await WorkoutSession.find({
|
||||
createdBy: user.nickname,
|
||||
'exercises.exerciseId': params.id
|
||||
})
|
||||
.sort({ startTime: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
// Extract only the relevant exercise data from each session
|
||||
const history = sessions.map((session) => {
|
||||
const exerciseData = session.exercises.find((e) => e.exerciseId === params.id);
|
||||
return {
|
||||
sessionId: session._id,
|
||||
sessionName: session.name,
|
||||
date: session.startTime,
|
||||
sets: exerciseData?.sets ?? [],
|
||||
notes: exerciseData?.notes
|
||||
};
|
||||
});
|
||||
|
||||
const total = await WorkoutSession.countDocuments({
|
||||
createdBy: user.nickname,
|
||||
'exercises.exerciseId': params.id
|
||||
});
|
||||
|
||||
return json({ history, total, limit, offset });
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAuth } from '$lib/server/middleware/auth';
|
||||
import { getExerciseById } from '$lib/data/exercises';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { WorkoutSession } from '$models/WorkoutSession';
|
||||
|
||||
/**
|
||||
* Epley formula for estimated 1RM
|
||||
*/
|
||||
function estimatedOneRepMax(weight: number, reps: number): number {
|
||||
if (reps <= 0 || weight <= 0) return 0;
|
||||
if (reps === 1) return weight;
|
||||
return Math.round(weight * (1 + reps / 30) * 10) / 10;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
|
||||
const exercise = getExerciseById(params.id);
|
||||
if (!exercise) {
|
||||
return json({ error: 'Exercise not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
await dbConnect();
|
||||
|
||||
const sessions = await WorkoutSession.find({
|
||||
createdBy: user.nickname,
|
||||
'exercises.exerciseId': params.id
|
||||
})
|
||||
.sort({ startTime: 1 })
|
||||
.lean();
|
||||
|
||||
// Build time-series and records data
|
||||
const est1rmOverTime: { date: Date; value: number }[] = [];
|
||||
const maxWeightOverTime: { date: Date; value: number }[] = [];
|
||||
const totalVolumeOverTime: { date: Date; value: number }[] = [];
|
||||
|
||||
// Track best performance at each rep count: { reps -> { weight, date, estimated1rm } }
|
||||
const repRecords = new Map<
|
||||
number,
|
||||
{ weight: number; reps: number; date: Date; estimated1rm: number }
|
||||
>();
|
||||
let bestEst1rm = 0;
|
||||
let bestMaxWeight = 0;
|
||||
let bestMaxVolume = 0;
|
||||
|
||||
for (const session of sessions) {
|
||||
const exerciseData = session.exercises.find((e) => e.exerciseId === params.id);
|
||||
if (!exerciseData) continue;
|
||||
|
||||
const completedSets = exerciseData.sets.filter((s) => s.completed && s.weight && s.reps > 0);
|
||||
if (completedSets.length === 0) continue;
|
||||
|
||||
// Best set est. 1RM for this session
|
||||
let sessionBestEst1rm = 0;
|
||||
let sessionMaxWeight = 0;
|
||||
let sessionVolume = 0;
|
||||
|
||||
for (const set of completedSets) {
|
||||
const weight = set.weight!;
|
||||
const reps = set.reps;
|
||||
const est1rm = estimatedOneRepMax(weight, reps);
|
||||
|
||||
sessionBestEst1rm = Math.max(sessionBestEst1rm, est1rm);
|
||||
sessionMaxWeight = Math.max(sessionMaxWeight, weight);
|
||||
sessionVolume += weight * reps;
|
||||
|
||||
// Update rep records
|
||||
const existing = repRecords.get(reps);
|
||||
if (!existing || weight > existing.weight) {
|
||||
repRecords.set(reps, {
|
||||
weight,
|
||||
reps,
|
||||
date: session.startTime,
|
||||
estimated1rm: est1rm
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
est1rmOverTime.push({ date: session.startTime, value: sessionBestEst1rm });
|
||||
maxWeightOverTime.push({ date: session.startTime, value: sessionMaxWeight });
|
||||
totalVolumeOverTime.push({ date: session.startTime, value: sessionVolume });
|
||||
|
||||
bestEst1rm = Math.max(bestEst1rm, sessionBestEst1rm);
|
||||
bestMaxWeight = Math.max(bestMaxWeight, sessionMaxWeight);
|
||||
bestMaxVolume = Math.max(bestMaxVolume, sessionVolume);
|
||||
}
|
||||
|
||||
// Convert rep records to sorted array
|
||||
const records = [...repRecords.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([reps, data]) => ({
|
||||
reps,
|
||||
weight: data.weight,
|
||||
date: data.date,
|
||||
estimated1rm: data.estimated1rm
|
||||
}));
|
||||
|
||||
return json({
|
||||
charts: {
|
||||
est1rmOverTime,
|
||||
maxWeightOverTime,
|
||||
totalVolumeOverTime
|
||||
},
|
||||
personalRecords: {
|
||||
estimatedOneRepMax: bestEst1rm,
|
||||
maxWeight: bestMaxWeight,
|
||||
maxVolume: bestMaxVolume
|
||||
},
|
||||
records,
|
||||
totalSessions: sessions.length
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAuth } from '$lib/server/middleware/auth';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { BodyMeasurement } from '$models/BodyMeasurement';
|
||||
|
||||
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
const limit = parseInt(url.searchParams.get('limit') || '50');
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0');
|
||||
const from = url.searchParams.get('from');
|
||||
const to = url.searchParams.get('to');
|
||||
|
||||
const query: Record<string, unknown> = { createdBy: user.nickname };
|
||||
|
||||
if (from || to) {
|
||||
const dateFilter: Record<string, Date> = {};
|
||||
if (from) dateFilter.$gte = new Date(from);
|
||||
if (to) dateFilter.$lte = new Date(to);
|
||||
query.date = dateFilter;
|
||||
}
|
||||
|
||||
const measurements = await BodyMeasurement.find(query)
|
||||
.sort({ date: -1 })
|
||||
.skip(offset)
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
const total = await BodyMeasurement.countDocuments(query);
|
||||
|
||||
return json({ measurements, total, limit, offset });
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
const data = await request.json();
|
||||
const { date, weight, bodyFatPercent, caloricIntake, measurements, notes } = data;
|
||||
|
||||
const measurement = new BodyMeasurement({
|
||||
date: date ? new Date(date) : new Date(),
|
||||
weight,
|
||||
bodyFatPercent,
|
||||
caloricIntake,
|
||||
measurements,
|
||||
notes,
|
||||
createdBy: user.nickname
|
||||
});
|
||||
|
||||
await measurement.save();
|
||||
return json({ measurement }, { status: 201 });
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAuth } from '$lib/server/middleware/auth';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { BodyMeasurement } from '$models/BodyMeasurement';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(params.id)) {
|
||||
return json({ error: 'Invalid measurement ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const measurement = await BodyMeasurement.findOne({
|
||||
_id: params.id,
|
||||
createdBy: user.nickname
|
||||
});
|
||||
|
||||
if (!measurement) {
|
||||
return json({ error: 'Measurement not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ measurement });
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async ({ params, request, locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(params.id)) {
|
||||
return json({ error: 'Invalid measurement ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
const { date, weight, bodyFatPercent, caloricIntake, measurements, notes } = data;
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (date) updateData.date = new Date(date);
|
||||
if (weight !== undefined) updateData.weight = weight;
|
||||
if (bodyFatPercent !== undefined) updateData.bodyFatPercent = bodyFatPercent;
|
||||
if (caloricIntake !== undefined) updateData.caloricIntake = caloricIntake;
|
||||
if (measurements !== undefined) updateData.measurements = measurements;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
|
||||
const measurement = await BodyMeasurement.findOneAndUpdate(
|
||||
{ _id: params.id, createdBy: user.nickname },
|
||||
updateData,
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!measurement) {
|
||||
return json({ error: 'Measurement not found or unauthorized' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ measurement });
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params, locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
if (!mongoose.Types.ObjectId.isValid(params.id)) {
|
||||
return json({ error: 'Invalid measurement ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const measurement = await BodyMeasurement.findOneAndDelete({
|
||||
_id: params.id,
|
||||
createdBy: user.nickname
|
||||
});
|
||||
|
||||
if (!measurement) {
|
||||
return json({ error: 'Measurement not found or unauthorized' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ message: 'Measurement deleted successfully' });
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAuth } from '$lib/server/middleware/auth';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { BodyMeasurement } from '$models/BodyMeasurement';
|
||||
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
// Get latest measurement that has each field
|
||||
const latestWithWeight = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
weight: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('weight date')
|
||||
.lean();
|
||||
|
||||
const latestWithBodyFat = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
bodyFatPercent: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('bodyFatPercent date')
|
||||
.lean();
|
||||
|
||||
const latestWithCalories = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
caloricIntake: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('caloricIntake date')
|
||||
.lean();
|
||||
|
||||
const latestWithMeasurements = await BodyMeasurement.findOne({
|
||||
createdBy: user.nickname,
|
||||
measurements: { $exists: true, $ne: null }
|
||||
})
|
||||
.sort({ date: -1 })
|
||||
.select('measurements date')
|
||||
.lean();
|
||||
|
||||
return json({
|
||||
weight: latestWithWeight
|
||||
? { value: latestWithWeight.weight, date: latestWithWeight.date }
|
||||
: null,
|
||||
bodyFatPercent: latestWithBodyFat
|
||||
? { value: latestWithBodyFat.bodyFatPercent, date: latestWithBodyFat.date }
|
||||
: null,
|
||||
caloricIntake: latestWithCalories
|
||||
? { value: latestWithCalories.caloricIntake, date: latestWithCalories.date }
|
||||
: null,
|
||||
measurements: latestWithMeasurements
|
||||
? {
|
||||
value: latestWithMeasurements.measurements,
|
||||
date: latestWithMeasurements.date
|
||||
}
|
||||
: null
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { requireAuth } from '$lib/server/middleware/auth';
|
||||
import { dbConnect } from '$utils/db';
|
||||
import { WorkoutSession } from '$models/WorkoutSession';
|
||||
import { BodyMeasurement } from '$models/BodyMeasurement';
|
||||
|
||||
export const GET: RequestHandler = async ({ locals }) => {
|
||||
console.time('[stats/profile] total');
|
||||
|
||||
console.time('[stats/profile] auth');
|
||||
const user = await requireAuth(locals);
|
||||
console.timeEnd('[stats/profile] auth');
|
||||
|
||||
console.time('[stats/profile] dbConnect');
|
||||
await dbConnect();
|
||||
console.timeEnd('[stats/profile] dbConnect');
|
||||
|
||||
const twelveWeeksAgo = new Date();
|
||||
twelveWeeksAgo.setDate(twelveWeeksAgo.getDate() - 84);
|
||||
|
||||
console.time('[stats/profile] countDocuments');
|
||||
const totalWorkouts = await WorkoutSession.countDocuments({ createdBy: user.nickname });
|
||||
console.timeEnd('[stats/profile] countDocuments');
|
||||
|
||||
console.time('[stats/profile] aggregate');
|
||||
const weeklyAgg = await WorkoutSession.aggregate([
|
||||
{
|
||||
$match: {
|
||||
createdBy: user.nickname,
|
||||
startTime: { $gte: twelveWeeksAgo }
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
year: { $isoWeekYear: '$startTime' },
|
||||
week: { $isoWeek: '$startTime' }
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
},
|
||||
{
|
||||
$sort: { '_id.year': 1, '_id.week': 1 }
|
||||
}
|
||||
]);
|
||||
console.timeEnd('[stats/profile] aggregate');
|
||||
|
||||
console.time('[stats/profile] measurements');
|
||||
const weightMeasurements = await BodyMeasurement.find(
|
||||
{ createdBy: user.nickname, weight: { $ne: null } },
|
||||
{ date: 1, weight: 1, _id: 0 }
|
||||
)
|
||||
.sort({ date: 1 })
|
||||
.limit(30)
|
||||
.lean();
|
||||
console.timeEnd('[stats/profile] measurements');
|
||||
|
||||
// Build chart-ready workouts-per-week with filled gaps
|
||||
const weekMap = new Map<string, number>();
|
||||
for (const item of weeklyAgg) {
|
||||
weekMap.set(`${item._id.year}-${item._id.week}`, item.count);
|
||||
}
|
||||
|
||||
const workoutsChart: { labels: string[]; data: number[] } = { labels: [], data: [] };
|
||||
const now = new Date();
|
||||
for (let i = 11; i >= 0; i--) {
|
||||
const d = new Date(now);
|
||||
d.setDate(d.getDate() - i * 7);
|
||||
const year = getISOWeekYear(d);
|
||||
const week = getISOWeek(d);
|
||||
const key = `${year}-${week}`;
|
||||
workoutsChart.labels.push(`W${week}`);
|
||||
workoutsChart.data.push(weekMap.get(key) ?? 0);
|
||||
}
|
||||
|
||||
// Build chart-ready weight data with SMA ± 1 std dev confidence band
|
||||
const weightChart: {
|
||||
labels: string[];
|
||||
data: number[];
|
||||
sma: (number | null)[];
|
||||
upper: (number | null)[];
|
||||
lower: (number | null)[];
|
||||
} = { labels: [], data: [], sma: [], upper: [], lower: [] };
|
||||
const weights: number[] = [];
|
||||
for (const m of weightMeasurements) {
|
||||
const d = new Date(m.date);
|
||||
weightChart.labels.push(
|
||||
d.toLocaleDateString('en', { month: 'short', day: 'numeric' })
|
||||
);
|
||||
weightChart.data.push(m.weight);
|
||||
weights.push(m.weight);
|
||||
}
|
||||
|
||||
// Adaptive window: 7 if enough data, otherwise half the data (min 2)
|
||||
const w = Math.min(7, Math.max(2, Math.floor(weights.length / 2)));
|
||||
for (let i = 0; i < weights.length; i++) {
|
||||
if (i < w - 1) {
|
||||
weightChart.sma.push(null);
|
||||
weightChart.upper.push(null);
|
||||
weightChart.lower.push(null);
|
||||
} else {
|
||||
let sum = 0;
|
||||
for (let j = i - w + 1; j <= i; j++) sum += weights[j];
|
||||
const mean = sum / w;
|
||||
|
||||
let variance = 0;
|
||||
for (let j = i - w + 1; j <= i; j++) variance += (weights[j] - mean) ** 2;
|
||||
const std = Math.sqrt(variance / w);
|
||||
|
||||
const round = (v: number) => Math.round(v * 100) / 100;
|
||||
weightChart.sma.push(round(mean));
|
||||
weightChart.upper.push(round(mean + std));
|
||||
weightChart.lower.push(round(mean - std));
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd('[stats/profile] total');
|
||||
return json({
|
||||
totalWorkouts,
|
||||
workoutsChart,
|
||||
weightChart
|
||||
});
|
||||
};
|
||||
|
||||
function getISOWeek(date: Date): number {
|
||||
const d = new Date(date.getTime());
|
||||
d.setHours(0, 0, 0, 0);
|
||||
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7));
|
||||
const week1 = new Date(d.getFullYear(), 0, 4);
|
||||
return 1 + Math.round(((d.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7);
|
||||
}
|
||||
|
||||
function getISOWeekYear(date: Date): number {
|
||||
const d = new Date(date.getTime());
|
||||
d.setDate(d.getDate() + 3 - ((d.getDay() + 6) % 7));
|
||||
return d.getFullYear();
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
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';
|
||||
|
||||
const defaultTemplates = [
|
||||
{
|
||||
name: 'Day 1 - Pull',
|
||||
description: 'Back and biceps focused 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: '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, shoulders, and triceps 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: '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: 'Lower body 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 }
|
||||
],
|
||||
restTime: 120
|
||||
},
|
||||
{
|
||||
exerciseId: 'leg-press-machine',
|
||||
sets: [
|
||||
{ reps: 12, weight: 100, rpe: 7 },
|
||||
{ reps: 12, weight: 100, rpe: 8 }
|
||||
],
|
||||
restTime: 120
|
||||
},
|
||||
{
|
||||
exerciseId: 'leg-curl-machine',
|
||||
sets: [
|
||||
{ reps: 12, weight: 40, rpe: 7 },
|
||||
{ reps: 12, weight: 40, rpe: 8 }
|
||||
],
|
||||
restTime: 90
|
||||
},
|
||||
{
|
||||
exerciseId: 'calf-raise-machine',
|
||||
sets: [
|
||||
{ reps: 15, weight: 60, rpe: 7 },
|
||||
{ reps: 15, weight: 60, rpe: 8 }
|
||||
],
|
||||
restTime: 60
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const POST: RequestHandler = async ({ locals }) => {
|
||||
const user = await requireAuth(locals);
|
||||
await dbConnect();
|
||||
|
||||
// Check if user already has templates (don't re-seed)
|
||||
const existingCount = await WorkoutTemplate.countDocuments({ createdBy: user.nickname });
|
||||
if (existingCount > 0) {
|
||||
return json({ message: 'Templates already exist', seeded: false });
|
||||
}
|
||||
|
||||
const templates = await WorkoutTemplate.insertMany(
|
||||
defaultTemplates.map((t) => ({
|
||||
...t,
|
||||
createdBy: user.nickname,
|
||||
isDefault: true
|
||||
}))
|
||||
);
|
||||
|
||||
return json({ message: 'Default templates created', templates, seeded: true }, { status: 201 });
|
||||
};
|
||||
Reference in New Issue
Block a user