// components/workout/WorkoutCompletionFlow.tsx import React, { useState, useEffect } from 'react'; import { View, TouchableOpacity, ScrollView } from 'react-native'; import { Text } from '@/components/ui/text'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { useWorkoutStore } from '@/stores/workoutStore'; import { useNDK, useNDKCurrentUser } from '@/lib/hooks/useNDK'; import { WorkoutCompletionOptions } from '@/types/workout'; import { Shield, Lock, FileText, Clock, Dumbbell, Trophy, 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, setOptions, onNext }: { options: WorkoutCompletionOptions, setOptions: (options: WorkoutCompletionOptions) => void, onNext: () => void }) { const { isAuthenticated } = useNDKCurrentUser(); // Handle storage option selection const handleStorageOptionSelect = (value: 'local_only' | 'publish_complete' | 'publish_limited') => { setOptions({ ...options, storageType: value }); }; // Handle template action selection const handleTemplateAction = (value: 'keep_original' | 'update_existing' | 'save_as_new') => { setOptions({ ...options, templateAction: value }); }; // Purple color used throughout the app const purpleColor = 'hsl(261, 90%, 66%)'; // Check if the workout is based on a template const { activeWorkout } = useWorkoutStore(); // Use a try-catch block for more resilience let hasTemplateChanges = false; try { if (activeWorkout && activeWorkout.templateId) { hasTemplateChanges = TemplateService.hasTemplateChanges(activeWorkout); } } catch (error) { console.error('Error checking template changes:', error); } return ( Storage Options Choose how you want to store your workout data {/* Local only option */} handleStorageOptionSelect('local_only')} activeOpacity={0.7} > Local Only Keep workout data private on this device {/* Publish complete option */} {isAuthenticated && ( handleStorageOptionSelect('publish_complete')} activeOpacity={0.7} > Publish Complete Publish full workout data to Nostr network )} {/* Limited metrics option */} {isAuthenticated && ( handleStorageOptionSelect('publish_limited')} activeOpacity={0.7} > Limited Metrics Publish workout with limited metrics for privacy )} {/* Template options section - only if needed */} {hasTemplateChanges && ( <> Template Options Your workout includes modifications to the original template {/* Keep original option */} handleTemplateAction('keep_original')} activeOpacity={0.7} > Keep Original Don't update the template {/* Update existing option */} handleTemplateAction('update_existing')} activeOpacity={0.7} > Update Template Save these changes to the original template {/* Save as new option */} handleTemplateAction('save_as_new')} activeOpacity={0.7} > Save as New Create a new template from this workout {/* Template name input if save as new is selected */} {options.templateAction === 'save_as_new' && ( New template name: setOptions({ ...options, newTemplateName: text })} /> )} )} {/* Next button */} ); } // Summary component function SummaryTab({ options, onBack, onFinish }: { options: WorkoutCompletionOptions, onBack: () => void, onFinish: () => void }) { const { activeWorkout } = useWorkoutStore(); // Helper functions const formatDuration = (ms: number): string => { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); return hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m ${seconds % 60}s`; }; // Get workout duration using timestamps const getDuration = (): number => { if (!activeWorkout || !activeWorkout.endTime) return 0; return activeWorkout.endTime - activeWorkout.startTime; }; const getTotalSets = (): number => { return activeWorkout?.exercises.reduce( (total, exercise) => total + exercise.sets.length, 0 ) || 0; }; const getCompletedSets = (): number => { return activeWorkout?.exercises.reduce( (total, exercise) => total + exercise.sets.filter(set => set.isCompleted).length, 0 ) || 0; }; // Calculate volume const getTotalVolume = (): number => { return activeWorkout?.exercises.reduce( (total, exercise) => total + exercise.sets.reduce( (setTotal, set) => setTotal + ((set.weight || 0) * (set.reps || 0)), 0 ), 0 ) || 0; }; // Mock PRs - in a real app, you'd compare with previous workouts const mockPRs = [ { exercise: "Bench Press", value: "80kg × 8 reps", previous: "75kg × 8 reps" }, { exercise: "Squat", value: "120kg × 5 reps", previous: "110kg × 5 reps" } ]; // Purple color used throughout the app const purpleColor = 'hsl(261, 90%, 66%)'; return ( Workout Summary {/* Duration card */} Duration {formatDuration(getDuration())} {/* Exercise stats card */} Exercises Total Exercises: {activeWorkout?.exercises.length || 0} Sets Completed: {getCompletedSets()} / {getTotalSets()} Total Volume: {getTotalVolume()} kg Estimated Calories: {Math.round(getDuration() / 1000 / 60 * 5)} kcal {/* PRs Card */} Personal Records {mockPRs.length > 0 ? ( {mockPRs.map((pr, index) => ( {pr.exercise} New PR: {pr.value} Previous: {pr.previous} {index < mockPRs.length - 1 && } ))} ) : ( No personal records set in this workout )} {/* Selected storage option card */} Selected Options Storage: {options.storageType === 'local_only' ? 'Local Only' : options.storageType === 'publish_complete' ? 'Full Metrics' : 'Limited Metrics' } {/* Navigation buttons */} ); } // Celebration component with share option function CelebrationTab({ 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! 💪"; 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! 🏆"; } } 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 ( {showConfetti && ( setShowConfetti(false)} /> )} {/* Trophy and heading */} Workout Complete! Great job on finishing your workout! {/* Show sharing options for Nostr if appropriate */} {isAuthenticated && options.storageType !== 'local_only' && ( <> Share Your Achievement Share your workout with your followers on Nostr )} {/* If local-only or not authenticated */} {(options.storageType === 'local_only' || !isAuthenticated) && ( )} ); } export function WorkoutCompletionFlow({ onComplete, onCancel }: { onComplete: (options: WorkoutCompletionOptions) => void; onCancel: () => void; }) { // States const [options, setOptions] = useState({ storageType: 'local_only', shareOnSocial: false, templateAction: 'keep_original', }); const [step, setStep] = useState<'options' | 'summary' | 'celebration'>('options'); // Navigate through steps const handleNext = () => { setStep('summary'); }; const handleBack = () => { setStep('options'); }; const handleFinish = () => { // Move to celebration screen setStep('celebration'); }; const handleComplete = (finalOptions: WorkoutCompletionOptions) => { // Call the completion function with the selected options onComplete(finalOptions); }; const renderStep = () => { switch (step) { case 'options': return ( ); case 'summary': return ( ); case 'celebration': return ( ); default: return null; } }; return ( {renderStep()} ); }