POWR/app/(workout)/create.tsx

512 lines
17 KiB
TypeScript
Raw Permalink Normal View History

2025-02-24 22:27:01 -05:00
// app/(workout)/create.tsx
import React, { useState, useEffect } from 'react';
import { View, ScrollView, StyleSheet, TouchableOpacity } from 'react-native';
import { router, useNavigation } from 'expo-router';
2025-02-24 22:27:01 -05:00
import { TabScreen } from '@/components/layout/TabScreen';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
2025-02-25 15:03:45 -05:00
AlertDialogTitle,
AlertDialogCancel
2025-02-24 22:27:01 -05:00
} from '@/components/ui/alert-dialog';
import { useWorkoutStore } from '@/stores/workoutStore';
2025-03-01 13:43:42 -05:00
import { Plus, Pause, Play, MoreHorizontal, Dumbbell, ChevronLeft } from 'lucide-react-native';
2025-02-24 22:27:01 -05:00
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import EditableText from '@/components/EditableText';
import { cn } from '@/lib/utils';
import { generateId } from '@/utils/ids';
import { WorkoutSet } from '@/types/workout';
2025-02-25 15:03:45 -05:00
import { formatTime } from '@/utils/formatTime';
import { ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
2025-03-01 13:43:42 -05:00
import SetInput from '@/components/workout/SetInput';
2025-03-12 19:23:28 -04:00
import { useColorScheme } from '@/lib/theme/useColorScheme';
2025-03-07 12:38:21 -05:00
import { WorkoutAlertDialog } from '@/components/workout/WorkoutAlertDialog';
2025-03-12 19:23:28 -04:00
import { useIconColor } from '@/lib/theme/iconUtils';
2025-02-24 22:27:01 -05:00
export default function CreateWorkoutScreen() {
const {
status,
activeWorkout,
elapsedTime,
restTimer,
2025-02-25 15:03:45 -05:00
clearAutoSave,
isMinimized
2025-02-24 22:27:01 -05:00
} = useWorkoutStore();
const {
pauseWorkout,
resumeWorkout,
completeWorkout,
updateWorkoutTitle,
2025-02-25 15:03:45 -05:00
updateSet,
cancelWorkout,
minimizeWorkout,
maximizeWorkout
2025-02-24 22:27:01 -05:00
} = useWorkoutStore.getState();
// Get theme colors
const { isDarkColorScheme } = useColorScheme();
2025-03-12 19:23:28 -04:00
// Get icon utilities
const { getIconProps } = useIconColor();
// Create dynamic styles based on theme
const dynamicStyles = StyleSheet.create({
timerText: {
fontVariant: ['tabular-nums']
},
cardContainer: {
marginBottom: 24,
backgroundColor: isDarkColorScheme ? '#1F1F23' : 'white',
borderRadius: 8,
borderWidth: 1,
borderColor: isDarkColorScheme ? '#333' : '#eee',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 2,
},
cardHeader: {
padding: 16,
flexDirection: 'row',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderBottomColor: isDarkColorScheme ? '#333' : '#eee'
},
cardTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#8B5CF6' // Purple is used in both themes
},
setsInfo: {
paddingHorizontal: 16,
paddingVertical: 4
},
setsInfoText: {
fontSize: 14,
color: isDarkColorScheme ? '#999' : '#666'
},
headerRow: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 4,
borderTopWidth: 1,
borderTopColor: isDarkColorScheme ? '#333' : '#eee',
backgroundColor: isDarkColorScheme ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)'
},
headerCell: {
fontSize: 14,
fontWeight: '500',
color: isDarkColorScheme ? '#999' : '#666',
textAlign: 'center'
},
setNumberCell: {
width: 32
},
prevCell: {
width: 80
},
valueCell: {
flex: 1
},
spacer: {
width: 44
},
setsList: {
padding: 0
},
actionButton: {
borderTopWidth: 1,
borderTopColor: isDarkColorScheme ? '#333' : '#eee'
},
iconColor: {
color: isDarkColorScheme ? '#999' : '#666'
}
});
type CreateScreenNavigationProp = NativeStackNavigationProp<ParamListBase>;
const navigation = useNavigation<CreateScreenNavigationProp>();
2025-02-25 15:03:45 -05:00
// Check if we're coming from minimized state when component mounts
useEffect(() => {
if (isMinimized) {
maximizeWorkout();
}
}, [isMinimized, maximizeWorkout]);
// Handle back navigation
useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', (e) => {
// If we have an active workout, just minimize it before continuing
if (activeWorkout && !isMinimized) {
// Call minimizeWorkout to update the state
minimizeWorkout();
// Let the navigation continue naturally
// Don't call router.back() here to avoid recursion
}
});
2025-02-25 15:03:45 -05:00
return unsubscribe;
}, [navigation, activeWorkout, isMinimized, minimizeWorkout]);
2025-02-25 15:03:45 -05:00
2025-03-07 12:38:21 -05:00
const [showFinishDialog, setShowFinishDialog] = useState(false);
2025-02-24 22:27:01 -05:00
const [showCancelDialog, setShowCancelDialog] = useState(false);
const insets = useSafeAreaInsets();
2025-02-25 15:03:45 -05:00
// Handler for confirming workout cancellation
const confirmCancelWorkout = async () => {
setShowCancelDialog(false);
2025-02-24 22:27:01 -05:00
2025-02-25 15:03:45 -05:00
// If cancelWorkout exists in the store, use it
if (typeof cancelWorkout === 'function') {
await cancelWorkout();
} else {
// Otherwise use the clearAutoSave function
await clearAutoSave();
2025-02-24 22:27:01 -05:00
}
2025-02-25 15:03:45 -05:00
router.back();
};
2025-02-24 22:27:01 -05:00
// Handler for adding a new set to an exercise
const handleAddSet = (exerciseIndex: number) => {
if (!activeWorkout) return;
const exercise = activeWorkout.exercises[exerciseIndex];
const lastSet = exercise.sets[exercise.sets.length - 1];
const newSet: WorkoutSet = {
id: generateId('local'),
weight: lastSet?.weight || 0,
reps: lastSet?.reps || 0,
type: 'normal',
isCompleted: false
};
updateSet(exerciseIndex, exercise.sets.length, newSet);
};
// Show empty state when no workout is active
if (!activeWorkout) {
return (
<TabScreen>
<View className="flex-1 items-center justify-center p-6">
<Text className="text-xl font-semibold text-foreground text-center mb-4">
No active workout
</Text>
<Button
onPress={() => router.back()}
>
<Text className="text-primary-foreground">Go Back</Text>
</Button>
</View>
</TabScreen>
);
}
2025-02-25 15:03:45 -05:00
// Show rest timer overlay when active
if (restTimer.isActive) {
return (
<TabScreen>
<View className="flex-1 items-center justify-center bg-background/80">
{/* Timer Display */}
<View className="items-center mb-8">
<Text className="text-4xl font-bold text-foreground mb-2">
Rest Timer
</Text>
<Text className="text-6xl font-bold text-primary">
{formatTime(restTimer.remaining * 1000)}
</Text>
</View>
{/* Controls */}
<View className="flex-row gap-4">
<Button
size="lg"
variant="outline"
onPress={() => useWorkoutStore.getState().stopRest()}
>
<Text>Skip</Text>
</Button>
<Button
size="lg"
variant="outline"
onPress={() => useWorkoutStore.getState().extendRest(30)}
>
<View>
2025-03-12 19:23:28 -04:00
<Plus {...getIconProps('primary')} size={18} />
</View>
2025-02-25 15:03:45 -05:00
<Text>Add 30s</Text>
</Button>
</View>
</View>
</TabScreen>
);
}
const hasExercises = activeWorkout.exercises.length > 0;
2025-02-24 22:27:01 -05:00
return (
<TabScreen>
<View style={{ flex: 1, paddingTop: insets.top }}>
{/* Header with back button */}
<View className="px-4 py-3 flex-row items-center justify-between border-b border-border">
<View className="flex-row items-center">
<Button
variant="ghost"
size="icon"
onPress={() => {
minimizeWorkout();
router.back();
}}
>
<View>
2025-03-12 19:23:28 -04:00
<ChevronLeft {...getIconProps('primary')} />
</View>
</Button>
<Text className="text-xl font-semibold ml-2">Back</Text>
</View>
<Button
variant="purple"
className="px-4"
2025-03-07 12:38:21 -05:00
onPress={() => setShowFinishDialog(true)}
disabled={!hasExercises}
>
<Text className="text-white font-medium">Finish</Text>
</Button>
</View>
{/* Full-width workout title */}
<View className="px-4 py-3">
<EditableText
value={activeWorkout.title}
onChangeText={(newTitle) => updateWorkoutTitle(newTitle)}
placeholder="Workout Title"
textStyle={{
fontSize: 24,
fontWeight: '700',
}}
/>
2025-02-24 22:27:01 -05:00
</View>
{/* Timer Display */}
<View className="flex-row items-center px-4 pb-3 border-b border-border">
<Text style={dynamicStyles.timerText} className={cn(
"text-2xl font-mono",
status === 'paused' ? "text-muted-foreground" : "text-foreground"
)}>
{formatTime(elapsedTime)}
</Text>
{status === 'active' ? (
2025-02-25 15:03:45 -05:00
<Button
variant="ghost"
size="icon"
className="ml-2"
onPress={pauseWorkout}
2025-02-25 15:03:45 -05:00
>
<View>
2025-03-12 19:23:28 -04:00
<Pause {...getIconProps('primary')} />
</View>
2025-02-25 15:03:45 -05:00
</Button>
) : (
2025-02-24 22:27:01 -05:00
<Button
variant="ghost"
size="icon"
className="ml-2"
onPress={resumeWorkout}
2025-02-24 22:27:01 -05:00
>
<View>
2025-03-12 19:23:28 -04:00
<Play {...getIconProps('primary')} />
</View>
2025-02-24 22:27:01 -05:00
</Button>
)}
2025-02-24 22:27:01 -05:00
</View>
2025-02-25 15:03:45 -05:00
{/* Content Area */}
2025-02-24 22:27:01 -05:00
<ScrollView
className="flex-1"
contentContainerStyle={{
paddingHorizontal: 16,
paddingBottom: insets.bottom + 20,
2025-02-25 15:03:45 -05:00
paddingTop: 16,
...(hasExercises ? {} : { flex: 1 })
2025-02-24 22:27:01 -05:00
}}
>
2025-02-25 15:03:45 -05:00
{hasExercises ? (
// Exercise List when exercises exist
<>
{activeWorkout.exercises.map((exercise, exerciseIndex) => (
<View key={exercise.id} style={dynamicStyles.cardContainer}>
2025-02-25 15:03:45 -05:00
{/* Exercise Header */}
<View style={dynamicStyles.cardHeader}>
<Text style={dynamicStyles.cardTitle}>
2025-02-25 15:03:45 -05:00
{exercise.title}
</Text>
<TouchableOpacity onPress={() => console.log('Open exercise options')}>
<View>
2025-03-12 19:23:28 -04:00
<MoreHorizontal {...getIconProps('muted')} size={20} />
</View>
</TouchableOpacity>
2025-02-25 15:03:45 -05:00
</View>
{/* Sets Info */}
<View style={dynamicStyles.setsInfo}>
<Text style={dynamicStyles.setsInfoText}>
2025-02-25 15:03:45 -05:00
{exercise.sets.filter(s => s.isCompleted).length} sets completed
</Text>
</View>
{/* Set Headers */}
<View style={dynamicStyles.headerRow}>
<Text style={[dynamicStyles.headerCell, dynamicStyles.setNumberCell]}>SET</Text>
<Text style={[dynamicStyles.headerCell, dynamicStyles.prevCell]}>PREV</Text>
<Text style={[dynamicStyles.headerCell, dynamicStyles.valueCell]}>KG</Text>
<Text style={[dynamicStyles.headerCell, dynamicStyles.valueCell]}>REPS</Text>
<View style={dynamicStyles.spacer} />
2025-02-25 15:03:45 -05:00
</View>
{/* Exercise Sets */}
<View style={dynamicStyles.setsList}>
2025-02-25 15:03:45 -05:00
{exercise.sets.map((set, setIndex) => {
2025-03-01 13:43:42 -05:00
const previousSet = setIndex > 0 ? exercise.sets[setIndex - 1] : undefined;
2025-02-25 15:03:45 -05:00
return (
2025-03-01 13:43:42 -05:00
<SetInput
2025-02-25 15:03:45 -05:00
key={set.id}
2025-03-01 13:43:42 -05:00
exerciseIndex={exerciseIndex}
setIndex={setIndex}
setNumber={setIndex + 1}
weight={set.weight}
reps={set.reps}
isCompleted={set.isCompleted}
previousSet={previousSet}
/>
2025-02-25 15:03:45 -05:00
);
})}
</View>
2025-02-25 15:03:45 -05:00
{/* Add Set Button */}
<View style={dynamicStyles.actionButton}>
<Button
variant="ghost"
className="flex-row justify-center items-center py-2"
onPress={() => handleAddSet(exerciseIndex)}
>
<View>
2025-03-12 19:23:28 -04:00
<Plus {...getIconProps('primary')} size={18} />
</View>
<Text className="text-foreground">Add Set</Text>
</Button>
</View>
</View>
2025-02-25 15:03:45 -05:00
))}
{/* Add Exercises Button */}
<Button
variant="purple"
className="w-full mb-4"
onPress={() => router.push('/(workout)/add-exercises')}
>
<Text className="text-white font-medium">Add Exercises</Text>
</Button>
{/* Cancel Button */}
<Button
variant="outline"
className="w-full mb-8"
onPress={() => setShowCancelDialog(true)}
>
<Text className="text-foreground">Cancel Workout</Text>
</Button>
2025-02-25 15:03:45 -05:00
</>
2025-02-24 22:27:01 -05:00
) : (
2025-02-25 15:03:45 -05:00
// Empty State with nice message and icon
<View className="flex-1 justify-center items-center px-4">
<View>
2025-03-12 19:23:28 -04:00
<Dumbbell {...getIconProps('muted')} size={80} />
</View>
2025-02-25 15:03:45 -05:00
<Text className="text-xl font-semibold text-center mb-2">
No exercises added
2025-02-24 22:27:01 -05:00
</Text>
2025-02-25 15:03:45 -05:00
<Text className="text-base text-muted-foreground text-center mb-8">
Add exercises to start tracking your workout
</Text>
{/* Add Exercises Button for empty state */}
<Button
variant="purple"
className="w-full mb-4"
onPress={() => router.push('/(workout)/add-exercises')}
>
<Text className="text-white font-medium">Add Exercises</Text>
</Button>
{/* Cancel Button for empty state */}
<Button
variant="outline"
className="w-full"
onPress={() => setShowCancelDialog(true)}
>
<Text className="text-foreground">Cancel Workout</Text>
</Button>
2025-02-24 22:27:01 -05:00
</View>
)}
</ScrollView>
</View>
2025-02-25 15:03:45 -05:00
2025-03-07 12:38:21 -05:00
{/* Finish Workout Dialog */}
<WorkoutAlertDialog
open={showFinishDialog}
onOpenChange={setShowFinishDialog}
onConfirm={() => {
setShowFinishDialog(false);
// Set the end time before navigating
useWorkoutStore.setState(state => ({
activeWorkout: state.activeWorkout ? {
...state.activeWorkout,
endTime: Date.now(),
lastUpdated: Date.now()
} : null
}));
// Navigate to completion screen
router.push('/(workout)/complete');
}}
title="Complete Workout?"
description="Are you sure you want to finish this workout? This will end your current session."
confirmText="Complete Workout"
/>
2025-02-25 15:03:45 -05:00
{/* Cancel Workout Dialog */}
<AlertDialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Text>Cancel Workout</Text>
</AlertDialogTitle>
2025-02-25 15:03:45 -05:00
<AlertDialogDescription>
<Text>Are you sure you want to cancel this workout? All progress will be lost.</Text>
</AlertDialogDescription>
</AlertDialogHeader>
2025-03-12 19:23:28 -04:00
<View className="flex-row justify-center gap-3 px-4 mt-2">
2025-02-25 15:03:45 -05:00
<AlertDialogCancel onPress={() => setShowCancelDialog(false)}>
<Text>Continue Workout</Text>
</AlertDialogCancel>
2025-03-12 19:23:28 -04:00
<AlertDialogAction onPress={confirmCancelWorkout} className='bg-destructive'>
<Text className='text-destructive-foreground'>Cancel Workout</Text>
2025-02-25 15:03:45 -05:00
</AlertDialogAction>
</View>
</AlertDialogContent>
</AlertDialog>
2025-02-24 22:27:01 -05:00
</TabScreen>
);
}