// 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 } | { type: 'PAUSE_WORKOUT' } | { type: 'RESUME_WORKOUT' } | { type: 'COMPLETE_WORKOUT' } | { type: 'UPDATE_SET'; payload: { exerciseIndex: number; setIndex: number; data: Partial } } | { 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) }; }