mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-22 16:51:33 +00:00
242 lines
5.6 KiB
TypeScript
242 lines
5.6 KiB
TypeScript
![]() |
// types/workout.ts
|
||
|
import type { WorkoutTemplate, TemplateType } from './templates';
|
||
|
import type { BaseExercise } from './exercise';
|
||
|
import type { SyncableContent } from './shared';
|
||
|
import type { NostrEvent } from './nostr';
|
||
|
|
||
|
/**
|
||
|
* Core workout status types
|
||
|
*/
|
||
|
export type WorkoutStatus = 'idle' | 'active' | 'paused' | 'completed';
|
||
|
|
||
|
/**
|
||
|
* Individual workout set
|
||
|
*/
|
||
|
export interface WorkoutSet {
|
||
|
id: string;
|
||
|
weight?: number;
|
||
|
reps?: number;
|
||
|
rpe?: number;
|
||
|
type: 'warmup' | 'normal' | 'drop' | 'failure';
|
||
|
isCompleted: boolean;
|
||
|
notes?: string;
|
||
|
timestamp?: number;
|
||
|
lastUpdated?: number;
|
||
|
completedAt?: number;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Exercise within a workout
|
||
|
*/
|
||
|
export interface WorkoutExercise extends BaseExercise {
|
||
|
sets: WorkoutSet[];
|
||
|
targetSets?: number;
|
||
|
targetReps?: number;
|
||
|
notes?: string;
|
||
|
restTime?: number; // Rest time in seconds
|
||
|
isCompleted?: boolean;
|
||
|
lastUpdated?: number;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Active workout tracking
|
||
|
*/
|
||
|
export interface Workout extends SyncableContent {
|
||
|
id: string;
|
||
|
title: string;
|
||
|
type: TemplateType;
|
||
|
exercises: WorkoutExercise[];
|
||
|
startTime: number;
|
||
|
endTime?: number;
|
||
|
notes?: string;
|
||
|
lastUpdated?: number;
|
||
|
tags?: string[];
|
||
|
|
||
|
// Template reference if workout was started from template
|
||
|
templateId?: string;
|
||
|
|
||
|
// Workout configuration
|
||
|
rounds?: number;
|
||
|
duration?: number; // Total duration in seconds
|
||
|
interval?: number; // For EMOM/interval workouts
|
||
|
restBetweenRounds?: number;
|
||
|
|
||
|
// Workout metrics
|
||
|
totalVolume?: number;
|
||
|
totalReps?: number;
|
||
|
averageRpe?: number;
|
||
|
|
||
|
// Completion tracking
|
||
|
isCompleted: boolean;
|
||
|
roundsCompleted?: number;
|
||
|
exercisesCompleted?: number;
|
||
|
|
||
|
// For Nostr integration
|
||
|
nostrEventId?: string;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Personal Records
|
||
|
*/
|
||
|
export interface PersonalRecord {
|
||
|
id: string;
|
||
|
exerciseId: string;
|
||
|
metric: 'weight' | 'reps' | 'volume' | 'time';
|
||
|
value: number;
|
||
|
workoutId: string;
|
||
|
achievedAt: number;
|
||
|
|
||
|
// Context about the PR
|
||
|
exercise: {
|
||
|
title: string;
|
||
|
equipment?: string;
|
||
|
};
|
||
|
previousValue?: number;
|
||
|
notes?: string;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Workout Summary Statistics
|
||
|
*/
|
||
|
export interface WorkoutSummary {
|
||
|
id: string;
|
||
|
title: string;
|
||
|
type: TemplateType;
|
||
|
duration: number; // Total time in milliseconds
|
||
|
startTime: number;
|
||
|
endTime: number;
|
||
|
|
||
|
// Overall stats
|
||
|
exerciseCount: number;
|
||
|
completedExercises: number;
|
||
|
totalVolume: number;
|
||
|
totalReps: number;
|
||
|
averageRpe?: number;
|
||
|
|
||
|
// Exercise-specific summaries
|
||
|
exerciseSummaries: Array<{
|
||
|
exerciseId: string;
|
||
|
title: string;
|
||
|
setCount: number;
|
||
|
completedSets: number;
|
||
|
volume: number;
|
||
|
peakWeight?: number;
|
||
|
totalReps: number;
|
||
|
averageRpe?: number;
|
||
|
}>;
|
||
|
|
||
|
// Achievements
|
||
|
personalRecords: PersonalRecord[];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Rest Timer State
|
||
|
*/
|
||
|
export interface RestTimer {
|
||
|
isActive: boolean;
|
||
|
duration: number; // Total rest duration in seconds
|
||
|
remaining: number; // Remaining time in seconds
|
||
|
exerciseId?: string; // Associated exercise if any
|
||
|
setIndex?: number; // Associated set if any
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Global Workout State
|
||
|
*/
|
||
|
export interface WorkoutState {
|
||
|
status: WorkoutStatus;
|
||
|
activeWorkout: Workout | null;
|
||
|
currentExerciseIndex: number;
|
||
|
currentSetIndex: number;
|
||
|
elapsedTime: number; // Total workout time in milliseconds
|
||
|
lastSaved?: number; // Timestamp of last save
|
||
|
restTimer: RestTimer;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Workout Actions
|
||
|
*/
|
||
|
export type WorkoutAction =
|
||
|
| { type: 'START_WORKOUT'; payload: Partial<Workout> }
|
||
|
| { type: 'PAUSE_WORKOUT' }
|
||
|
| { type: 'RESUME_WORKOUT' }
|
||
|
| { type: 'COMPLETE_WORKOUT' }
|
||
|
| { type: 'UPDATE_SET'; payload: { exerciseIndex: number; setIndex: number; data: Partial<WorkoutSet> } }
|
||
|
| { type: 'NEXT_EXERCISE' }
|
||
|
| { type: 'PREVIOUS_EXERCISE' }
|
||
|
| { type: 'START_REST'; payload: number }
|
||
|
| { type: 'STOP_REST' }
|
||
|
| { type: 'TICK'; payload: number }
|
||
|
| { type: 'RESET' };
|
||
|
|
||
|
/**
|
||
|
* Helper functions
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Converts a template to an active workout
|
||
|
*/
|
||
|
export function templateToWorkout(template: WorkoutTemplate): Workout {
|
||
|
return {
|
||
|
id: crypto.randomUUID(),
|
||
|
title: template.title,
|
||
|
type: template.type,
|
||
|
exercises: template.exercises.map(ex => ({
|
||
|
...ex.exercise,
|
||
|
sets: Array(ex.targetSets).fill({
|
||
|
id: crypto.randomUUID(),
|
||
|
type: 'normal',
|
||
|
reps: ex.targetReps,
|
||
|
isCompleted: false
|
||
|
}),
|
||
|
targetSets: ex.targetSets,
|
||
|
targetReps: ex.targetReps,
|
||
|
notes: ex.notes
|
||
|
})),
|
||
|
templateId: template.id,
|
||
|
startTime: Date.now(),
|
||
|
isCompleted: false,
|
||
|
rounds: template.rounds,
|
||
|
duration: template.duration,
|
||
|
interval: template.interval,
|
||
|
restBetweenRounds: template.restBetweenRounds,
|
||
|
created_at: Date.now(),
|
||
|
availability: {
|
||
|
source: ['local']
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a Nostr workout record event
|
||
|
*/
|
||
|
export function createNostrWorkoutEvent(workout: Workout): NostrEvent {
|
||
|
const exerciseTags = workout.exercises.flatMap(exercise =>
|
||
|
exercise.sets.map(set => [
|
||
|
'exercise',
|
||
|
`33401:${exercise.id}`,
|
||
|
set.weight?.toString() || '',
|
||
|
set.reps?.toString() || '',
|
||
|
set.rpe?.toString() || '',
|
||
|
set.type
|
||
|
])
|
||
|
);
|
||
|
|
||
|
const workoutTags = workout.tags ? workout.tags.map(tag => ['t', tag]) : [];
|
||
|
|
||
|
return {
|
||
|
kind: 33403,
|
||
|
content: workout.notes || '',
|
||
|
tags: [
|
||
|
['d', workout.id],
|
||
|
['title', workout.title],
|
||
|
['type', workout.type],
|
||
|
['start', Math.floor(workout.startTime / 1000).toString()],
|
||
|
['end', Math.floor(workout.endTime! / 1000).toString()],
|
||
|
['completed', workout.isCompleted.toString()],
|
||
|
...exerciseTags,
|
||
|
...workoutTags
|
||
|
],
|
||
|
created_at: Math.floor(Date.now() / 1000)
|
||
|
};
|
||
|
}
|