diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 7738fb2..6d4df20 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -1,78 +1,100 @@
// app/(tabs)/_layout.tsx
-import React from 'react';
-import { Platform } from 'react-native';
-import { Tabs } from 'expo-router';
+import React, { useEffect } from 'react';
+import { Platform, View } from 'react-native';
+import { Tabs, useNavigation } from 'expo-router';
import { useTheme } from '@react-navigation/native';
-import { Dumbbell, Library, Users, History, User, } from 'lucide-react-native';
+import { Dumbbell, Library, Users, History, User } from 'lucide-react-native';
import type { CustomTheme } from '@/lib/theme';
+import ActiveWorkoutBar from '@/components/workout/ActiveWorkoutBar';
+import { useWorkoutStore } from '@/stores/workoutStore';
export default function TabLayout() {
const theme = useTheme() as CustomTheme;
+ const navigation = useNavigation();
+ const { isActive, isMinimized } = useWorkoutStore();
+ const { minimizeWorkout } = useWorkoutStore.getState();
+
+ // Auto-minimize workout when navigating between tabs
+ useEffect(() => {
+ const unsubscribe = navigation.addListener('state', (e) => {
+ // If workout is active but not minimized, minimize it when changing tabs
+ if (isActive && !isMinimized) {
+ minimizeWorkout();
+ }
+ });
+
+ return unsubscribe;
+ }, [navigation, isActive, isMinimized, minimizeWorkout]);
return (
-
- (
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
-
+
+
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+
+
+ {/* Render the ActiveWorkoutBar above the tab bar */}
+
+
);
}
\ No newline at end of file
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index f3eee4f..59a2d4d 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -37,12 +37,18 @@ interface FavoriteTemplateData {
source: 'local' | 'powr' | 'nostr';
}
+// Type for tracking pending workout actions
+type PendingWorkoutAction =
+ | { type: 'quick-start' }
+ | { type: 'template', templateId: string }
+ | { type: 'template-select' };
+
export default function WorkoutScreen() {
const { startWorkout } = useWorkoutStore.getState();
- const [showActiveWorkoutModal, setShowActiveWorkoutModal] = useState(false)
- const [pendingTemplateId, setPendingTemplateId] = useState(null)
- const [favoriteWorkouts, setFavoriteWorkouts] = useState([])
- const [isLoadingFavorites, setIsLoadingFavorites] = useState(true)
+ const [showActiveWorkoutModal, setShowActiveWorkoutModal] = useState(false);
+ const [pendingAction, setPendingAction] = useState(null);
+ const [favoriteWorkouts, setFavoriteWorkouts] = useState([]);
+ const [isLoadingFavorites, setIsLoadingFavorites] = useState(true);
const {
getFavorites,
@@ -51,16 +57,16 @@ export default function WorkoutScreen() {
checkFavoriteStatus,
isActive,
endWorkout
- } = useWorkoutStore()
+ } = useWorkoutStore();
useEffect(() => {
- loadFavorites()
- }, [])
+ loadFavorites();
+ }, []);
const loadFavorites = async () => {
- setIsLoadingFavorites(true)
+ setIsLoadingFavorites(true);
try {
- const favorites = await getFavorites()
+ const favorites = await getFavorites();
const workoutTemplates = favorites
.filter(f => f.content && f.content.id && checkFavoriteStatus(f.content.id))
@@ -87,57 +93,51 @@ export default function WorkoutScreen() {
} as FavoriteTemplateData;
});
- setFavoriteWorkouts(workoutTemplates)
+ setFavoriteWorkouts(workoutTemplates);
} catch (error) {
- console.error('Error loading favorites:', error)
+ console.error('Error loading favorites:', error);
} finally {
- setIsLoadingFavorites(false)
+ setIsLoadingFavorites(false);
}
- }
+ };
+ // Handle starting a template-based workout
const handleStartWorkout = async (templateId: string) => {
if (isActive) {
- setPendingTemplateId(templateId)
- setShowActiveWorkoutModal(true)
- return
+ // Save what the user wants to do for later
+ setPendingAction({ type: 'template', templateId });
+ setShowActiveWorkoutModal(true);
+ return;
}
try {
- await startWorkoutFromTemplate(templateId)
- router.push('/(workout)/create')
+ await startWorkoutFromTemplate(templateId);
+ router.push('/(workout)/create');
} catch (error) {
- console.error('Error starting workout:', error)
+ console.error('Error starting workout:', error);
}
- }
+ };
- const handleStartNew = async () => {
- if (!pendingTemplateId) return
+ // Handle selecting a template
+ const handleSelectTemplate = () => {
+ if (isActive) {
+ setPendingAction({ type: 'template-select' });
+ setShowActiveWorkoutModal(true);
+ return;
+ }
- const templateToStart = pendingTemplateId
- setShowActiveWorkoutModal(false)
- setPendingTemplateId(null)
-
- await endWorkout()
- await startWorkoutFromTemplate(templateToStart)
- router.push('/(workout)/create')
- }
-
- const handleContinueExisting = () => {
- setShowActiveWorkoutModal(false)
- setPendingTemplateId(null)
- router.push('/(workout)/create')
- }
-
- const handleFavoritePress = async (templateId: string) => {
- try {
- await removeFavorite(templateId)
- await loadFavorites()
- } catch (error) {
- console.error('Error toggling favorite:', error)
- }
- }
+ router.push('/(workout)/template-select');
+ };
+ // Handle quick start
const handleQuickStart = () => {
+ // Check if there's already an active workout
+ if (isActive) {
+ setPendingAction({ type: 'quick-start' });
+ setShowActiveWorkoutModal(true);
+ return;
+ }
+
// Initialize a new workout with a random funny title
startWorkout({
title: getRandomWorkoutTitle(),
@@ -148,18 +148,71 @@ export default function WorkoutScreen() {
router.push('/(workout)/create');
};
+ // Handle starting a new workout (after ending the current one)
+ const handleStartNew = async () => {
+ if (!pendingAction) return;
+
+ setShowActiveWorkoutModal(false);
+
+ // End the current workout first
+ await endWorkout();
+
+ // Now handle the pending action
+ switch (pendingAction.type) {
+ case 'quick-start':
+ // Start a new quick workout
+ startWorkout({
+ title: getRandomWorkoutTitle(),
+ type: 'strength',
+ exercises: []
+ });
+ router.push('/(workout)/create');
+ break;
+
+ case 'template':
+ // Start a workout from the selected template
+ await startWorkoutFromTemplate(pendingAction.templateId);
+ router.push('/(workout)/create');
+ break;
+
+ case 'template-select':
+ // Navigate to template selection
+ router.push('/(workout)/template-select');
+ break;
+ }
+
+ // Clear the pending action
+ setPendingAction(null);
+ };
+
+ // Handle continuing the existing workout
+ const handleContinueExisting = () => {
+ setShowActiveWorkoutModal(false);
+ setPendingAction(null);
+ router.push('/(workout)/create');
+ };
+
+ const handleFavoritePress = async (templateId: string) => {
+ try {
+ await removeFavorite(templateId);
+ await loadFavorites();
+ } catch (error) {
+ console.error('Error toggling favorite:', error);
+ }
+ };
+
return (
router.push('/(workout)/template-select')}
+ onStartBlank={handleQuickStart}
+ onSelectTemplate={handleSelectTemplate}
/>
{/* Favorites section */}
@@ -205,11 +258,11 @@ export default function WorkoutScreen() {
Active Workout
- You have an active workout in progress. Would you like to finish it first?
+ You have an active workout in progress. Would you like to continue it or start a new workout?
- setShowActiveWorkoutModal(false)}>
+
Start New
@@ -219,5 +272,5 @@ export default function WorkoutScreen() {
- )
+ );
}
\ No newline at end of file
diff --git a/app/(workout)/create.tsx b/app/(workout)/create.tsx
index e69ba40..4ecb43c 100644
--- a/app/(workout)/create.tsx
+++ b/app/(workout)/create.tsx
@@ -1,6 +1,6 @@
// app/(workout)/create.tsx
import React, { useState, useEffect } from 'react';
-import { View, ScrollView, StyleSheet } from 'react-native';
+import { View, ScrollView, StyleSheet, TextInput } from 'react-native';
import { router } from 'expo-router';
import { TabScreen } from '@/components/layout/TabScreen';
import { Text } from '@/components/ui/text';
@@ -12,16 +12,18 @@ import {
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
- AlertDialogTitle
+ AlertDialogTitle,
+ AlertDialogCancel
} from '@/components/ui/alert-dialog';
import { useWorkoutStore } from '@/stores/workoutStore';
-import { Plus, Pause, Play, MoreHorizontal, CheckCircle2 } from 'lucide-react-native';
+import { ArrowLeft, Plus, Pause, Play, MoreHorizontal, CheckCircle2, Dumbbell } 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 { FloatingActionButton } from '@/components/shared/FloatingActionButton';
+import { formatTime } from '@/utils/formatTime';
export default function CreateWorkoutScreen() {
const {
@@ -29,7 +31,8 @@ export default function CreateWorkoutScreen() {
activeWorkout,
elapsedTime,
restTimer,
- clearAutoSave
+ clearAutoSave,
+ isMinimized
} = useWorkoutStore();
const {
@@ -37,36 +40,38 @@ export default function CreateWorkoutScreen() {
resumeWorkout,
completeWorkout,
updateWorkoutTitle,
- updateSet
+ updateSet,
+ cancelWorkout,
+ minimizeWorkout,
+ maximizeWorkout
} = useWorkoutStore.getState();
+ // Check if we're coming from minimized state when component mounts
+ useEffect(() => {
+ if (isMinimized) {
+ maximizeWorkout();
+ }
+
+ // No need to set up a timer here as it's now managed by the store
+ }, [isMinimized]);
+
const [showCancelDialog, setShowCancelDialog] = useState(false);
const insets = useSafeAreaInsets();
- // Format time as mm:ss in monospace font
- const formatTime = (ms: number) => {
- const totalSeconds = Math.floor(ms / 1000);
- const minutes = Math.floor(totalSeconds / 60);
- const seconds = totalSeconds % 60;
- return `${minutes}:${seconds.toString().padStart(2, '0')}`;
- };
-
- // Timer update effect
- useEffect(() => {
- let timerInterval: NodeJS.Timeout | null = null;
+ // Handler for confirming workout cancellation
+ const confirmCancelWorkout = async () => {
+ setShowCancelDialog(false);
- if (status === 'active') {
- timerInterval = setInterval(() => {
- useWorkoutStore.getState().tick(1000);
- }, 1000);
+ // If cancelWorkout exists in the store, use it
+ if (typeof cancelWorkout === 'function') {
+ await cancelWorkout();
+ } else {
+ // Otherwise use the clearAutoSave function
+ await clearAutoSave();
}
- return () => {
- if (timerInterval) {
- clearInterval(timerInterval);
- }
- };
- }, [status]);
+ router.back();
+ };
// Handler for adding a new set to an exercise
const handleAddSet = (exerciseIndex: number) => {
@@ -99,6 +104,12 @@ export default function CreateWorkoutScreen() {
});
};
+ // Handler for minimizing workout and going back
+ const handleMinimize = () => {
+ minimizeWorkout();
+ router.back();
+ };
+
// Show empty state when no workout is active
if (!activeWorkout) {
return (
@@ -117,22 +128,73 @@ export default function CreateWorkoutScreen() {
);
}
+ // Show rest timer overlay when active
+ if (restTimer.isActive) {
+ return (
+
+
+ {/* Timer Display */}
+
+
+ Rest Timer
+
+
+ {formatTime(restTimer.remaining * 1000)}
+
+
+
+ {/* Controls */}
+
+
+
+
+
+
+
+ );
+ }
+
+ const hasExercises = activeWorkout.exercises.length > 0;
+
return (
- {/* Swipe indicator */}
+ {/* Swipe indicator and back button */}
{/* Header with Title and Finish Button */}
- {/* Finish button in top right */}
-
+ {/* Top row with minimize and finish buttons */}
+
+
+
@@ -182,177 +244,224 @@ export default function CreateWorkoutScreen() {
- {/* Scrollable Exercises List */}
+ {/* Content Area */}
- {activeWorkout.exercises.length > 0 ? (
- activeWorkout.exercises.map((exercise, exerciseIndex) => (
-
- {/* Exercise Header */}
-
-
- {exercise.title}
-
-
-
-
- {/* Sets Info */}
-
-
- {exercise.sets.filter(s => s.isCompleted).length} sets completed
-
-
-
- {/* Set Headers */}
-
- SET
- PREV
- KG
- REPS
-
-
-
- {/* Exercise Sets */}
-
- {exercise.sets.map((set, setIndex) => {
- const previousSet = setIndex > 0 ? exercise.sets[setIndex - 1] : null;
- return (
-
- {/* Set Number */}
-
- {setIndex + 1}
-
-
- {/* Previous Set */}
-
- {previousSet ? `${previousSet.weight}×${previousSet.reps}` : '—'}
-
-
+ {hasExercises ? (
+ // Exercise List when exercises exist
+ <>
+ {activeWorkout.exercises.map((exercise, exerciseIndex) => (
+
+ {/* Exercise Header */}
+
+
+ {exercise.title}
+
+
+
+
+ {/* Sets Info */}
+
+
+ {exercise.sets.filter(s => s.isCompleted).length} sets completed
+
+
+
+ {/* Set Headers */}
+
+ SET
+ PREV
+ KG
+ REPS
+
+
+
+ {/* Exercise Sets */}
+
+ {exercise.sets.map((set, setIndex) => {
+ const previousSet = setIndex > 0 ? exercise.sets[setIndex - 1] : null;
+ return (
+
+ {/* Set Number */}
+
+ {setIndex + 1}
+
+
+ {/* Previous Set */}
+
+ {previousSet ? `${previousSet.weight}×${previousSet.reps}` : '—'}
+
+
{/* Weight Input */}
-
-
- {set.weight}
-
-
+ )}
+ value={set.weight ? set.weight.toString() : ''}
+ onChangeText={(text) => {
+ const weight = text === '' ? 0 : parseFloat(text);
+ if (!isNaN(weight)) {
+ updateSet(exerciseIndex, setIndex, { weight });
+ }
+ }}
+ keyboardType="numeric"
+ selectTextOnFocus
+ />
-
+
{/* Reps Input */}
-
-
- {set.reps}
-
-
+ )}
+ 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
+ />
-
- {/* Complete Button */}
-
-
- );
- })}
-
-
- {/* Add Set Button */}
+
+ {/* Complete Button */}
+
+
+ );
+ })}
+
+
+ {/* Add Set Button */}
+
+
+ ))}
+
+ {/* Cancel Button - only shown at the bottom when exercises exist */}
+
-
- ))
+
+ >
) : (
-
-
- No exercises added. Add exercises to start your workout.
+ // Empty State with nice message and icon
+
+
+
+ No exercises added
+
+ Add exercises to start tracking your workout
+
+
+ {/* Add Exercises Button for empty state */}
+
+
+ {/* Cancel Button for empty state */}
+
)}
- {/* Add Exercise FAB */}
-
- router.push('/(workout)/add-exercises')}
- />
-
-
- {/* Cancel Workout Dialog */}
-
-
-
- Cancel Workout
-
- Are you sure you want to cancel this workout? All progress will be lost.
-
-
-
- setShowCancelDialog(false)}
- >
- Continue Workout
-
- {
- await clearAutoSave();
- router.back();
- }}
- >
- Cancel Workout
-
-
-
-
+ {/* Add Exercise FAB - only shown when exercises exist */}
+ {hasExercises && (
+
+ router.push('/(workout)/add-exercises')}
+ />
+
+ )}
+
+ {/* Cancel Workout Dialog */}
+
+
+
+ Cancel Workout
+
+ Are you sure you want to cancel this workout? All progress will be lost.
+
+
+
+ setShowCancelDialog(false)}>
+ Continue Workout
+
+
+ Cancel Workout
+
+
+
+
);
}
diff --git a/components/workout/ActiveWorkoutBar.tsx b/components/workout/ActiveWorkoutBar.tsx
new file mode 100644
index 0000000..96e045f
--- /dev/null
+++ b/components/workout/ActiveWorkoutBar.tsx
@@ -0,0 +1,175 @@
+// components/workout/ActiveWorkoutBar.tsx
+import React, { useEffect } from 'react';
+import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native';
+import Animated, {
+ useAnimatedStyle,
+ useSharedValue,
+ withRepeat,
+ withSequence,
+ withTiming,
+ Easing
+} from 'react-native-reanimated';
+import { useWorkoutStore } from '@/stores/workoutStore';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { Activity } from 'lucide-react-native';
+import { router } from 'expo-router';
+import { formatTime } from '@/utils/formatTime';
+import { useTheme } from '@react-navigation/native';
+import type { CustomTheme } from '@/lib/theme';
+
+export default function ActiveWorkoutBar() {
+ // Use Zustand store
+ const {
+ activeWorkout,
+ isActive,
+ isMinimized,
+ status,
+ elapsedTime
+ } = useWorkoutStore();
+
+ const { maximizeWorkout } = useWorkoutStore.getState();
+ const insets = useSafeAreaInsets();
+ const theme = useTheme() as CustomTheme;
+
+ // Animation values
+ const glowOpacity = useSharedValue(0.5);
+ const scale = useSharedValue(1);
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }],
+ }));
+
+ const glowStyle = useAnimatedStyle(() => ({
+ opacity: glowOpacity.value,
+ }));
+
+ useEffect(() => {
+ // Only run animations if the bar should be visible
+ if (isActive && isMinimized && activeWorkout && activeWorkout.exercises.length > 0) {
+ // Pulse animation
+ scale.value = withRepeat(
+ withSequence(
+ withTiming(1.02, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
+ withTiming(1, { duration: 1000, easing: Easing.inOut(Easing.ease) })
+ ),
+ -1,
+ true
+ );
+
+ // Glow animation
+ glowOpacity.value = withRepeat(
+ withSequence(
+ withTiming(0.8, { duration: 1000 }),
+ withTiming(0.5, { duration: 1000 })
+ ),
+ -1,
+ true
+ );
+ }
+ }, [isActive, isMinimized, activeWorkout]);
+
+ const handlePress = () => {
+ maximizeWorkout();
+ router.push('/(workout)/create');
+ };
+
+ // Don't render anything if there's no active workout or if it's not minimized
+ if (!isActive || !isMinimized || !activeWorkout) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+
+ {activeWorkout.title}
+
+
+ {formatTime(elapsedTime)}
+
+ {activeWorkout.exercises.length} exercise{activeWorkout.exercises.length !== 1 ? 's' : ''}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ height: 40,
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ zIndex: 100,
+ elevation: Platform.OS === 'android' ? 1 : 0,
+ },
+ touchable: {
+ flex: 1,
+ borderRadius: 8,
+ marginHorizontal: 10,
+ overflow: 'hidden',
+ },
+ glow: {
+ ...StyleSheet.absoluteFillObject,
+ opacity: 0.5,
+ },
+ content: {
+ paddingHorizontal: 16,
+ height: '100%',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ leftContent: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ flex: 1,
+ },
+ rightContent: {
+ alignItems: 'flex-end',
+ },
+ title: {
+ color: 'white',
+ fontSize: 14,
+ fontWeight: '600',
+ flex: 1,
+ marginLeft: 8,
+ },
+ time: {
+ color: 'white',
+ fontSize: 14,
+ fontWeight: '600',
+ fontFamily: Platform.select({
+ ios: 'Courier',
+ android: 'monospace'
+ }),
+ marginBottom: 2,
+ },
+ exerciseCount: {
+ color: 'rgba(255, 255, 255, 0.8)',
+ fontSize: 12,
+ },
+});
\ No newline at end of file
diff --git a/components/workout/HomeWorkout.tsx b/components/workout/HomeWorkout.tsx
index 3f48687..02b3526 100644
--- a/components/workout/HomeWorkout.tsx
+++ b/components/workout/HomeWorkout.tsx
@@ -19,7 +19,7 @@ export default function HomeWorkout({ onStartBlank, onSelectTemplate }: HomeWork