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:
2026-03-19 08:17:51 +01:00
parent e427dc2d25
commit 1c62819d18
38 changed files with 5899 additions and 24 deletions

View File

@@ -0,0 +1,99 @@
import mongoose from 'mongoose';
export interface IBodyPartMeasurements {
neck?: number;
shoulders?: number;
chest?: number;
leftBicep?: number;
rightBicep?: number;
leftForearm?: number;
rightForearm?: number;
waist?: number;
hips?: number;
leftThigh?: number;
rightThigh?: number;
leftCalf?: number;
rightCalf?: number;
}
export interface IBodyMeasurement {
_id?: string;
date: Date;
weight?: number;
bodyFatPercent?: number;
caloricIntake?: number;
measurements?: IBodyPartMeasurements;
notes?: string;
createdBy: string;
createdAt?: Date;
updatedAt?: Date;
}
const BodyPartMeasurementsSchema = new mongoose.Schema(
{
neck: { type: Number, min: 0 },
shoulders: { type: Number, min: 0 },
chest: { type: Number, min: 0 },
leftBicep: { type: Number, min: 0 },
rightBicep: { type: Number, min: 0 },
leftForearm: { type: Number, min: 0 },
rightForearm: { type: Number, min: 0 },
waist: { type: Number, min: 0 },
hips: { type: Number, min: 0 },
leftThigh: { type: Number, min: 0 },
rightThigh: { type: Number, min: 0 },
leftCalf: { type: Number, min: 0 },
rightCalf: { type: Number, min: 0 }
},
{ _id: false }
);
const BodyMeasurementSchema = new mongoose.Schema(
{
date: {
type: Date,
required: true,
default: Date.now
},
weight: {
type: Number,
min: 0,
max: 500
},
bodyFatPercent: {
type: Number,
min: 0,
max: 100
},
caloricIntake: {
type: Number,
min: 0,
max: 50000
},
measurements: {
type: BodyPartMeasurementsSchema
},
notes: {
type: String,
trim: true,
maxlength: 500
},
createdBy: {
type: String,
required: true,
trim: true
}
},
{
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
BodyMeasurementSchema.index({ createdBy: 1, date: -1 });
export const BodyMeasurement = mongoose.model<IBodyMeasurement>(
'BodyMeasurement',
BodyMeasurementSchema
);