POWR/app/(workout)/create.tsx

476 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// app/(workout)/create.tsx
import React, { useState, useEffect } from 'react';
import { View, ScrollView, StyleSheet, TextInput } from 'react-native';
import { router, useNavigation } from 'expo-router';
import { TabScreen } from '@/components/layout/TabScreen';
import { Text } from '@/components/ui/text';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
AlertDialog,
AlertDialogAction,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogCancel
} from '@/components/ui/alert-dialog';
import { useWorkoutStore } from '@/stores/workoutStore';
import { Plus, Pause, Play, MoreHorizontal, CheckCircle2, Dumbbell, ChevronLeft } from 'lucide-react-native';
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';
import { formatTime } from '@/utils/formatTime';
import { ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
// Define styles outside of component
const styles = StyleSheet.create({
timerText: {
fontVariant: ['tabular-nums']
}
});
export default function CreateWorkoutScreen() {
const {
status,
activeWorkout,
elapsedTime,
restTimer,
clearAutoSave,
isMinimized
} = useWorkoutStore();
const {
pauseWorkout,
resumeWorkout,
completeWorkout,
updateWorkoutTitle,
updateSet,
cancelWorkout,
minimizeWorkout,
maximizeWorkout
} = useWorkoutStore.getState();
type CreateScreenNavigationProp = NativeStackNavigationProp<ParamListBase>;
const navigation = useNavigation<CreateScreenNavigationProp>();
// 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
}
});
return unsubscribe;
}, [navigation, activeWorkout, isMinimized, minimizeWorkout]);
const [showCancelDialog, setShowCancelDialog] = useState(false);
const insets = useSafeAreaInsets();
// Handler for confirming workout cancellation
const confirmCancelWorkout = async () => {
setShowCancelDialog(false);
// If cancelWorkout exists in the store, use it
if (typeof cancelWorkout === 'function') {
await cancelWorkout();
} else {
// Otherwise use the clearAutoSave function
await clearAutoSave();
}
router.back();
};
// 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);
};
// Handler for completing a set
const handleCompleteSet = (exerciseIndex: number, setIndex: number) => {
if (!activeWorkout) return;
const exercise = activeWorkout.exercises[exerciseIndex];
const set = exercise.sets[setIndex];
updateSet(exerciseIndex, setIndex, {
...set,
isCompleted: !set.isCompleted
});
};
// 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>
);
}
// 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)}
>
<Plus className="mr-2 text-foreground" size={18} />
<Text>Add 30s</Text>
</Button>
</View>
</View>
</TabScreen>
);
}
const hasExercises = activeWorkout.exercises.length > 0;
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();
}}
>
<ChevronLeft className="text-foreground" />
</Button>
<Text className="text-xl font-semibold ml-2">Back</Text>
</View>
<Button
variant="purple"
className="px-4"
onPress={() => completeWorkout()}
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',
}}
/>
</View>
{/* Timer Display */}
<View className="flex-row items-center px-4 pb-3 border-b border-border">
<Text style={styles.timerText} className={cn(
"text-2xl font-mono",
status === 'paused' ? "text-muted-foreground" : "text-foreground"
)}>
{formatTime(elapsedTime)}
</Text>
{status === 'active' ? (
<Button
variant="ghost"
size="icon"
className="ml-2"
onPress={pauseWorkout}
>
<Pause className="text-foreground" />
</Button>
) : (
<Button
variant="ghost"
size="icon"
className="ml-2"
onPress={resumeWorkout}
>
<Play className="text-foreground" />
</Button>
)}
</View>
{/* Content Area */}
<ScrollView
className="flex-1"
contentContainerStyle={{
paddingHorizontal: 16,
paddingBottom: insets.bottom + 20,
paddingTop: 16,
...(hasExercises ? {} : { flex: 1 })
}}
>
{hasExercises ? (
// Exercise List when exercises exist
<>
{activeWorkout.exercises.map((exercise, exerciseIndex) => (
<Card
key={exercise.id}
className="mb-6 overflow-hidden border border-border bg-card"
>
{/* Exercise Header */}
<View className="flex-row justify-between items-center px-4 py-3 border-b border-border">
<Text className="text-lg font-semibold text-purple">
{exercise.title}
</Text>
<Button
variant="ghost"
size="icon"
onPress={() => {
// Open exercise options menu
console.log('Open exercise options');
}}
>
<MoreHorizontal className="text-muted-foreground" size={20} />
</Button>
</View>
{/* Sets Info */}
<View className="px-4 py-2">
<Text className="text-sm text-muted-foreground">
{exercise.sets.filter(s => s.isCompleted).length} sets completed
</Text>
</View>
{/* Set Headers */}
<View className="flex-row px-4 py-2 border-t border-border bg-muted/30">
<Text className="w-16 text-sm font-medium text-muted-foreground">SET</Text>
<Text className="w-20 text-sm font-medium text-muted-foreground">PREV</Text>
<Text className="flex-1 text-sm font-medium text-center text-muted-foreground">KG</Text>
<Text className="flex-1 text-sm font-medium text-center text-muted-foreground">REPS</Text>
<View style={{ width: 44 }} />
</View>
{/* Exercise Sets */}
<CardContent className="p-0">
{exercise.sets.map((set, setIndex) => {
const previousSet = setIndex > 0 ? exercise.sets[setIndex - 1] : null;
return (
<View
key={set.id}
className={cn(
"flex-row items-center px-4 py-3 border-t border-border",
set.isCompleted && "bg-primary/5"
)}
>
{/* Set Number */}
<Text className="w-16 text-base font-medium text-foreground">
{setIndex + 1}
</Text>
{/* Previous Set */}
<Text className="w-20 text-sm text-muted-foreground">
{previousSet ? `${previousSet.weight}×${previousSet.reps}` : '—'}
</Text>
{/* Weight Input */}
<View className="flex-1 px-2">
<TextInput
className={cn(
"bg-secondary h-10 rounded-md px-3 text-center text-foreground",
set.isCompleted && "bg-primary/10"
)}
value={set.weight ? set.weight.toString() : ''}
onChangeText={(text) => {
const weight = text === '' ? 0 : parseFloat(text);
if (!isNaN(weight)) {
updateSet(exerciseIndex, setIndex, { weight });
}
}}
keyboardType="numeric"
selectTextOnFocus
/>
</View>
{/* Reps Input */}
<View className="flex-1 px-2">
<TextInput
className={cn(
"bg-secondary h-10 rounded-md px-3 text-center text-foreground",
set.isCompleted && "bg-primary/10"
)}
value={set.reps ? set.reps.toString() : ''}
onChangeText={(text) => {
const reps = text === '' ? 0 : parseInt(text, 10);
if (!isNaN(reps)) {
updateSet(exerciseIndex, setIndex, { reps });
}
}}
keyboardType="numeric"
selectTextOnFocus
/>
</View>
{/* Complete Button */}
<Button
variant="ghost"
size="icon"
className="w-11 h-11"
onPress={() => handleCompleteSet(exerciseIndex, setIndex)}
>
<CheckCircle2
className={set.isCompleted ? "text-purple" : "text-muted-foreground"}
fill={set.isCompleted ? "currentColor" : "none"}
size={22}
/>
</Button>
</View>
);
})}
</CardContent>
{/* Add Set Button */}
<Button
variant="ghost"
className="flex-row justify-center items-center py-3 border-t border-border"
onPress={() => handleAddSet(exerciseIndex)}
>
<Plus size={18} className="text-foreground mr-2" />
<Text className="text-foreground">Add Set</Text>
</Button>
</Card>
))}
{/* 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>
</>
) : (
// Empty State with nice message and icon
<View className="flex-1 justify-center items-center px-4">
<Dumbbell className="text-muted-foreground mb-6" size={80} />
<Text className="text-xl font-semibold text-center mb-2">
No exercises added
</Text>
<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>
</View>
)}
</ScrollView>
</View>
{/* Cancel Workout Dialog */}
<AlertDialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Cancel Workout</AlertDialogTitle>
<AlertDialogDescription>
<Text>Are you sure you want to cancel this workout? All progress will be lost.</Text>
</AlertDialogDescription>
</AlertDialogHeader>
<View className="flex-row justify-end gap-3">
<AlertDialogCancel onPress={() => setShowCancelDialog(false)}>
<Text>Continue Workout</Text>
</AlertDialogCancel>
<AlertDialogAction onPress={confirmCancelWorkout}>
<Text>Cancel Workout</Text>
</AlertDialogAction>
</View>
</AlertDialogContent>
</AlertDialog>
</TabScreen>
);
}