2025-03-07 23:46:41 -05:00
|
|
|
|
// components/workout/WorkoutCard.tsx
|
|
|
|
|
import React from 'react';
|
2025-03-23 15:53:34 -04:00
|
|
|
|
import { View, TouchableOpacity } from 'react-native';
|
|
|
|
|
import { Text } from '@/components/ui/text';
|
|
|
|
|
import { ChevronRight, CloudIcon, SmartphoneIcon, CloudOffIcon } from 'lucide-react-native';
|
2025-03-07 23:46:41 -05:00
|
|
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
|
|
|
import { Workout } from '@/types/workout';
|
|
|
|
|
import { format } from 'date-fns';
|
|
|
|
|
import { useRouter } from 'expo-router';
|
2025-03-23 15:53:34 -04:00
|
|
|
|
import { cn } from '@/lib/utils';
|
2025-03-07 23:46:41 -05:00
|
|
|
|
|
2025-03-23 15:53:34 -04:00
|
|
|
|
export interface EnhancedWorkoutCardProps {
|
2025-03-07 23:46:41 -05:00
|
|
|
|
workout: Workout;
|
|
|
|
|
showDate?: boolean;
|
|
|
|
|
showExercises?: boolean;
|
2025-03-23 15:53:34 -04:00
|
|
|
|
source?: 'local' | 'nostr' | 'both';
|
|
|
|
|
publishStatus?: {
|
|
|
|
|
isPublished: boolean;
|
|
|
|
|
relayCount?: number;
|
|
|
|
|
lastPublished?: number;
|
|
|
|
|
};
|
|
|
|
|
onShare?: () => void;
|
|
|
|
|
onImport?: () => void;
|
2025-03-07 23:46:41 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate duration in hours and minutes
|
|
|
|
|
const formatDuration = (startTime: number, endTime: number) => {
|
|
|
|
|
const durationMs = endTime - startTime;
|
|
|
|
|
const hours = Math.floor(durationMs / (1000 * 60 * 60));
|
|
|
|
|
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
|
|
|
|
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
return `${hours}h ${minutes}m`;
|
|
|
|
|
}
|
|
|
|
|
return `${minutes}m`;
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-23 15:53:34 -04:00
|
|
|
|
export const WorkoutCard: React.FC<EnhancedWorkoutCardProps> = ({
|
2025-03-07 23:46:41 -05:00
|
|
|
|
workout,
|
|
|
|
|
showDate = true,
|
2025-03-23 15:53:34 -04:00
|
|
|
|
showExercises = true,
|
|
|
|
|
source,
|
|
|
|
|
publishStatus,
|
|
|
|
|
onShare,
|
|
|
|
|
onImport
|
2025-03-07 23:46:41 -05:00
|
|
|
|
}) => {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
const handlePress = () => {
|
|
|
|
|
// Navigate to workout details
|
|
|
|
|
console.log(`Navigate to workout ${workout.id}`);
|
2025-03-23 15:53:34 -04:00
|
|
|
|
router.push(`/workout/${workout.id}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Determine source if not explicitly provided
|
|
|
|
|
const workoutSource = source ||
|
|
|
|
|
(workout.availability?.source?.includes('nostr') && workout.availability?.source?.includes('local')
|
|
|
|
|
? 'both'
|
|
|
|
|
: workout.availability?.source?.includes('nostr')
|
|
|
|
|
? 'nostr'
|
|
|
|
|
: 'local');
|
|
|
|
|
|
|
|
|
|
// Determine publish status if not explicitly provided
|
|
|
|
|
const workoutPublishStatus = publishStatus || {
|
|
|
|
|
isPublished: Boolean(workout.availability?.nostrEventId),
|
|
|
|
|
relayCount: workout.availability?.nostrRelayCount,
|
|
|
|
|
lastPublished: workout.availability?.nostrPublishedAt
|
2025-03-07 23:46:41 -05:00
|
|
|
|
};
|
2025-03-23 15:53:34 -04:00
|
|
|
|
|
|
|
|
|
// Debug: Log exercises
|
|
|
|
|
console.log(`WorkoutCard for ${workout.id} has ${workout.exercises?.length || 0} exercises`);
|
|
|
|
|
if (workout.exercises && workout.exercises.length > 0) {
|
|
|
|
|
console.log(`First exercise: ${workout.exercises[0].title}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define colors for icons
|
|
|
|
|
const primaryColor = "#8b5cf6"; // Purple color
|
|
|
|
|
const mutedColor = "#9ca3af"; // Gray color
|
2025-03-07 23:46:41 -05:00
|
|
|
|
|
|
|
|
|
return (
|
2025-03-23 15:53:34 -04:00
|
|
|
|
<TouchableOpacity onPress={handlePress} activeOpacity={0.7} testID={`workout-card-${workout.id}`}>
|
|
|
|
|
<Card
|
|
|
|
|
className={cn(
|
|
|
|
|
"mb-4",
|
|
|
|
|
workoutSource === 'nostr' && "border-primary border-2",
|
|
|
|
|
workoutSource === 'both' && "border-primary border"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<CardContent className="p-4">
|
|
|
|
|
<View className="flex-row justify-between items-center mb-2">
|
|
|
|
|
<View className="flex-row items-center">
|
|
|
|
|
<Text
|
|
|
|
|
className={cn(
|
|
|
|
|
"text-lg font-semibold",
|
|
|
|
|
workoutSource === 'nostr' || workoutSource === 'both'
|
|
|
|
|
? "text-primary"
|
|
|
|
|
: "text-foreground"
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{workout.title}
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
|
|
{/* Source indicator */}
|
|
|
|
|
<View className="ml-2">
|
|
|
|
|
{workoutSource === 'local' && (
|
|
|
|
|
<SmartphoneIcon size={16} color={mutedColor} />
|
|
|
|
|
)}
|
|
|
|
|
{workoutSource === 'nostr' && (
|
|
|
|
|
<CloudIcon size={16} color={primaryColor} />
|
|
|
|
|
)}
|
|
|
|
|
{workoutSource === 'both' && (
|
|
|
|
|
<View className="flex-row">
|
|
|
|
|
<SmartphoneIcon size={16} color={mutedColor} />
|
|
|
|
|
<View style={{ width: 4 }} />
|
|
|
|
|
<CloudIcon size={16} color={primaryColor} />
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
<ChevronRight size={20} color={mutedColor} />
|
|
|
|
|
</View>
|
2025-03-07 23:46:41 -05:00
|
|
|
|
|
|
|
|
|
{showDate && (
|
|
|
|
|
<Text className="text-muted-foreground mb-2">
|
|
|
|
|
{format(workout.startTime, 'EEEE, MMM d')}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-03-23 15:53:34 -04:00
|
|
|
|
{/* Publish status indicator */}
|
|
|
|
|
{workoutSource !== 'nostr' && (
|
|
|
|
|
<View className="flex-row items-center mb-2">
|
|
|
|
|
{workoutPublishStatus.isPublished ? (
|
|
|
|
|
<View className="flex-row items-center">
|
|
|
|
|
<CloudIcon size={14} color={primaryColor} style={{ marginRight: 4 }} />
|
|
|
|
|
<Text className="text-xs text-muted-foreground">
|
|
|
|
|
Published to {workoutPublishStatus.relayCount || 0} relays
|
|
|
|
|
{workoutPublishStatus.lastPublished &&
|
|
|
|
|
` on ${format(workoutPublishStatus.lastPublished, 'MMM d')}`}
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
|
|
{onShare && (
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
onPress={onShare}
|
|
|
|
|
className="ml-2 px-2 py-1 bg-primary/10 rounded"
|
|
|
|
|
>
|
|
|
|
|
<Text className="text-xs text-primary">Republish</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
) : (
|
|
|
|
|
<View className="flex-row items-center">
|
|
|
|
|
<CloudOffIcon size={14} color={mutedColor} style={{ marginRight: 4 }} />
|
|
|
|
|
<Text className="text-xs text-muted-foreground">Local only</Text>
|
|
|
|
|
|
|
|
|
|
{onShare && (
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
onPress={onShare}
|
|
|
|
|
className="ml-2 px-2 py-1 bg-primary/10 rounded"
|
|
|
|
|
>
|
|
|
|
|
<Text className="text-xs text-primary">Publish</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Import button for Nostr-only workouts */}
|
|
|
|
|
{workoutSource === 'nostr' && onImport && (
|
|
|
|
|
<View className="mb-2">
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
onPress={onImport}
|
|
|
|
|
className="px-2 py-1 bg-primary/10 rounded self-start"
|
|
|
|
|
>
|
|
|
|
|
<Text className="text-xs text-primary">Import to local</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-03-07 23:46:41 -05:00
|
|
|
|
<View className="flex-row items-center mt-2">
|
|
|
|
|
<View className="flex-row items-center mr-4">
|
|
|
|
|
<View className="w-6 h-6 items-center justify-center mr-1">
|
|
|
|
|
<Text className="text-muted-foreground">⏱️</Text>
|
|
|
|
|
</View>
|
|
|
|
|
<Text className="text-muted-foreground">
|
|
|
|
|
{formatDuration(workout.startTime, workout.endTime || Date.now())}
|
|
|
|
|
</Text>
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
<View className="flex-row items-center mr-4">
|
|
|
|
|
<View className="w-6 h-6 items-center justify-center mr-1">
|
|
|
|
|
<Text className="text-muted-foreground">⚖️</Text>
|
|
|
|
|
</View>
|
|
|
|
|
<Text className="text-muted-foreground">
|
|
|
|
|
{workout.totalVolume ? `${workout.totalVolume} lb` : '0 lb'}
|
|
|
|
|
</Text>
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
{workout.totalReps && (
|
|
|
|
|
<View className="flex-row items-center">
|
|
|
|
|
<View className="w-6 h-6 items-center justify-center mr-1">
|
|
|
|
|
<Text className="text-muted-foreground">🔄</Text>
|
|
|
|
|
</View>
|
|
|
|
|
<Text className="text-muted-foreground">
|
|
|
|
|
{workout.totalReps} reps
|
|
|
|
|
</Text>
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
{/* Show exercises if requested */}
|
|
|
|
|
{showExercises && (
|
|
|
|
|
<View className="mt-4">
|
|
|
|
|
<Text className="text-foreground font-semibold mb-1">Exercise</Text>
|
|
|
|
|
{/* In a real implementation, you would map through actual exercises */}
|
|
|
|
|
{workout.exercises && workout.exercises.length > 0 ? (
|
|
|
|
|
workout.exercises.slice(0, 3).map((exercise, idx) => (
|
|
|
|
|
<Text key={idx} className="text-foreground mb-1">
|
|
|
|
|
{exercise.title}
|
|
|
|
|
</Text>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
<Text className="text-muted-foreground">No exercises recorded</Text>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{workout.exercises && workout.exercises.length > 3 && (
|
|
|
|
|
<Text className="text-muted-foreground">
|
|
|
|
|
+{workout.exercises.length - 3} more exercises
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
2025-03-23 15:53:34 -04:00
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</TouchableOpacity>
|
2025-03-07 23:46:41 -05:00
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-23 15:53:34 -04:00
|
|
|
|
export default WorkoutCard;
|