mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-22 16:51:33 +00:00
351 lines
11 KiB
TypeScript
351 lines
11 KiB
TypeScript
// app/(workout)/create-template.tsx
|
|
import React, { useState } from 'react';
|
|
import {
|
|
View, ScrollView, StyleSheet, Platform, TextInput,
|
|
TouchableOpacity, Alert, KeyboardAvoidingView
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { Feather } from '@expo/vector-icons';
|
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
|
import { router, useLocalSearchParams } from 'expo-router';
|
|
import { useWorkout } from '@/contexts/WorkoutContext';
|
|
import { Input } from '@/components/form/Input';
|
|
import { Select } from '@/components/form/Select';
|
|
import { ThemedText } from '@/components/ThemedText';
|
|
import EditableText from '@/components/EditableText';
|
|
import { spacing } from '@/styles/sharedStyles';
|
|
import { generateId } from '@/utils/ids';
|
|
import { BaseExercise } from '@/types/exercise';
|
|
import { WorkoutTemplate, TemplateCategory } from '@/types/workout';
|
|
import { NostrEventKind } from '@/types/events';
|
|
|
|
const WORKOUT_TYPES: Array<{ label: string; value: WorkoutTemplate['type'] }> = [
|
|
{ label: 'Strength', value: 'strength' },
|
|
{ label: 'Circuit', value: 'circuit' },
|
|
{ label: 'EMOM', value: 'emom' },
|
|
{ label: 'AMRAP', value: 'amrap' }
|
|
];
|
|
|
|
const TEMPLATE_CATEGORIES: Array<{ label: string; value: TemplateCategory }> = [
|
|
{ label: 'Full Body', value: 'Full Body' },
|
|
{ label: 'Upper/Lower', value: 'Upper/Lower' },
|
|
{ label: 'Push/Pull/Legs', value: 'Push/Pull/Legs' },
|
|
{ label: 'Custom', value: 'Custom' }
|
|
];
|
|
|
|
interface CreateTemplateScreenProps {
|
|
initialExercises?: BaseExercise[];
|
|
}
|
|
|
|
function CreateTemplateScreen({ initialExercises = [] }: CreateTemplateScreenProps) {
|
|
const { colors } = useColorScheme();
|
|
const params = useLocalSearchParams();
|
|
const { saveTemplate } = useWorkout();
|
|
|
|
const parsedExercises = params.exercises ?
|
|
JSON.parse(decodeURIComponent(params.exercises as string)) as BaseExercise[] :
|
|
initialExercises;
|
|
|
|
// Form state matching Nostr spec
|
|
const [title, setTitle] = useState('New Template');
|
|
const [description, setDescription] = useState('');
|
|
const [workoutType, setWorkoutType] = useState<WorkoutTemplate['type']>('strength');
|
|
const [category, setCategory] = useState<TemplateCategory>('Custom');
|
|
const [exercises, setExercises] = useState<BaseExercise[]>(parsedExercises);
|
|
const [rounds, setRounds] = useState('');
|
|
const [duration, setDuration] = useState('');
|
|
const [intervalTime, setIntervalTime] = useState('');
|
|
const [restBetweenRounds, setRestBetweenRounds] = useState('');
|
|
const [tags, setTags] = useState<string[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleWorkoutTypeChange = (value: string | string[]) => {
|
|
if (typeof value === 'string') {
|
|
setWorkoutType(value as WorkoutTemplate['type']);
|
|
}
|
|
};
|
|
|
|
const handleCategoryChange = (value: string | string[]) => {
|
|
if (typeof value === 'string') {
|
|
setCategory(value as TemplateCategory);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
if (!title.trim()) {
|
|
Alert.alert('Error', 'Template must have a title');
|
|
return;
|
|
}
|
|
|
|
if (exercises.length === 0) {
|
|
Alert.alert('Error', 'Template must include at least one exercise');
|
|
return;
|
|
}
|
|
|
|
// Create template following NIP-XX spec
|
|
const template: WorkoutTemplate = {
|
|
id: generateId(),
|
|
title: title.trim(),
|
|
type: workoutType,
|
|
description: description,
|
|
category: category,
|
|
exercises: exercises.map(exercise => ({
|
|
exercise,
|
|
targetSets: 0,
|
|
targetReps: 0,
|
|
})),
|
|
tags: tags,
|
|
rounds: rounds ? parseInt(rounds) : undefined,
|
|
duration: duration ? parseInt(duration) * 60 : undefined,
|
|
interval: intervalTime ? parseInt(intervalTime) : undefined,
|
|
restBetweenRounds: restBetweenRounds ? parseInt(restBetweenRounds) : undefined,
|
|
isPublic: false,
|
|
created_at: Date.now(),
|
|
availability: {
|
|
source: ['local']
|
|
},
|
|
notes: ''
|
|
};
|
|
|
|
await saveTemplate(template);
|
|
router.back();
|
|
} catch (error) {
|
|
console.error('Error saving template:', error);
|
|
setError('Failed to save template. Please try again.');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView
|
|
style={[styles.container, { backgroundColor: colors.background }]}
|
|
edges={['top', 'left', 'right']}
|
|
>
|
|
<View style={[styles.header, { backgroundColor: colors.background, borderBottomColor: colors.border }]}>
|
|
<View style={styles.headerRow}>
|
|
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
|
|
<Feather name="arrow-left" size={24} color={colors.text} />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.saveButton, { backgroundColor: colors.primary }]}
|
|
onPress={handleSave}
|
|
>
|
|
<ThemedText style={[styles.saveButtonText, { color: colors.background }]}>
|
|
Save Template
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
style={styles.keyboardAvoidView}
|
|
>
|
|
<ScrollView
|
|
style={styles.scrollContainer}
|
|
contentContainerStyle={styles.scrollContent}
|
|
>
|
|
<View style={styles.titleSection}>
|
|
<EditableText
|
|
value={title}
|
|
onChangeText={setTitle}
|
|
style={styles.titleContainer}
|
|
textStyle={[styles.title, { color: colors.text }]}
|
|
placeholder="Template Name"
|
|
/>
|
|
</View>
|
|
|
|
<View style={[styles.section, { backgroundColor: colors.cardBg }]}>
|
|
<Select
|
|
label="Workout Type"
|
|
value={workoutType}
|
|
onValueChange={handleWorkoutTypeChange}
|
|
items={WORKOUT_TYPES}
|
|
required
|
|
/>
|
|
<Select
|
|
label="Category"
|
|
value={category}
|
|
onValueChange={handleCategoryChange}
|
|
items={TEMPLATE_CATEGORIES}
|
|
required
|
|
/>
|
|
<Input
|
|
label="Description"
|
|
value={description}
|
|
onChangeText={setDescription}
|
|
multiline
|
|
numberOfLines={4}
|
|
placeholder="Add a description for your template"
|
|
/>
|
|
</View>
|
|
|
|
{workoutType !== 'strength' && (
|
|
<View style={[styles.section, { backgroundColor: colors.cardBg }]}>
|
|
<ThemedText type="subtitle" style={styles.sectionTitle}>
|
|
Workout Parameters
|
|
</ThemedText>
|
|
|
|
<Input
|
|
label="Number of Rounds"
|
|
value={rounds}
|
|
onChangeText={setRounds}
|
|
keyboardType="numeric"
|
|
placeholder="e.g., 5"
|
|
/>
|
|
|
|
<Input
|
|
label="Total Duration (minutes)"
|
|
value={duration}
|
|
onChangeText={setDuration}
|
|
keyboardType="numeric"
|
|
placeholder="e.g., 20"
|
|
/>
|
|
|
|
<Input
|
|
label="Interval Time (seconds)"
|
|
value={intervalTime}
|
|
onChangeText={setIntervalTime}
|
|
keyboardType="numeric"
|
|
placeholder="e.g., 40"
|
|
/>
|
|
|
|
<Input
|
|
label="Rest Between Rounds (seconds)"
|
|
value={restBetweenRounds}
|
|
onChangeText={setRestBetweenRounds}
|
|
keyboardType="numeric"
|
|
placeholder="e.g., 60"
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
<View style={[styles.section, { backgroundColor: colors.cardBg }]}>
|
|
<ThemedText type="subtitle" style={styles.sectionTitle}>
|
|
Exercises ({exercises.length})
|
|
</ThemedText>
|
|
|
|
{exercises.map((exercise: BaseExercise, index: number) => (
|
|
<View
|
|
key={exercise.id}
|
|
style={[styles.exerciseCard, { borderBottomColor: colors.border }]}
|
|
>
|
|
<View style={styles.exerciseHeader}>
|
|
<ThemedText style={styles.exerciseName}>
|
|
{exercise.title}
|
|
</ThemedText>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
const newExercises = [...exercises];
|
|
newExercises.splice(index, 1);
|
|
setExercises(newExercises);
|
|
}}
|
|
>
|
|
<Feather name="trash-2" size={20} color={colors.error} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
))}
|
|
|
|
<TouchableOpacity
|
|
style={[styles.addButton, { backgroundColor: colors.primary }]}
|
|
onPress={() => {
|
|
router.push({
|
|
pathname: '/(workout)/add-exercises' as const,
|
|
params: { mode: 'template' }
|
|
});
|
|
}}
|
|
>
|
|
<ThemedText style={[styles.addButtonText, { color: colors.background }]}>
|
|
Add Exercise
|
|
</ThemedText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
keyboardAvoidView: {
|
|
flex: 1,
|
|
},
|
|
header: {
|
|
paddingHorizontal: spacing.medium,
|
|
paddingVertical: spacing.small,
|
|
borderBottomWidth: 1,
|
|
},
|
|
headerRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
},
|
|
backButton: {
|
|
padding: spacing.small,
|
|
},
|
|
saveButton: {
|
|
paddingVertical: spacing.small,
|
|
paddingHorizontal: spacing.medium,
|
|
borderRadius: 8,
|
|
},
|
|
saveButtonText: {
|
|
fontWeight: '600',
|
|
fontSize: 16,
|
|
},
|
|
scrollContainer: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
padding: spacing.medium,
|
|
},
|
|
titleSection: {
|
|
marginBottom: spacing.medium,
|
|
},
|
|
titleContainer: {
|
|
flex: 1,
|
|
},
|
|
title: {
|
|
fontSize: 32,
|
|
fontWeight: 'bold',
|
|
},
|
|
section: {
|
|
marginBottom: spacing.medium,
|
|
padding: spacing.medium,
|
|
borderRadius: 12,
|
|
gap: spacing.medium,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
},
|
|
exerciseCard: {
|
|
paddingVertical: spacing.medium,
|
|
borderBottomWidth: 1,
|
|
},
|
|
exerciseHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
},
|
|
exerciseName: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
addButton: {
|
|
padding: spacing.medium,
|
|
borderRadius: 8,
|
|
alignItems: 'center',
|
|
marginTop: spacing.small,
|
|
},
|
|
addButtonText: {
|
|
fontSize: 16,
|
|
fontWeight: '600',
|
|
},
|
|
});
|
|
|
|
export default CreateTemplateScreen; |