mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00
bug fix workout completion flow
This commit is contained in:
parent
25d40b3a12
commit
3feb366a78
@ -1,5 +1,5 @@
|
||||
// app/(workout)/complete.tsx - revised version
|
||||
import React from 'react';
|
||||
// app/(workout)/complete.tsx
|
||||
import React, { useEffect } from 'react';
|
||||
import { View, Modal, TouchableOpacity } from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import { Text } from '@/components/ui/text';
|
||||
@ -9,132 +9,47 @@ import { WorkoutCompletionFlow } from '@/components/workout/WorkoutCompletionFlo
|
||||
import { WorkoutCompletionOptions } from '@/types/workout';
|
||||
import { useColorScheme } from '@/lib/useColorScheme';
|
||||
|
||||
/**
|
||||
* Workout Completion Screen
|
||||
*
|
||||
* This modal screen is presented when a user chooses to complete their workout.
|
||||
* It serves as a container for the multi-step completion flow, handling:
|
||||
* - Modal presentation and dismissal
|
||||
* - Delegating completion logic to the WorkoutCompletionFlow component
|
||||
*
|
||||
* The screen acts primarily as a UI wrapper, with the core completion logic
|
||||
* handled by the WorkoutStore and the step-by-step flow managed by the
|
||||
* WorkoutCompletionFlow component.
|
||||
*
|
||||
* Note: Workout timing is already stopped at this point, as the end time
|
||||
* was set when the user confirmed finishing in the create screen.
|
||||
*/
|
||||
export default function CompleteWorkoutScreen() {
|
||||
const { resumeWorkout } = useWorkoutStore.getState();
|
||||
const { resumeWorkout, activeWorkout } = useWorkoutStore();
|
||||
const { isDarkColorScheme } = useColorScheme();
|
||||
|
||||
// Handle complete with options
|
||||
const handleComplete = async (options: WorkoutCompletionOptions) => {
|
||||
// Get a fresh reference to completeWorkout and other functions
|
||||
const { completeWorkout, activeWorkout } = useWorkoutStore.getState();
|
||||
// Check if we have a workout to complete
|
||||
if (!activeWorkout) {
|
||||
// If there's no active workout, redirect back to the home screen
|
||||
router.replace('/(tabs)');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle complete with options
|
||||
const handleComplete = async (options: WorkoutCompletionOptions) => {
|
||||
// Get a fresh reference to completeWorkout
|
||||
const { completeWorkout } = useWorkoutStore.getState();
|
||||
|
||||
// 1. Complete the workout locally first
|
||||
await completeWorkout();
|
||||
|
||||
// 2. If publishing to Nostr is selected, create and publish workout event
|
||||
let workoutEventId = null;
|
||||
if (options.storageType !== 'local_only' && activeWorkout) {
|
||||
try {
|
||||
const ndkStore = require('@/lib/stores/ndk').useNDKStore.getState();
|
||||
|
||||
// Create workout tags based on NIP-4e
|
||||
const workoutTags = [
|
||||
['d', activeWorkout.id], // Unique identifier
|
||||
['title', activeWorkout.title],
|
||||
['type', activeWorkout.type],
|
||||
['start', Math.floor(activeWorkout.startTime / 1000).toString()],
|
||||
['end', Math.floor(Date.now() / 1000).toString()],
|
||||
['completed', 'true']
|
||||
];
|
||||
|
||||
// Add exercise tags
|
||||
activeWorkout.exercises.forEach(exercise => {
|
||||
// Add exercise tags following NIP-4e format
|
||||
exercise.sets.forEach(set => {
|
||||
if (set.isCompleted) {
|
||||
workoutTags.push([
|
||||
'exercise',
|
||||
`33401:${exercise.id}`,
|
||||
'', // relay URL can be empty for now
|
||||
set.weight?.toString() || '',
|
||||
set.reps?.toString() || '',
|
||||
set.rpe?.toString() || '',
|
||||
set.type || 'normal'
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add template reference if workout was based on a template
|
||||
if (activeWorkout.templateId) {
|
||||
workoutTags.push(['template', `33402:${activeWorkout.templateId}`, '']);
|
||||
}
|
||||
|
||||
// Add hashtags
|
||||
workoutTags.push(['t', 'workout'], ['t', 'fitness']);
|
||||
|
||||
// Attempt to publish the workout event
|
||||
console.log("Publishing workout event with tags:", workoutTags);
|
||||
const workoutEvent = await ndkStore.publishEvent(
|
||||
1301, // Use kind 1301 for workout records
|
||||
activeWorkout.notes || "Completed workout", // Content is workout notes
|
||||
workoutTags
|
||||
);
|
||||
|
||||
if (workoutEvent) {
|
||||
workoutEventId = workoutEvent.id;
|
||||
console.log("Successfully published workout event:", workoutEventId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error publishing workout event:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If social sharing is enabled, create a reference to the workout event
|
||||
if (options.shareOnSocial && options.socialMessage && workoutEventId) {
|
||||
try {
|
||||
const ndkStore = require('@/lib/stores/ndk').useNDKStore.getState();
|
||||
|
||||
// Create social post tags
|
||||
const socialTags = [
|
||||
['t', 'workout'],
|
||||
['t', 'fitness'],
|
||||
['t', 'powr'],
|
||||
['client', 'POWR']
|
||||
];
|
||||
|
||||
// Get current user pubkey
|
||||
const currentUserPubkey = ndkStore.currentUser?.pubkey;
|
||||
|
||||
// Add quote reference to the workout event using 'q' tag
|
||||
if (workoutEventId) {
|
||||
// Format: ["q", "<event-id>", "<relay-url>", "<author-pubkey>"]
|
||||
socialTags.push(['q', workoutEventId, '', currentUserPubkey || '']);
|
||||
}
|
||||
|
||||
// Publish social post
|
||||
await ndkStore.publishEvent(1, options.socialMessage, socialTags);
|
||||
console.log("Successfully published social post quoting workout");
|
||||
} catch (error) {
|
||||
console.error("Error publishing social post:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Handle template updates if needed
|
||||
if (activeWorkout?.templateId && options.templateAction !== 'keep_original') {
|
||||
try {
|
||||
const TemplateService = require('@/lib/db/services/TemplateService').TemplateService;
|
||||
|
||||
if (options.templateAction === 'update_existing') {
|
||||
await TemplateService.updateExistingTemplate(activeWorkout);
|
||||
} else if (options.templateAction === 'save_as_new' && options.newTemplateName) {
|
||||
await TemplateService.saveAsNewTemplate(activeWorkout, options.newTemplateName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling template action:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to home or history page
|
||||
router.replace('/(tabs)/history');
|
||||
// Complete the workout with the provided options
|
||||
await completeWorkout(options);
|
||||
};
|
||||
|
||||
// Handle cancellation
|
||||
// Handle cancellation (go back to workout)
|
||||
const handleCancel = () => {
|
||||
resumeWorkout();
|
||||
// Go back to the workout screen
|
||||
router.back();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
|
@ -26,6 +26,7 @@ import { ParamListBase } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import SetInput from '@/components/workout/SetInput';
|
||||
import { useColorScheme } from '@/lib/useColorScheme';
|
||||
import { WorkoutAlertDialog } from '@/components/workout/WorkoutAlertDialog';
|
||||
|
||||
export default function CreateWorkoutScreen() {
|
||||
const {
|
||||
@ -152,6 +153,7 @@ export default function CreateWorkoutScreen() {
|
||||
return unsubscribe;
|
||||
}, [navigation, activeWorkout, isMinimized, minimizeWorkout]);
|
||||
|
||||
const [showFinishDialog, setShowFinishDialog] = useState(false);
|
||||
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
@ -273,7 +275,7 @@ export default function CreateWorkoutScreen() {
|
||||
<Button
|
||||
variant="purple"
|
||||
className="px-4"
|
||||
onPress={() => router.push('/(workout)/complete')}
|
||||
onPress={() => setShowFinishDialog(true)}
|
||||
disabled={!hasExercises}
|
||||
>
|
||||
<Text className="text-white font-medium">Finish</Text>
|
||||
@ -459,6 +461,28 @@ export default function CreateWorkoutScreen() {
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* 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"
|
||||
/>
|
||||
|
||||
{/* Cancel Workout Dialog */}
|
||||
<AlertDialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
|
||||
<AlertDialogContent>
|
||||
|
67
components/workout/WorkoutAlertDialog.tsx
Normal file
67
components/workout/WorkoutAlertDialog.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
// components/workout/WorkoutAlertDialog.tsx
|
||||
import React from 'react';
|
||||
import { Text } from '@/components/ui/text';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
interface WorkoutAlertDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable alert dialog component for workout-related confirmations
|
||||
* Includes styling specific to workout flows including emoji and purple accent
|
||||
*/
|
||||
export function WorkoutAlertDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
title = "Complete Workout?",
|
||||
description = "Are you sure you want to finish this workout?",
|
||||
confirmText = "Complete",
|
||||
cancelText = "Cancel"
|
||||
}: WorkoutAlertDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
<Text className="text-xl">
|
||||
💪 {title}
|
||||
</Text>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Text>
|
||||
{description}
|
||||
</Text>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onPress={() => onOpenChange(false)}>
|
||||
<Text>{cancelText}</Text>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onPress={onConfirm}
|
||||
className="bg-purple-500"
|
||||
>
|
||||
<Text className="text-white">{confirmText}</Text>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// components/workout/WorkoutCompletionFlow.tsx - streamlined with celebration
|
||||
// components/workout/WorkoutCompletionFlow.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import { Text } from '@/components/ui/text';
|
||||
@ -17,12 +17,30 @@ import {
|
||||
Clock,
|
||||
Dumbbell,
|
||||
Trophy,
|
||||
Share,
|
||||
Cog
|
||||
} from 'lucide-react-native';
|
||||
import { TemplateService } from '@/lib/db/services/TemplateService';
|
||||
import Confetti from '@/components/Confetti';
|
||||
|
||||
/**
|
||||
* Workout Completion Flow Component
|
||||
*
|
||||
* This component manages the multi-step process of completing a workout:
|
||||
* 1. Storage Options - How the workout data should be stored (local/published)
|
||||
* 2. Summary - Displaying workout statistics before finalizing
|
||||
* 3. Celebration - Confirming completion and providing sharing options
|
||||
*
|
||||
* Key features:
|
||||
* - Step-based navigation with back/next functionality
|
||||
* - Nostr sharing options for completed workouts
|
||||
* - Template management options if workout was based on a template
|
||||
*
|
||||
* This component handles UI state and user input, but delegates the actual
|
||||
* workout completion to the parent component via the onComplete callback.
|
||||
* The Nostr publishing happens in the WorkoutStore after receiving the
|
||||
* selected options from this flow.
|
||||
*/
|
||||
|
||||
// Storage options component
|
||||
function StorageOptionsTab({
|
||||
options,
|
||||
@ -57,10 +75,6 @@ function StorageOptionsTab({
|
||||
// Check if the workout is based on a template
|
||||
const { activeWorkout } = useWorkoutStore();
|
||||
|
||||
// Add debug logging
|
||||
console.log('Active workout:', activeWorkout?.id);
|
||||
console.log('Template ID:', activeWorkout?.templateId);
|
||||
|
||||
// Use a try-catch block for more resilience
|
||||
let hasTemplateChanges = false;
|
||||
try {
|
||||
@ -71,9 +85,6 @@ function StorageOptionsTab({
|
||||
console.error('Error checking template changes:', error);
|
||||
}
|
||||
|
||||
console.log('Has template changes:', hasTemplateChanges);
|
||||
console.log('Template action:', options.templateAction);
|
||||
|
||||
return (
|
||||
<ScrollView className="flex-1 px-2">
|
||||
<View className="py-4 gap-6">
|
||||
@ -298,15 +309,10 @@ function SummaryTab({
|
||||
: `${minutes}m ${seconds % 60}s`;
|
||||
};
|
||||
|
||||
// Get workout duration using timestamps
|
||||
const getDuration = (): number => {
|
||||
if (!activeWorkout) return 0;
|
||||
|
||||
if (activeWorkout.endTime && activeWorkout.startTime) {
|
||||
return activeWorkout.endTime - activeWorkout.startTime;
|
||||
}
|
||||
|
||||
// If no end time, use current time
|
||||
return Date.now() - activeWorkout.startTime;
|
||||
if (!activeWorkout || !activeWorkout.endTime) return 0;
|
||||
return activeWorkout.endTime - activeWorkout.startTime;
|
||||
};
|
||||
|
||||
const getTotalSets = (): number => {
|
||||
@ -494,141 +500,141 @@ function SummaryTab({
|
||||
|
||||
// Celebration component with share option
|
||||
function CelebrationTab({
|
||||
options,
|
||||
onComplete
|
||||
}: {
|
||||
options: WorkoutCompletionOptions,
|
||||
onComplete: (options: WorkoutCompletionOptions) => void
|
||||
}) {
|
||||
const { isAuthenticated } = useNDKCurrentUser();
|
||||
const { activeWorkout } = useWorkoutStore(); // Move hook usage to component level
|
||||
const [showConfetti, setShowConfetti] = useState(true);
|
||||
const [shareMessage, setShareMessage] = useState('');
|
||||
options,
|
||||
onComplete
|
||||
}: {
|
||||
options: WorkoutCompletionOptions,
|
||||
onComplete: (options: WorkoutCompletionOptions) => void
|
||||
}) {
|
||||
const { isAuthenticated } = useNDKCurrentUser();
|
||||
const { activeWorkout } = useWorkoutStore();
|
||||
const [showConfetti, setShowConfetti] = useState(true);
|
||||
const [shareMessage, setShareMessage] = useState('');
|
||||
|
||||
// Purple color used throughout the app
|
||||
const purpleColor = 'hsl(261, 90%, 66%)';
|
||||
|
||||
// Generate default share message
|
||||
useEffect(() => {
|
||||
// Create default message based on workout data
|
||||
let message = "Just completed a workout! 💪";
|
||||
|
||||
// Purple color used throughout the app
|
||||
const purpleColor = 'hsl(261, 90%, 66%)';
|
||||
|
||||
// Generate default share message
|
||||
useEffect(() => {
|
||||
// Create default message based on workout data
|
||||
let message = "Just completed a workout! 💪";
|
||||
if (activeWorkout) {
|
||||
const exerciseCount = activeWorkout.exercises.length;
|
||||
const completedSets = activeWorkout.exercises.reduce(
|
||||
(total, exercise) => total + exercise.sets.filter(set => set.isCompleted).length, 0
|
||||
);
|
||||
|
||||
if (activeWorkout) {
|
||||
const exerciseCount = activeWorkout.exercises.length;
|
||||
const completedSets = activeWorkout.exercises.reduce(
|
||||
(total, exercise) => total + exercise.sets.filter(set => set.isCompleted).length, 0
|
||||
);
|
||||
|
||||
// Add workout details
|
||||
message = `Just completed a workout with ${exerciseCount} exercises and ${completedSets} sets! 💪`;
|
||||
|
||||
// Add mock PR info
|
||||
if (Math.random() > 0.5) {
|
||||
message += " Hit some new PRs today! 🏆";
|
||||
}
|
||||
// Add workout details
|
||||
message = `Just completed a workout with ${exerciseCount} exercises and ${completedSets} sets! 💪`;
|
||||
|
||||
// Add mock PR info
|
||||
if (Math.random() > 0.5) {
|
||||
message += " Hit some new PRs today! 🏆";
|
||||
}
|
||||
|
||||
setShareMessage(message);
|
||||
}, [activeWorkout]);
|
||||
}
|
||||
|
||||
const handleShare = () => {
|
||||
// This will trigger a kind 1 note creation via the onComplete handler
|
||||
onComplete({
|
||||
...options,
|
||||
shareOnSocial: true,
|
||||
socialMessage: shareMessage
|
||||
});
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
// Complete the workout without sharing
|
||||
onComplete({
|
||||
...options,
|
||||
shareOnSocial: false
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
|
||||
<View className="flex-1 items-center justify-center px-4 py-8">
|
||||
{showConfetti && (
|
||||
<Confetti onComplete={() => setShowConfetti(false)} />
|
||||
setShareMessage(message);
|
||||
}, [activeWorkout]);
|
||||
|
||||
const handleShare = () => {
|
||||
// This will trigger a kind 1 note creation via the onComplete handler
|
||||
onComplete({
|
||||
...options,
|
||||
shareOnSocial: true,
|
||||
socialMessage: shareMessage
|
||||
});
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
// Complete the workout without sharing
|
||||
onComplete({
|
||||
...options,
|
||||
shareOnSocial: false
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={{ flexGrow: 1 }}>
|
||||
<View className="flex-1 items-center justify-center px-4 py-8">
|
||||
{showConfetti && (
|
||||
<Confetti onComplete={() => setShowConfetti(false)} />
|
||||
)}
|
||||
|
||||
<View className="w-full max-w-md">
|
||||
{/* Trophy and heading */}
|
||||
<View className="items-center mb-8">
|
||||
<Trophy size={60} color="#F59E0B" />
|
||||
<Text className="text-2xl font-bold text-center mt-4 mb-2">
|
||||
Workout Complete!
|
||||
</Text>
|
||||
<Text className="text-center text-muted-foreground">
|
||||
Great job on finishing your workout!
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Show sharing options for Nostr if appropriate */}
|
||||
{isAuthenticated && options.storageType !== 'local_only' && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
|
||||
<View className="mb-4">
|
||||
<Text className="text-lg font-semibold mb-3">
|
||||
Share Your Achievement
|
||||
</Text>
|
||||
|
||||
<Text className="text-muted-foreground mb-3">
|
||||
Share your workout with your followers on Nostr
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
value={shareMessage}
|
||||
onChangeText={setShareMessage}
|
||||
className="min-h-[120px] p-3 mb-4"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full mb-3"
|
||||
style={{ backgroundColor: purpleColor }}
|
||||
onPress={handleShare}
|
||||
>
|
||||
<Text className="text-white font-medium">
|
||||
Share to Nostr
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onPress={handleSkip}
|
||||
>
|
||||
<Text>
|
||||
Skip Sharing
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View className="w-full max-w-md">
|
||||
{/* Trophy and heading */}
|
||||
<View className="items-center mb-8">
|
||||
<Trophy size={60} color="#F59E0B" />
|
||||
<Text className="text-2xl font-bold text-center mt-4 mb-2">
|
||||
Workout Complete!
|
||||
{/* If local-only or not authenticated */}
|
||||
{(options.storageType === 'local_only' || !isAuthenticated) && (
|
||||
<Button
|
||||
className="w-full mt-4"
|
||||
style={{ backgroundColor: purpleColor }}
|
||||
onPress={handleSkip}
|
||||
>
|
||||
<Text className="text-white font-medium">
|
||||
Continue
|
||||
</Text>
|
||||
<Text className="text-center text-muted-foreground">
|
||||
Great job on finishing your workout!
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Show sharing options for Nostr if appropriate */}
|
||||
{isAuthenticated && options.storageType !== 'local_only' && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
|
||||
<View className="mb-4">
|
||||
<Text className="text-lg font-semibold mb-3">
|
||||
Share Your Achievement
|
||||
</Text>
|
||||
|
||||
<Text className="text-muted-foreground mb-3">
|
||||
Share your workout with your followers on Nostr
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
value={shareMessage}
|
||||
onChangeText={setShareMessage}
|
||||
className="min-h-[120px] p-3 mb-4"
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full mb-3"
|
||||
style={{ backgroundColor: purpleColor }}
|
||||
onPress={handleShare}
|
||||
>
|
||||
<Text className="text-white font-medium">
|
||||
Share to Nostr
|
||||
</Text>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onPress={handleSkip}
|
||||
>
|
||||
<Text>
|
||||
Skip Sharing
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* If local-only or not authenticated */}
|
||||
{(options.storageType === 'local_only' || !isAuthenticated) && (
|
||||
<Button
|
||||
className="w-full mt-4"
|
||||
style={{ backgroundColor: purpleColor }}
|
||||
onPress={handleSkip}
|
||||
>
|
||||
<Text className="text-white font-medium">
|
||||
Continue
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkoutCompletionFlow({
|
||||
onComplete,
|
||||
@ -637,8 +643,6 @@ export function WorkoutCompletionFlow({
|
||||
onComplete: (options: WorkoutCompletionOptions) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { stopWorkoutTimer } = useWorkoutStore();
|
||||
|
||||
// States
|
||||
const [options, setOptions] = useState<WorkoutCompletionOptions>({
|
||||
storageType: 'local_only',
|
||||
@ -658,9 +662,6 @@ export function WorkoutCompletionFlow({
|
||||
};
|
||||
|
||||
const handleFinish = () => {
|
||||
// Stop workout timer when finishing
|
||||
stopWorkoutTimer();
|
||||
|
||||
// Move to celebration screen
|
||||
setStep('celebration');
|
||||
};
|
||||
|
@ -1,5 +1,20 @@
|
||||
// stores/workoutStore.ts
|
||||
|
||||
/**
|
||||
* Workout Store
|
||||
*
|
||||
* This store manages the state for active workouts including:
|
||||
* - Starting, pausing, and completing workouts
|
||||
* - Managing exercise sets and completion status
|
||||
* - Handling workout timing and duration tracking
|
||||
* - Publishing workout data to Nostr when requested
|
||||
* - Tracking favorite templates
|
||||
*
|
||||
* The store uses a timestamp-based approach for duration tracking,
|
||||
* capturing start and end times to accurately represent workout duration
|
||||
* even when accounting for time spent in completion flow.
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { createSelectors } from '@/utils/createSelectors';
|
||||
import { generateId } from '@/utils/ids';
|
||||
@ -76,8 +91,8 @@ interface ExtendedWorkoutActions extends WorkoutActions {
|
||||
startWorkout: (workout: Partial<Workout>) => void;
|
||||
pauseWorkout: () => void;
|
||||
resumeWorkout: () => void;
|
||||
completeWorkout: () => void;
|
||||
cancelWorkout: () => void;
|
||||
completeWorkout: (options?: WorkoutCompletionOptions) => Promise<void>;
|
||||
cancelWorkout: () => Promise<void>;
|
||||
reset: () => void;
|
||||
publishEvent: (event: NostrEvent) => Promise<any>;
|
||||
|
||||
@ -142,6 +157,9 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
|
||||
// Core Workout Flow
|
||||
startWorkout: (workoutData: Partial<Workout> = {}) => {
|
||||
// First stop any existing timer to avoid duplicate timers
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
const workout: Workout = {
|
||||
id: generateId('local'),
|
||||
title: workoutData.title || 'Quick Workout',
|
||||
@ -174,6 +192,9 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
const { status, activeWorkout } = get();
|
||||
if (status !== 'active' || !activeWorkout) return;
|
||||
|
||||
// Stop the timer interval when pausing
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
set({ status: 'paused' });
|
||||
// Auto-save when pausing
|
||||
saveWorkout(activeWorkout);
|
||||
@ -184,29 +205,30 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
if (status !== 'paused' || !activeWorkout) return;
|
||||
|
||||
set({ status: 'active' });
|
||||
|
||||
// Restart the timer when resuming
|
||||
get().startWorkoutTimer();
|
||||
},
|
||||
|
||||
// Update completeWorkout in workoutStore.ts
|
||||
completeWorkout: async (options?: WorkoutCompletionOptions) => {
|
||||
const { activeWorkout } = get();
|
||||
if (!activeWorkout) return;
|
||||
|
||||
// Stop the workout timer
|
||||
|
||||
// Ensure workout timer is stopped
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
|
||||
// If no options were provided, show the completion flow
|
||||
if (!options) {
|
||||
// Navigate to the completion flow screen
|
||||
router.push('/(workout)/complete');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const completedWorkout = {
|
||||
...activeWorkout,
|
||||
isCompleted: true,
|
||||
endTime: Date.now(),
|
||||
lastUpdated: Date.now()
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
// Save workout locally regardless of storage option
|
||||
@ -291,13 +313,20 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure workout timer is stopped again (just to be extra safe)
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
// Finally update the app state
|
||||
set({
|
||||
status: 'completed',
|
||||
activeWorkout: completedWorkout,
|
||||
activeWorkout: null, // Set to null to fully clear the workout
|
||||
isActive: false,
|
||||
isMinimized: false
|
||||
});
|
||||
|
||||
// Ensure we fully reset the state
|
||||
get().reset();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error completing workout:', error);
|
||||
// Consider showing an error message to the user
|
||||
@ -336,7 +365,7 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
const { activeWorkout } = get();
|
||||
if (!activeWorkout) return;
|
||||
|
||||
// Stop the workout timer
|
||||
// Ensure workout timer is stopped
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
// Prepare canceled workout with proper metadata
|
||||
@ -545,7 +574,7 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
}
|
||||
},
|
||||
|
||||
// Workout timer management - new functions
|
||||
// Workout timer management - improved for reliability
|
||||
startWorkoutTimer: () => {
|
||||
// Clear any existing timer first to prevent duplicates
|
||||
if (workoutTimerInterval) {
|
||||
@ -555,6 +584,7 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
|
||||
// Start a new timer that continues to run even when components unmount
|
||||
workoutTimerInterval = setInterval(() => {
|
||||
// Get fresh state reference to avoid stale closures
|
||||
const { status } = useWorkoutStoreBase.getState();
|
||||
if (status === 'active') {
|
||||
useWorkoutStoreBase.getState().tick(1000);
|
||||
@ -720,13 +750,28 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
endWorkout: async () => {
|
||||
const { activeWorkout } = get();
|
||||
if (!activeWorkout) return;
|
||||
|
||||
await get().completeWorkout();
|
||||
|
||||
// Set the end time right when entering completion flow
|
||||
set({
|
||||
activeWorkout: {
|
||||
...activeWorkout,
|
||||
endTime: Date.now(),
|
||||
lastUpdated: Date.now()
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure to stop the timer before navigating
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
// Navigate to completion screen
|
||||
router.push('/(workout)/complete');
|
||||
},
|
||||
|
||||
clearAutoSave: async () => {
|
||||
// TODO: Implement clearing autosave from storage
|
||||
get().stopWorkoutTimer(); // Make sure to stop the timer
|
||||
|
||||
// Make sure to stop the timer
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
// Preserve favorites when resetting
|
||||
const favoriteIds = get().favoriteIds;
|
||||
@ -749,7 +794,8 @@ const useWorkoutStoreBase = create<ExtendedWorkoutState & ExtendedWorkoutActions
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
get().stopWorkoutTimer(); // Make sure to stop the timer
|
||||
// Make sure to stop the timer
|
||||
get().stopWorkoutTimer();
|
||||
|
||||
// Preserve favorites when resetting
|
||||
const favoriteIds = get().favoriteIds;
|
||||
@ -792,8 +838,9 @@ async function getTemplate(templateId: string): Promise<WorkoutTemplate | null>
|
||||
|
||||
async function saveWorkout(workout: Workout): Promise<void> {
|
||||
try {
|
||||
// Make sure we're capturing the duration properly in what's saved
|
||||
console.log('Saving workout with endTime:', workout.endTime);
|
||||
// TODO: Implement actual save logic using our database service
|
||||
console.log('Saving workout:', workout);
|
||||
} catch (error) {
|
||||
console.error('Error saving workout:', error);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user