fitness: add per-exercise metrics, cardio support, and stats page
- Add metrics system (weight/reps/rpe/distance/duration) per exercise type so cardio exercises show distance+duration instead of weight+reps - Add 8 new cardio exercises: swimming, hiking, rowing outdoor, cycling outdoor, elliptical, stair climber, jump rope, walking - Add bilateral flag to dumbbell exercises for accurate tonnage calculation - Make SetTable, SessionCard, history detail, template editor, and exercise stats API all render/compute dynamically based on exercise metrics - Rename Profile to Stats with lifetime cards: workouts, tonnage, cardio km - Move route /fitness/profile -> /fitness/stats, API /stats/profile -> /stats/overview
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { getExerciseById } from '$lib/data/exercises';
|
||||
import { getExerciseById, getExerciseMetrics } from '$lib/data/exercises';
|
||||
import { Clock, Weight, Trophy } from 'lucide-svelte';
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@
|
||||
* prs?: Array<any>,
|
||||
* exercises: Array<{
|
||||
* exerciseId: string,
|
||||
* sets: Array<{ reps: number, weight: number, rpe?: number }>
|
||||
* sets: Array<{ reps?: number, weight?: number, rpe?: number, distance?: number, duration?: number }>
|
||||
* }>
|
||||
* }
|
||||
* }}
|
||||
@@ -41,16 +41,35 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<{ reps: number, weight: number, rpe?: number }>} sets
|
||||
* @param {Array<Record<string, any>>} sets
|
||||
* @param {string} exerciseId
|
||||
*/
|
||||
function bestSet(sets) {
|
||||
function bestSetLabel(sets, exerciseId) {
|
||||
const exercise = getExerciseById(exerciseId);
|
||||
const metrics = getExerciseMetrics(exercise);
|
||||
const isCardio = metrics.includes('distance');
|
||||
|
||||
if (isCardio) {
|
||||
let best = sets[0];
|
||||
for (const s of sets) {
|
||||
if ((s.distance ?? 0) > (best.distance ?? 0)) best = s;
|
||||
}
|
||||
const parts = [];
|
||||
if (best.distance) parts.push(`${best.distance} km`);
|
||||
if (best.duration) parts.push(`${best.duration} min`);
|
||||
if (best.rpe) parts.push(`@ ${best.rpe}`);
|
||||
return parts.join(' · ') || null;
|
||||
}
|
||||
|
||||
let best = sets[0];
|
||||
for (const s of sets) {
|
||||
if (s.weight > best.weight || (s.weight === best.weight && s.reps > best.reps)) {
|
||||
if ((s.weight ?? 0) > (best.weight ?? 0) || ((s.weight ?? 0) === (best.weight ?? 0) && (s.reps ?? 0) > (best.reps ?? 0))) {
|
||||
best = s;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
let label = `${best.weight ?? 0} kg × ${best.reps ?? 0}`;
|
||||
if (best.rpe) label += ` @ ${best.rpe}`;
|
||||
return label;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -63,11 +82,11 @@
|
||||
<div class="exercise-list">
|
||||
{#each session.exercises.slice(0, 4) as ex (ex.exerciseId)}
|
||||
{@const exercise = getExerciseById(ex.exerciseId)}
|
||||
{@const best = bestSet(ex.sets)}
|
||||
{@const label = bestSetLabel(ex.sets, ex.exerciseId)}
|
||||
<div class="exercise-row">
|
||||
<span class="ex-sets">{ex.sets.length} × {exercise?.name ?? ex.exerciseId}</span>
|
||||
{#if best}
|
||||
<span class="ex-best">{best.weight} kg × {best.reps}{#if best.rpe} @ {best.rpe}{/if}</span>
|
||||
{#if label}
|
||||
<span class="ex-best">{label}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script>
|
||||
import { Check } from 'lucide-svelte';
|
||||
import { METRIC_LABELS } from '$lib/data/exercises';
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* sets: Array<{ reps: number | null, weight: number | null, rpe?: number | null, completed?: boolean }>,
|
||||
* previousSets?: Array<{ reps: number, weight: number }> | null,
|
||||
* sets: Array<{ reps?: number | null, weight?: number | null, rpe?: number | null, distance?: number | null, duration?: number | null, completed?: boolean }>,
|
||||
* previousSets?: Array<Record<string, any>> | null,
|
||||
* metrics?: Array<'weight' | 'reps' | 'rpe' | 'distance' | 'duration'>,
|
||||
* editable?: boolean,
|
||||
* onUpdate?: ((setIndex: number, data: { reps?: number | null, weight?: number | null, rpe?: number | null }) => void) | null,
|
||||
* onUpdate?: ((setIndex: number, data: Record<string, number | null>) => void) | null,
|
||||
* onToggleComplete?: ((setIndex: number) => void) | null,
|
||||
* onRemove?: ((setIndex: number) => void) | null
|
||||
* }}
|
||||
@@ -14,12 +16,17 @@
|
||||
let {
|
||||
sets,
|
||||
previousSets = null,
|
||||
metrics = ['weight', 'reps', 'rpe'],
|
||||
editable = false,
|
||||
onUpdate = null,
|
||||
onToggleComplete = null,
|
||||
onRemove = null
|
||||
} = $props();
|
||||
|
||||
/** Metrics to show in the main columns (not RPE, which is edit-only) */
|
||||
const mainMetrics = $derived(metrics.filter((m) => m !== 'rpe'));
|
||||
const hasRpe = $derived(metrics.includes('rpe'));
|
||||
|
||||
/**
|
||||
* @param {number} index
|
||||
* @param {string} field
|
||||
@@ -30,6 +37,20 @@
|
||||
const val = target.value === '' ? null : Number(target.value);
|
||||
onUpdate?.(index, { [field]: val });
|
||||
}
|
||||
|
||||
/** Format a previous set for display */
|
||||
function formatPrev(/** @type {Record<string, any>} */ prev) {
|
||||
const parts = [];
|
||||
for (const m of mainMetrics) {
|
||||
if (prev[m] != null) parts.push(`${prev[m]}`);
|
||||
}
|
||||
return parts.join(' × ');
|
||||
}
|
||||
|
||||
/** @param {string} metric */
|
||||
function inputMode(metric) {
|
||||
return metric === 'reps' ? 'numeric' : 'decimal';
|
||||
}
|
||||
</script>
|
||||
|
||||
<table class="set-table">
|
||||
@@ -39,10 +60,13 @@
|
||||
{#if previousSets}
|
||||
<th class="col-prev">PREVIOUS</th>
|
||||
{/if}
|
||||
<th class="col-weight">KG</th>
|
||||
<th class="col-reps">REPS</th>
|
||||
{#if editable}
|
||||
{#each mainMetrics as metric (metric)}
|
||||
<th class="col-metric">{METRIC_LABELS[metric]}</th>
|
||||
{/each}
|
||||
{#if editable && hasRpe}
|
||||
<th class="col-rpe">RPE</th>
|
||||
{/if}
|
||||
{#if editable}
|
||||
<th class="col-check"></th>
|
||||
{/if}
|
||||
</tr>
|
||||
@@ -54,39 +78,28 @@
|
||||
{#if previousSets}
|
||||
<td class="col-prev">
|
||||
{#if previousSets[i]}
|
||||
{previousSets[i].weight} × {previousSets[i].reps}
|
||||
{formatPrev(previousSets[i])}
|
||||
{:else}
|
||||
—
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
<td class="col-weight">
|
||||
{#if editable}
|
||||
<input
|
||||
type="number"
|
||||
inputmode="decimal"
|
||||
value={set.weight ?? ''}
|
||||
placeholder="0"
|
||||
oninput={(e) => handleInput(i, 'weight', e)}
|
||||
/>
|
||||
{:else}
|
||||
{set.weight ?? '—'}
|
||||
{/if}
|
||||
</td>
|
||||
<td class="col-reps">
|
||||
{#if editable}
|
||||
<input
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
value={set.reps ?? ''}
|
||||
placeholder="0"
|
||||
oninput={(e) => handleInput(i, 'reps', e)}
|
||||
/>
|
||||
{:else}
|
||||
{set.reps ?? '—'}
|
||||
{/if}
|
||||
</td>
|
||||
{#if editable}
|
||||
{#each mainMetrics as metric (metric)}
|
||||
<td class="col-metric">
|
||||
{#if editable}
|
||||
<input
|
||||
type="number"
|
||||
inputmode={inputMode(metric)}
|
||||
value={set[metric] ?? ''}
|
||||
placeholder="0"
|
||||
oninput={(e) => handleInput(i, metric, e)}
|
||||
/>
|
||||
{:else}
|
||||
{set[metric] ?? '—'}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
{#if editable && hasRpe}
|
||||
<td class="col-rpe">
|
||||
<input
|
||||
type="number"
|
||||
@@ -98,6 +111,8 @@
|
||||
oninput={(e) => handleInput(i, 'rpe', e)}
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
{#if editable}
|
||||
<td class="col-check">
|
||||
<button
|
||||
class="check-btn"
|
||||
@@ -143,7 +158,7 @@
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.col-weight, .col-reps {
|
||||
.col-metric {
|
||||
width: 4rem;
|
||||
}
|
||||
.col-rpe {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
export type MetricField = 'weight' | 'reps' | 'rpe' | 'distance' | 'duration';
|
||||
|
||||
export const METRIC_LABELS: Record<MetricField, string> = {
|
||||
weight: 'KG',
|
||||
reps: 'REPS',
|
||||
rpe: 'RPE',
|
||||
distance: 'KM',
|
||||
duration: 'MIN'
|
||||
};
|
||||
|
||||
export const METRIC_PRESETS = {
|
||||
strength: ['weight', 'reps', 'rpe'] as MetricField[],
|
||||
cardio: ['distance', 'duration', 'rpe'] as MetricField[],
|
||||
timed: ['duration', 'reps'] as MetricField[]
|
||||
};
|
||||
|
||||
export interface Exercise {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -6,9 +22,17 @@ export interface Exercise {
|
||||
target: string;
|
||||
secondaryMuscles: string[];
|
||||
instructions: string[];
|
||||
metrics?: MetricField[];
|
||||
bilateral?: boolean; // true = weight entered is per hand, actual load is 2×
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export function getExerciseMetrics(exercise: Exercise | undefined): MetricField[] {
|
||||
if (exercise?.metrics) return exercise.metrics;
|
||||
if (exercise?.bodyPart === 'cardio') return METRIC_PRESETS.cardio;
|
||||
return METRIC_PRESETS.strength;
|
||||
}
|
||||
|
||||
export const exercises: Exercise[] = [
|
||||
// === CHEST ===
|
||||
{
|
||||
@@ -71,6 +95,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Bench Press (Dumbbell)',
|
||||
bodyPart: 'chest',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'pectorals',
|
||||
secondaryMuscles: ['triceps', 'anterior deltoids'],
|
||||
instructions: [
|
||||
@@ -84,6 +109,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Incline Bench Press (Dumbbell)',
|
||||
bodyPart: 'chest',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'pectorals',
|
||||
secondaryMuscles: ['triceps', 'anterior deltoids'],
|
||||
instructions: [
|
||||
@@ -98,6 +124,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Chest Fly (Dumbbell)',
|
||||
bodyPart: 'chest',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'pectorals',
|
||||
secondaryMuscles: ['anterior deltoids'],
|
||||
instructions: [
|
||||
@@ -261,6 +288,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Incline Row (Dumbbell)',
|
||||
bodyPart: 'back',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'lats',
|
||||
secondaryMuscles: ['rhomboids', 'biceps', 'rear deltoids'],
|
||||
instructions: [
|
||||
@@ -304,6 +332,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Overhead Press (Dumbbell)',
|
||||
bodyPart: 'shoulders',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'anterior deltoids',
|
||||
secondaryMuscles: ['triceps', 'lateral deltoids', 'traps'],
|
||||
instructions: [
|
||||
@@ -317,6 +346,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Lateral Raise (Dumbbell)',
|
||||
bodyPart: 'shoulders',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'lateral deltoids',
|
||||
secondaryMuscles: ['traps'],
|
||||
instructions: [
|
||||
@@ -343,6 +373,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Front Raise (Dumbbell)',
|
||||
bodyPart: 'shoulders',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'anterior deltoids',
|
||||
secondaryMuscles: ['lateral deltoids'],
|
||||
instructions: [
|
||||
@@ -356,6 +387,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Reverse Fly (Dumbbell)',
|
||||
bodyPart: 'shoulders',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'rear deltoids',
|
||||
secondaryMuscles: ['rhomboids', 'traps'],
|
||||
instructions: [
|
||||
@@ -395,6 +427,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Shrug (Dumbbell)',
|
||||
bodyPart: 'shoulders',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'traps',
|
||||
secondaryMuscles: [],
|
||||
instructions: [
|
||||
@@ -423,6 +456,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Bicep Curl (Dumbbell)',
|
||||
bodyPart: 'arms',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'biceps',
|
||||
secondaryMuscles: ['forearms'],
|
||||
instructions: [
|
||||
@@ -436,6 +470,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Hammer Curl (Dumbbell)',
|
||||
bodyPart: 'arms',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'biceps',
|
||||
secondaryMuscles: ['brachioradialis', 'forearms'],
|
||||
instructions: [
|
||||
@@ -503,6 +538,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Skullcrusher (Dumbbell)',
|
||||
bodyPart: 'arms',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'triceps',
|
||||
secondaryMuscles: [],
|
||||
instructions: [
|
||||
@@ -610,6 +646,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Lunge (Dumbbell)',
|
||||
bodyPart: 'legs',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'quadriceps',
|
||||
secondaryMuscles: ['glutes', 'hamstrings'],
|
||||
instructions: [
|
||||
@@ -623,6 +660,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Bulgarian Split Squat (Dumbbell)',
|
||||
bodyPart: 'legs',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'quadriceps',
|
||||
secondaryMuscles: ['glutes', 'hamstrings'],
|
||||
instructions: [
|
||||
@@ -676,6 +714,7 @@ export const exercises: Exercise[] = [
|
||||
name: 'Romanian Deadlift (Dumbbell)',
|
||||
bodyPart: 'legs',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'hamstrings',
|
||||
secondaryMuscles: ['glutes', 'erector spinae'],
|
||||
instructions: [
|
||||
@@ -775,6 +814,7 @@ export const exercises: Exercise[] = [
|
||||
bodyPart: 'core',
|
||||
equipment: 'body weight',
|
||||
target: 'abdominals',
|
||||
metrics: ['duration'],
|
||||
secondaryMuscles: ['obliques', 'erector spinae'],
|
||||
instructions: [
|
||||
'Start in a forearm plank position with elbows under shoulders.',
|
||||
@@ -888,6 +928,15 @@ export const exercises: Exercise[] = [
|
||||
secondaryMuscles: ['quadriceps', 'hamstrings', 'calves', 'glutes'],
|
||||
instructions: ['Run at a steady pace for the desired duration or distance.']
|
||||
},
|
||||
{
|
||||
id: 'walking',
|
||||
name: 'Walking',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'body weight',
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['quadriceps', 'hamstrings', 'calves', 'glutes'],
|
||||
instructions: ['Walk at a brisk pace for the desired duration or distance.']
|
||||
},
|
||||
{
|
||||
id: 'cycling-indoor',
|
||||
name: 'Cycling (Indoor)',
|
||||
@@ -897,6 +946,24 @@ export const exercises: Exercise[] = [
|
||||
secondaryMuscles: ['quadriceps', 'hamstrings', 'calves'],
|
||||
instructions: ['Cycle at a steady pace on a stationary bike for the desired duration.']
|
||||
},
|
||||
{
|
||||
id: 'cycling-outdoor',
|
||||
name: 'Cycling (Outdoor)',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'body weight',
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['quadriceps', 'hamstrings', 'calves', 'glutes'],
|
||||
instructions: ['Cycle outdoors at a steady pace for the desired duration or distance.']
|
||||
},
|
||||
{
|
||||
id: 'swimming',
|
||||
name: 'Swimming',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'body weight',
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['lats', 'shoulders', 'core', 'quadriceps'],
|
||||
instructions: ['Swim laps at a steady pace using your preferred stroke.']
|
||||
},
|
||||
{
|
||||
id: 'rowing-machine',
|
||||
name: 'Rowing Machine',
|
||||
@@ -910,6 +977,52 @@ export const exercises: Exercise[] = [
|
||||
'Return to the starting position by extending arms, then bending knees.'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'rowing-outdoor',
|
||||
name: 'Rowing (Outdoor)',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'body weight',
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['lats', 'biceps', 'quadriceps', 'core', 'shoulders'],
|
||||
instructions: ['Row on water at a steady pace for the desired duration or distance.']
|
||||
},
|
||||
{
|
||||
id: 'hiking',
|
||||
name: 'Hiking',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'body weight',
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['quadriceps', 'hamstrings', 'calves', 'glutes', 'core'],
|
||||
instructions: ['Hike at a steady pace on trails or uneven terrain.']
|
||||
},
|
||||
{
|
||||
id: 'elliptical',
|
||||
name: 'Elliptical',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'machine',
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['quadriceps', 'hamstrings', 'glutes', 'calves'],
|
||||
instructions: ['Use the elliptical machine at a steady pace for the desired duration.']
|
||||
},
|
||||
{
|
||||
id: 'stair-climber',
|
||||
name: 'Stair Climber',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'machine',
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['quadriceps', 'hamstrings', 'glutes', 'calves'],
|
||||
instructions: ['Climb at a steady pace on the stair climber for the desired duration.']
|
||||
},
|
||||
{
|
||||
id: 'jump-rope',
|
||||
name: 'Jump Rope',
|
||||
bodyPart: 'cardio',
|
||||
equipment: 'body weight',
|
||||
metrics: ['duration', 'reps', 'rpe'],
|
||||
target: 'cardiovascular system',
|
||||
secondaryMuscles: ['calves', 'shoulders', 'forearms', 'core'],
|
||||
instructions: ['Jump rope at a steady pace, landing lightly on the balls of your feet.']
|
||||
},
|
||||
|
||||
// === ADDITIONAL COMPOUND MOVEMENTS ===
|
||||
{
|
||||
@@ -931,6 +1044,7 @@ export const exercises: Exercise[] = [
|
||||
name: "Farmer's Walk",
|
||||
bodyPart: 'core',
|
||||
equipment: 'dumbbell',
|
||||
bilateral: true,
|
||||
target: 'forearms',
|
||||
secondaryMuscles: ['traps', 'core', 'grip'],
|
||||
instructions: [
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface WorkoutSet {
|
||||
reps: number | null;
|
||||
weight: number | null;
|
||||
rpe: number | null;
|
||||
distance: number | null;
|
||||
duration: number | null;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
@@ -24,7 +26,7 @@ export interface TemplateData {
|
||||
name: string;
|
||||
exercises: Array<{
|
||||
exerciseId: string;
|
||||
sets: Array<{ reps?: number; weight?: number; rpe?: number }>;
|
||||
sets: Array<{ reps?: number; weight?: number; rpe?: number; distance?: number; duration?: number }>;
|
||||
restTime?: number;
|
||||
}>;
|
||||
}
|
||||
@@ -55,7 +57,7 @@ export interface RemoteState {
|
||||
}
|
||||
|
||||
function createEmptySet(): WorkoutSet {
|
||||
return { reps: null, weight: null, rpe: null, completed: false };
|
||||
return { reps: null, weight: null, rpe: null, distance: null, duration: null, completed: false };
|
||||
}
|
||||
|
||||
function saveToStorage(state: StoredState) {
|
||||
@@ -213,6 +215,8 @@ export function createWorkout() {
|
||||
reps: s.reps ?? null,
|
||||
weight: s.weight ?? null,
|
||||
rpe: s.rpe ?? null,
|
||||
distance: s.distance ?? null,
|
||||
duration: s.duration ?? null,
|
||||
completed: false
|
||||
}))
|
||||
: [createEmptySet()],
|
||||
@@ -356,9 +360,11 @@ export function createWorkout() {
|
||||
sets: e.sets
|
||||
.filter((s) => s.completed)
|
||||
.map((s) => ({
|
||||
reps: s.reps ?? 0,
|
||||
weight: s.weight ?? 0,
|
||||
reps: s.reps ?? undefined,
|
||||
weight: s.weight ?? undefined,
|
||||
rpe: s.rpe ?? undefined,
|
||||
distance: s.distance ?? undefined,
|
||||
duration: s.duration ?? undefined,
|
||||
completed: true
|
||||
}))
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user