2025-02-24 22:27:01 -05:00
|
|
|
// types/templates.ts
|
|
|
|
import { BaseExercise, Equipment, ExerciseCategory, SetType } from './exercise';
|
2025-02-19 21:39:47 -05:00
|
|
|
import { StorageSource, SyncableContent } from './shared';
|
|
|
|
import { generateId } from '@/utils/ids';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Template Classifications
|
2025-02-24 22:27:01 -05:00
|
|
|
* Aligned with NIP-33402
|
2025-02-19 21:39:47 -05:00
|
|
|
*/
|
|
|
|
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;
|
2025-02-27 20:24:04 -05:00
|
|
|
equipment?: string;
|
2025-02-19 21:39:47 -05:00
|
|
|
notes?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface TemplateExerciseConfig {
|
2025-03-08 15:48:07 -05:00
|
|
|
id?: string; // Add this line
|
2025-02-19 21:39:47 -05:00
|
|
|
exercise: BaseExercise;
|
2025-03-08 15:48:07 -05:00
|
|
|
targetSets?: number;
|
|
|
|
targetReps?: number;
|
|
|
|
targetWeight?: number;
|
2025-02-19 21:39:47 -05:00
|
|
|
weight?: number;
|
|
|
|
rpe?: number;
|
2025-02-24 22:27:01 -05:00
|
|
|
setType?: SetType;
|
2025-02-19 21:39:47 -05:00
|
|
|
restSeconds?: number;
|
|
|
|
notes?: string;
|
2025-02-24 22:27:01 -05:00
|
|
|
|
|
|
|
// Format configuration from NIP-33401
|
2025-02-19 21:39:47 -05:00
|
|
|
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';
|
|
|
|
};
|
2025-02-24 22:27:01 -05:00
|
|
|
|
|
|
|
// For timed workouts
|
|
|
|
duration?: number;
|
|
|
|
interval?: number;
|
|
|
|
|
|
|
|
// For circuit/EMOM
|
|
|
|
position?: number;
|
|
|
|
roundRest?: number;
|
2025-02-19 21:39:47 -05:00
|
|
|
}
|
|
|
|
|
2025-03-13 22:39:28 -04:00
|
|
|
export interface TemplateExerciseWithData {
|
|
|
|
id: string;
|
|
|
|
exercise: BaseExercise;
|
|
|
|
displayOrder: number;
|
|
|
|
targetSets: number | null;
|
|
|
|
targetReps: number | null;
|
|
|
|
targetWeight: number | null;
|
|
|
|
notes: string | null;
|
|
|
|
nostrReference?: string | null;
|
|
|
|
}
|
|
|
|
|
2025-02-19 21:39:47 -05:00
|
|
|
/**
|
|
|
|
* 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;
|
2025-02-24 22:27:01 -05:00
|
|
|
notes?: string;
|
2025-02-19 21:39:47 -05:00
|
|
|
tags: string[];
|
2025-02-24 22:27:01 -05:00
|
|
|
|
|
|
|
// Workout structure
|
|
|
|
rounds?: number;
|
|
|
|
duration?: number;
|
|
|
|
interval?: number;
|
|
|
|
restBetweenRounds?: number;
|
|
|
|
|
|
|
|
// Metadata
|
2025-02-19 21:39:47 -05:00
|
|
|
metadata?: {
|
|
|
|
lastUsed?: number;
|
|
|
|
useCount?: number;
|
|
|
|
averageDuration?: number;
|
2025-02-24 22:27:01 -05:00
|
|
|
completionRate?: number;
|
2025-02-19 21:39:47 -05:00
|
|
|
};
|
2025-02-24 22:27:01 -05:00
|
|
|
|
2025-02-19 21:39:47 -05:00
|
|
|
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;
|
2025-03-13 22:39:28 -04:00
|
|
|
lastUpdated?: number;
|
|
|
|
parentId?: string;
|
2025-02-19 21:39:47 -05:00
|
|
|
|
|
|
|
// 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;
|
2025-02-24 22:27:01 -05:00
|
|
|
relayUrls?: string[];
|
2025-03-13 22:39:28 -04:00
|
|
|
authorPubkey?: string;
|
|
|
|
|
|
|
|
// Template management
|
|
|
|
isArchived?: boolean;
|
2025-02-19 21:39:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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,
|
2025-03-08 15:48:07 -05:00
|
|
|
targetSets: ex.targetSets || 0, // Add default value
|
|
|
|
targetReps: ex.targetReps || 0, // Add default value
|
2025-02-19 21:39:47 -05:00
|
|
|
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,
|
2025-02-24 22:27:01 -05:00
|
|
|
equipment: 'barbell' as Equipment,
|
2025-02-19 21:39:47 -05:00
|
|
|
tags: [],
|
2025-02-24 22:27:01 -05:00
|
|
|
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'
|
|
|
|
},
|
2025-02-19 21:39:47 -05:00
|
|
|
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']
|
|
|
|
}
|
|
|
|
};
|
2025-02-24 22:27:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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}`,
|
2025-03-08 15:48:07 -05:00
|
|
|
(ex.targetSets || 0).toString(),
|
|
|
|
(ex.targetReps || 0).toString(),
|
2025-02-24 22:27:01 -05:00
|
|
|
ex.setType || 'normal'
|
|
|
|
]),
|
|
|
|
...template.tags.map(tag => ['t', tag])
|
|
|
|
],
|
|
|
|
created_at: Math.floor(Date.now() / 1000)
|
|
|
|
};
|
2025-02-19 21:39:47 -05:00
|
|
|
}
|