mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 19:01:18 +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.
276 lines
6.3 KiB
TypeScript
276 lines
6.3 KiB
TypeScript
// types/templates.ts
|
|
import { BaseExercise, Equipment, ExerciseCategory, SetType } from './exercise';
|
|
import { StorageSource, SyncableContent } from './shared';
|
|
import { generateId } from '@/utils/ids';
|
|
|
|
/**
|
|
* Template Classifications
|
|
* Aligned with NIP-33402
|
|
*/
|
|
export type TemplateType = 'strength' | 'circuit' | 'emom' | 'amrap';
|
|
|
|
export type TemplateCategory =
|
|
| 'Full Body'
|
|
| 'Push/Pull/Legs'
|
|
| 'Upper/Lower'
|
|
| 'Custom'
|
|
| 'Cardio'
|
|
| 'CrossFit'
|
|
| 'Strength'
|
|
| 'Conditioning';
|
|
|
|
/**
|
|
* Exercise configurations within templates
|
|
*/
|
|
export interface TemplateExerciseDisplay {
|
|
title: string;
|
|
targetSets: number;
|
|
targetReps: number;
|
|
equipment?: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface TemplateExerciseConfig {
|
|
id?: string; // Add this line
|
|
exercise: BaseExercise;
|
|
targetSets?: number;
|
|
targetReps?: number;
|
|
targetWeight?: number;
|
|
weight?: number;
|
|
rpe?: number;
|
|
setType?: SetType;
|
|
restSeconds?: number;
|
|
notes?: string;
|
|
|
|
// Format configuration from NIP-33401
|
|
format?: {
|
|
weight?: boolean;
|
|
reps?: boolean;
|
|
rpe?: boolean;
|
|
set_type?: boolean;
|
|
};
|
|
format_units?: {
|
|
weight?: 'kg' | 'lbs';
|
|
reps?: 'count';
|
|
rpe?: '0-10';
|
|
set_type?: 'warmup|normal|drop|failure';
|
|
};
|
|
|
|
// For timed workouts
|
|
duration?: number;
|
|
interval?: number;
|
|
|
|
// For circuit/EMOM
|
|
position?: number;
|
|
roundRest?: number;
|
|
}
|
|
|
|
/**
|
|
* Template versioning and derivation tracking
|
|
*/
|
|
export interface TemplateSource {
|
|
id: string;
|
|
eventId?: string;
|
|
authorName?: string;
|
|
authorPubkey?: string;
|
|
version?: number;
|
|
}
|
|
|
|
/**
|
|
* Base template properties shared between UI and database
|
|
*/
|
|
export interface TemplateBase {
|
|
id: string;
|
|
title: string;
|
|
type: TemplateType;
|
|
category: TemplateCategory;
|
|
description?: string;
|
|
notes?: string;
|
|
tags: string[];
|
|
|
|
// Workout structure
|
|
rounds?: number;
|
|
duration?: number;
|
|
interval?: number;
|
|
restBetweenRounds?: number;
|
|
|
|
// Metadata
|
|
metadata?: {
|
|
lastUsed?: number;
|
|
useCount?: number;
|
|
averageDuration?: number;
|
|
completionRate?: number;
|
|
};
|
|
|
|
author?: {
|
|
name: string;
|
|
pubkey?: string;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* UI Template - Used for display and interaction
|
|
*/
|
|
export interface Template extends TemplateBase {
|
|
exercises: TemplateExerciseDisplay[];
|
|
source: 'local' | 'powr' | 'nostr';
|
|
isFavorite: boolean; // Required for UI state
|
|
}
|
|
|
|
/**
|
|
* Full Template - Used for database storage and Nostr events
|
|
*/
|
|
export interface WorkoutTemplate extends TemplateBase, SyncableContent {
|
|
exercises: TemplateExerciseConfig[];
|
|
isPublic: boolean;
|
|
version: number;
|
|
lastUpdated?: number; // Add this line
|
|
parentId?: string; // Add this line
|
|
|
|
// Template configuration
|
|
format?: {
|
|
weight?: boolean;
|
|
reps?: boolean;
|
|
rpe?: boolean;
|
|
set_type?: boolean;
|
|
};
|
|
format_units?: {
|
|
weight: 'kg' | 'lbs';
|
|
reps: 'count';
|
|
rpe: '0-10';
|
|
set_type: 'warmup|normal|drop|failure';
|
|
};
|
|
|
|
// Template derivation
|
|
sourceTemplate?: TemplateSource;
|
|
derivatives?: {
|
|
count: number;
|
|
lastCreated: number;
|
|
};
|
|
|
|
// Nostr integration
|
|
nostrEventId?: string;
|
|
relayUrls?: string[];
|
|
}
|
|
|
|
/**
|
|
* Helper Functions
|
|
*/
|
|
|
|
/**
|
|
* Gets a display string for the template source
|
|
*/
|
|
export function getSourceDisplay(template: WorkoutTemplate): string {
|
|
if (!template.sourceTemplate) {
|
|
return template.availability.source.includes('nostr')
|
|
? 'NOSTR'
|
|
: template.availability.source.includes('powr')
|
|
? 'POWR'
|
|
: 'Local Template';
|
|
}
|
|
|
|
const author = template.sourceTemplate.authorName || 'Unknown Author';
|
|
if (template.sourceTemplate.version) {
|
|
return `Modified from ${author} (v${template.sourceTemplate.version})`;
|
|
}
|
|
return `Original by ${author}`;
|
|
}
|
|
|
|
/**
|
|
* Converts a WorkoutTemplate to Template for UI display
|
|
*/
|
|
export function toTemplateDisplay(template: WorkoutTemplate): Template {
|
|
return {
|
|
id: template.id,
|
|
title: template.title,
|
|
type: template.type,
|
|
category: template.category,
|
|
description: template.description,
|
|
exercises: template.exercises.map(ex => ({
|
|
title: ex.exercise.title,
|
|
targetSets: ex.targetSets || 0, // Add default value
|
|
targetReps: ex.targetReps || 0, // Add default value
|
|
notes: ex.notes
|
|
})),
|
|
tags: template.tags,
|
|
source: template.availability.source.includes('nostr')
|
|
? 'nostr'
|
|
: template.availability.source.includes('powr')
|
|
? 'powr'
|
|
: 'local',
|
|
isFavorite: false,
|
|
metadata: template.metadata,
|
|
author: template.author
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Converts a Template to WorkoutTemplate for storage/sync
|
|
*/
|
|
export function toWorkoutTemplate(template: Template): WorkoutTemplate {
|
|
return {
|
|
...template,
|
|
exercises: template.exercises.map(ex => ({
|
|
exercise: {
|
|
id: generateId(),
|
|
title: ex.title,
|
|
type: 'strength',
|
|
category: 'Push' as ExerciseCategory,
|
|
equipment: 'barbell' as Equipment,
|
|
tags: [],
|
|
format: {
|
|
weight: true,
|
|
reps: true,
|
|
rpe: true,
|
|
set_type: true
|
|
},
|
|
format_units: {
|
|
weight: 'kg',
|
|
reps: 'count',
|
|
rpe: '0-10',
|
|
set_type: 'warmup|normal|drop|failure'
|
|
},
|
|
availability: {
|
|
source: ['local']
|
|
},
|
|
created_at: Date.now()
|
|
},
|
|
targetSets: ex.targetSets,
|
|
targetReps: ex.targetReps,
|
|
notes: ex.notes
|
|
})),
|
|
isPublic: false,
|
|
version: 1,
|
|
created_at: Date.now(),
|
|
availability: {
|
|
source: ['local']
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a Nostr event from a template (NIP-33402)
|
|
*/
|
|
export function createNostrTemplateEvent(template: WorkoutTemplate) {
|
|
return {
|
|
kind: 33402,
|
|
content: template.description || '',
|
|
tags: [
|
|
['d', template.id],
|
|
['title', template.title],
|
|
['type', template.type],
|
|
...(template.rounds ? [['rounds', template.rounds.toString()]] : []),
|
|
...(template.duration ? [['duration', template.duration.toString()]] : []),
|
|
...(template.interval ? [['interval', template.interval.toString()]] : []),
|
|
...template.exercises.map(ex => [
|
|
'exercise',
|
|
`33401:${ex.exercise.id}`,
|
|
(ex.targetSets || 0).toString(),
|
|
(ex.targetReps || 0).toString(),
|
|
ex.setType || 'normal'
|
|
]),
|
|
...template.tags.map(tag => ['t', tag])
|
|
],
|
|
created_at: Math.floor(Date.now() / 1000)
|
|
};
|
|
} |