mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-22 16:51:33 +00:00

Implements database tables and services for workout and template storage: - Updates schema to version 5 with new workout and template tables - Adds WorkoutService for CRUD operations on workouts - Enhances TemplateService for template management - Creates NostrWorkoutService for bidirectional Nostr event handling - Implements React hooks for database access - Connects workout store to database layer for persistence - Improves offline support with publication queue This change ensures workouts and templates are properly saved to SQLite and can be referenced across app sessions, while maintaining Nostr integration for social sharing.
264 lines
6.1 KiB
TypeScript
264 lines
6.1 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;
|
|
duration?: number;
|
|
}
|
|
|
|
/**
|
|
* Exercise within a workout
|
|
*/
|
|
export interface WorkoutExercise extends BaseExercise {
|
|
exerciseId?: string;
|
|
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; // Keep only one templateId property
|
|
templatePubkey?: string; // Add this for template references
|
|
|
|
// Add shareStatus property
|
|
shareStatus?: 'local' | 'public' | 'limited';
|
|
|
|
// Workout configuration
|
|
rounds?: number;
|
|
duration?: number;
|
|
interval?: number;
|
|
restBetweenRounds?: number;
|
|
|
|
// Workout metrics
|
|
totalVolume?: number;
|
|
totalReps?: number;
|
|
averageRpe?: number;
|
|
|
|
// Completion tracking
|
|
isCompleted: boolean;
|
|
roundsCompleted?: number;
|
|
exercisesCompleted?: number;
|
|
|
|
// For Nostr integration
|
|
nostrEventId?: string;
|
|
}
|
|
|
|
/**
|
|
* Options for completing a workout
|
|
*/
|
|
export interface WorkoutCompletionOptions {
|
|
// Storage option
|
|
storageType: 'local_only' | 'publish_complete' | 'publish_limited';
|
|
|
|
// Social sharing option
|
|
shareOnSocial: boolean;
|
|
socialMessage?: string;
|
|
|
|
// Template update options
|
|
templateAction: 'keep_original' | 'update_existing' | 'save_as_new';
|
|
newTemplateName?: 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)
|
|
};
|
|
} |