// components/library/NewTemplateSheet.tsx
import React, { useState, useEffect } from 'react';
import { View, ScrollView, TouchableOpacity, Modal } from 'react-native';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import {
Template,
TemplateType,
TemplateCategory,
TemplateExerciseDisplay
} from '@/types/templates';
import { ExerciseDisplay } from '@/types/exercise';
import { generateId } from '@/utils/ids';
import { useSQLiteContext } from 'expo-sqlite';
import { LibraryService } from '@/lib/db/services/LibraryService';
import { ChevronLeft, Dumbbell, Clock, RotateCw, List, Search, X } from 'lucide-react-native';
import { useColorScheme } from '@/lib/theme/useColorScheme';
interface NewTemplateSheetProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (template: Template) => void;
}
// Steps in template creation
type CreationStep = 'type' | 'info' | 'exercises' | 'config' | 'review';
// Purple color used throughout the app
const purpleColor = 'hsl(261, 90%, 66%)';
// Enhanced template exercise display that includes the original exercise object
interface EnhancedTemplateExerciseDisplay extends TemplateExerciseDisplay {
exercise: ExerciseDisplay;
}
// Step 0: Workout Type Selection
interface WorkoutTypeStepProps {
onSelectType: (type: TemplateType) => void;
onCancel: () => void;
}
function WorkoutTypeStep({ onSelectType, onCancel }: WorkoutTypeStepProps) {
const workoutTypes = [
{
type: 'strength' as TemplateType,
title: 'Strength Workout',
description: 'Traditional sets and reps with rest periods',
icon: Dumbbell,
available: true
},
{
type: 'circuit' as TemplateType,
title: 'Circuit Training',
description: 'Multiple exercises performed in sequence',
icon: RotateCw,
available: false
},
{
type: 'emom' as TemplateType,
title: 'EMOM Workout',
description: 'Every Minute On the Minute timed exercises',
icon: Clock,
available: false
},
{
type: 'amrap' as TemplateType,
title: 'AMRAP Workout',
description: 'As Many Rounds As Possible in a time cap',
icon: List,
available: false
}
];
return (
Select the type of workout template you want to create:
{workoutTypes.map(workout => (
{workout.available ? (
onSelectType(workout.type)}
className="flex-row justify-between items-center"
activeOpacity={0.7}
>
{workout.title}
{workout.description}
) : (
{workout.title}
{workout.description}
Coming Soon
)}
))}
);
}
// Step 1: Basic Info
interface BasicInfoStepProps {
title: string;
description: string;
category: TemplateCategory;
onTitleChange: (title: string) => void;
onDescriptionChange: (description: string) => void;
onCategoryChange: (category: string) => void;
onNext: () => void;
onCancel: () => void;
}
function BasicInfoStep({
title,
description,
category,
onTitleChange,
onDescriptionChange,
onCategoryChange,
onNext,
onCancel
}: BasicInfoStepProps) {
const categories: TemplateCategory[] = ['Full Body', 'Custom', 'Push/Pull/Legs', 'Upper/Lower', 'Conditioning'];
return (
Workout Name
{!title && (
* Required field
)}
Description (Optional)
Category
{categories.map((cat) => (
))}
);
}
// Step 2: Exercise Selection
interface ExerciseSelectionStepProps {
exercises: ExerciseDisplay[];
onExercisesSelected: (selected: ExerciseDisplay[]) => void;
onBack: () => void;
}
function ExerciseSelectionStep({
exercises,
onExercisesSelected,
onBack
}: ExerciseSelectionStepProps) {
const [selectedIds, setSelectedIds] = useState([]);
const [search, setSearch] = useState('');
const filteredExercises = exercises.filter(e =>
e.title.toLowerCase().includes(search.toLowerCase()) ||
e.tags.some(t => t.toLowerCase().includes(search.toLowerCase()))
);
const handleToggleSelection = (id: string) => {
setSelectedIds(prev =>
prev.includes(id)
? prev.filter(i => i !== id)
: [...prev, id]
);
};
const handleContinue = () => {
// Get the full exercise objects with their original IDs
const selected = exercises.filter(e => selectedIds.includes(e.id));
onExercisesSelected(selected);
};
return (
Selected: {selectedIds.length} exercises
{filteredExercises.map(exercise => (
handleToggleSelection(exercise.id)}
activeOpacity={0.7}
>
{exercise.title}
{exercise.category}
{exercise.equipment && (
{exercise.equipment}
)}
))}
{filteredExercises.length === 0 && (
No exercises found
)}
);
}
// Step 3: Exercise Configuration
interface ExerciseConfigStepProps {
exercises: ExerciseDisplay[];
config: EnhancedTemplateExerciseDisplay[];
onUpdateConfig: (index: number, sets: number, reps: number) => void;
onNext: () => void;
onBack: () => void;
}
function ExerciseConfigStep({
exercises,
config,
onUpdateConfig,
onNext,
onBack
}: ExerciseConfigStepProps) {
return (
{exercises.map((exercise, index) => (
{exercise.title}
Sets
{
const sets = text ? parseInt(text) : 0;
const reps = config[index]?.targetReps || 0;
onUpdateConfig(index, sets, reps);
}}
placeholder="Optional"
className="bg-input placeholder:text-muted-foreground"
/>
Reps
{
const reps = text ? parseInt(text) : 0;
const sets = config[index]?.targetSets || 0;
onUpdateConfig(index, sets, reps);
}}
placeholder="Optional"
className="bg-input placeholder:text-muted-foreground"
/>
))}
);
}
// Step 4: Review
interface ReviewStepProps {
title: string;
description: string;
category: TemplateCategory;
type: TemplateType;
exercises: EnhancedTemplateExerciseDisplay[];
onSubmit: () => void;
onBack: () => void;
}
function ReviewStep({
title,
description,
category,
type,
exercises,
onSubmit,
onBack
}: ReviewStepProps) {
return (
{title}
{description ? (
{description}
) : null}
{type}
{category}
Exercises
{exercises.map((exercise, index) => (
{exercise.title}
{exercise.targetSets || exercise.targetReps ?
`${exercise.targetSets || '–'} sets × ${exercise.targetReps || '–'} reps` :
'No prescription set'
}
))}
);
}
export function NewTemplateSheet({ isOpen, onClose, onSubmit }: NewTemplateSheetProps) {
const db = useSQLiteContext();
const [libraryService] = useState(() => new LibraryService(db));
const [step, setStep] = useState('type');
const [workoutType, setWorkoutType] = useState('strength');
const [exercises, setExercises] = useState([]);
const [selectedExercises, setSelectedExercises] = useState([]);
const [configuredExercises, setConfiguredExercises] = useState([]);
const { isDarkColorScheme } = useColorScheme();
// Template info
const [templateInfo, setTemplateInfo] = useState<{
title: string;
description: string;
category: TemplateCategory;
tags: string[];
}>({
title: '',
description: '',
category: 'Full Body',
tags: ['strength']
});
// Load exercises on mount
useEffect(() => {
const loadExercises = async () => {
try {
const data = await libraryService.getExercises();
setExercises(data);
} catch (error) {
console.error('Failed to load exercises:', error);
}
};
if (isOpen) {
loadExercises();
}
}, [isOpen, libraryService]);
// Reset state when sheet closes
useEffect(() => {
if (!isOpen) {
// Add a delay to ensure the closing animation completes first
const timer = setTimeout(() => {
setStep('type');
setWorkoutType('strength');
setSelectedExercises([]);
setConfiguredExercises([]);
setTemplateInfo({
title: '',
description: '',
category: 'Full Body',
tags: ['strength']
});
}, 300);
return () => clearTimeout(timer);
}
}, [isOpen]);
const handleGoBack = () => {
switch(step) {
case 'info': setStep('type'); break;
case 'exercises': setStep('info'); break;
case 'config': setStep('exercises'); break;
case 'review': setStep('config'); break;
}
};
const handleSelectType = (type: TemplateType) => {
setWorkoutType(type);
setStep('info');
};
const handleSubmitInfo = () => {
if (!templateInfo.title) return;
setStep('exercises');
};
const handleSelectExercises = (selected: ExerciseDisplay[]) => {
setSelectedExercises(selected);
// Pre-populate configured exercises with full exercise objects
const initialConfig = selected.map(exercise => ({
title: exercise.title,
exercise: exercise, // Store the complete exercise object with its original ID
targetSets: 0,
targetReps: 0
}));
setConfiguredExercises(initialConfig);
setStep('config');
};
const handleUpdateExerciseConfig = (index: number, sets: number, reps: number) => {
setConfiguredExercises(prev => {
const updated = [...prev];
updated[index] = {
...updated[index],
targetSets: sets,
targetReps: reps
};
return updated;
});
};
const handleConfigComplete = () => {
setStep('review');
};
const handleCreateTemplate = () => {
const newTemplate: Template = {
id: generateId(),
title: templateInfo.title,
description: templateInfo.description,
type: workoutType,
category: templateInfo.category,
exercises: configuredExercises,
tags: templateInfo.tags,
source: 'local',
isFavorite: false
};
// Close first, then submit with a small delay
onClose();
setTimeout(() => {
onSubmit(newTemplate);
}, 50);
};
// Get title based on current step
const getStepTitle = () => {
switch (step) {
case 'type': return 'Select Workout Type';
case 'info': return `New ${workoutType.charAt(0).toUpperCase() + workoutType.slice(1)} Workout`;
case 'exercises': return 'Select Exercises';
case 'config': return 'Configure Exercises';
case 'review': return 'Review Template';
}
};
// Show back button for all steps except the first
const showBackButton = step !== 'type';
// Render content based on current step
const renderContent = () => {
switch (step) {
case 'type':
return (
);
case 'info':
return (
setTemplateInfo(prev => ({ ...prev, title }))}
onDescriptionChange={(description) => setTemplateInfo(prev => ({ ...prev, description }))}
onCategoryChange={(category) => setTemplateInfo(prev => ({ ...prev, category: category as TemplateCategory }))}
onNext={handleSubmitInfo}
onCancel={onClose}
/>
);
case 'exercises':
return (
setStep('info')}
/>
);
case 'config':
return (
setStep('exercises')}
/>
);
case 'review':
return (
setStep('config')}
/>
);
}
};
// Return null if not open
if (!isOpen) return null;
return (
{/* Header */}
{showBackButton && (
)}
{getStepTitle()}
{/* Content */}
{renderContent()}
);
}