POWR/types/templates.ts
DocNR 29c4dd1675 feat(database): Add workout and template persistence
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.
2025-03-08 15:48:07 -05:00

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)
};
}