2025-02-09 20:38:38 -05:00
|
|
|
// app/(tabs)/library/exercises.tsx
|
2025-02-18 12:15:19 -05:00
|
|
|
import React, { useRef, useState, useCallback } from 'react';
|
|
|
|
import { View, SectionList, TouchableOpacity, ViewToken } from 'react-native';
|
2025-02-09 20:38:38 -05:00
|
|
|
import { Text } from '@/components/ui/text';
|
2025-02-18 12:15:19 -05:00
|
|
|
import { Button } from '@/components/ui/button';
|
2025-02-09 20:38:38 -05:00
|
|
|
import { ExerciseCard } from '@/components/exercises/ExerciseCard';
|
|
|
|
import { FloatingActionButton } from '@/components/shared/FloatingActionButton';
|
|
|
|
import { NewExerciseSheet } from '@/components/library/NewExerciseSheet';
|
|
|
|
import { Dumbbell } from 'lucide-react-native';
|
2025-02-18 12:15:19 -05:00
|
|
|
import { BaseExercise, Exercise } from '@/types/exercise';
|
|
|
|
import { useExercises } from '@/lib/hooks/useExercises';
|
2025-02-17 11:24:17 -05:00
|
|
|
|
2025-02-09 20:38:38 -05:00
|
|
|
export default function ExercisesScreen() {
|
2025-02-17 11:24:17 -05:00
|
|
|
const sectionListRef = useRef<SectionList>(null);
|
2025-02-09 20:38:38 -05:00
|
|
|
const [showNewExercise, setShowNewExercise] = useState(false);
|
2025-02-18 12:15:19 -05:00
|
|
|
const [currentSection, setCurrentSection] = useState<string>('');
|
2025-02-09 20:38:38 -05:00
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
const {
|
|
|
|
exercises,
|
|
|
|
loading,
|
|
|
|
error,
|
|
|
|
stats,
|
|
|
|
createExercise,
|
|
|
|
deleteExercise,
|
|
|
|
refreshExercises
|
|
|
|
} = useExercises();
|
2025-02-16 23:53:28 -05:00
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
// Organize exercises into sections
|
|
|
|
const sections = React.useMemo(() => {
|
2025-02-17 11:24:17 -05:00
|
|
|
const exercisesByLetter = exercises.reduce((acc, exercise) => {
|
|
|
|
const firstLetter = exercise.title[0].toUpperCase();
|
|
|
|
if (!acc[firstLetter]) {
|
|
|
|
acc[firstLetter] = [];
|
|
|
|
}
|
|
|
|
acc[firstLetter].push(exercise);
|
|
|
|
return acc;
|
|
|
|
}, {} as Record<string, Exercise[]>);
|
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
return Object.entries(exercisesByLetter)
|
2025-02-17 11:24:17 -05:00
|
|
|
.map(([letter, exercises]) => ({
|
|
|
|
title: letter,
|
|
|
|
data: exercises.sort((a, b) => a.title.localeCompare(b.title))
|
|
|
|
}))
|
|
|
|
.sort((a, b) => a.title.localeCompare(b.title));
|
|
|
|
}, [exercises]);
|
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
const handleViewableItemsChanged = useCallback(({
|
|
|
|
viewableItems
|
|
|
|
}: {
|
|
|
|
viewableItems: ViewToken[];
|
|
|
|
}) => {
|
|
|
|
const firstSection = viewableItems.find(item => item.section)?.section?.title;
|
|
|
|
if (firstSection) {
|
|
|
|
setCurrentSection(firstSection);
|
2025-02-16 23:53:28 -05:00
|
|
|
}
|
2025-02-18 12:15:19 -05:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
const scrollToSection = useCallback((letter: string) => {
|
|
|
|
const sectionIndex = sections.findIndex(section => section.title === letter);
|
|
|
|
if (sectionIndex !== -1 && sectionListRef.current) {
|
|
|
|
// Try to scroll to section
|
|
|
|
sectionListRef.current.scrollToLocation({
|
|
|
|
animated: true,
|
|
|
|
sectionIndex,
|
|
|
|
itemIndex: 0,
|
|
|
|
viewPosition: 0, // 0 means top of the view
|
|
|
|
});
|
|
|
|
|
|
|
|
// Log for debugging
|
|
|
|
if (__DEV__) {
|
|
|
|
console.log('Scrolling to section:', {
|
|
|
|
letter,
|
|
|
|
sectionIndex,
|
|
|
|
totalSections: sections.length
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [sections]);
|
2025-02-09 20:38:38 -05:00
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
// Add getItemLayout to optimize scrolling
|
|
|
|
const getItemLayout = useCallback((data: any, index: number) => ({
|
|
|
|
length: 100, // Approximate height of each item
|
|
|
|
offset: 100 * index,
|
|
|
|
index,
|
|
|
|
}), []);
|
|
|
|
|
|
|
|
|
|
|
|
const handleAddExercise = async (exerciseData: Omit<BaseExercise, 'id' | 'availability' | 'created_at'>) => {
|
2025-02-16 23:53:28 -05:00
|
|
|
try {
|
2025-02-18 12:15:19 -05:00
|
|
|
const newExercise: Omit<Exercise, 'id'> = {
|
2025-02-16 23:53:28 -05:00
|
|
|
...exerciseData,
|
2025-02-18 12:15:19 -05:00
|
|
|
source: 'local',
|
2025-02-16 23:53:28 -05:00
|
|
|
created_at: Date.now(),
|
2025-02-18 12:15:19 -05:00
|
|
|
availability: {
|
|
|
|
source: ['local']
|
|
|
|
},
|
|
|
|
format_json: exerciseData.format ? JSON.stringify(exerciseData.format) : undefined,
|
|
|
|
format_units_json: exerciseData.format_units ? JSON.stringify(exerciseData.format_units) : undefined
|
|
|
|
};
|
|
|
|
|
|
|
|
await createExercise(newExercise);
|
2025-02-16 23:53:28 -05:00
|
|
|
setShowNewExercise(false);
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error adding exercise:', error);
|
|
|
|
}
|
|
|
|
};
|
2025-02-09 20:38:38 -05:00
|
|
|
|
2025-02-16 23:53:28 -05:00
|
|
|
const handleDelete = async (id: string) => {
|
|
|
|
try {
|
2025-02-18 12:15:19 -05:00
|
|
|
await deleteExercise(id);
|
2025-02-16 23:53:28 -05:00
|
|
|
} catch (error) {
|
|
|
|
console.error('Error deleting exercise:', error);
|
|
|
|
}
|
2025-02-09 20:38:38 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
const handleExercisePress = (exerciseId: string) => {
|
|
|
|
console.log('Selected exercise:', exerciseId);
|
|
|
|
};
|
|
|
|
|
2025-02-16 23:53:28 -05:00
|
|
|
const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
2025-02-17 11:24:17 -05:00
|
|
|
const availableLetters = new Set(sections.map(section => section.title));
|
2025-02-16 23:53:28 -05:00
|
|
|
|
2025-02-18 12:15:19 -05:00
|
|
|
if (loading) {
|
|
|
|
return (
|
|
|
|
<View className="flex-1 items-center justify-center bg-background">
|
|
|
|
<Text>Loading exercises...</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
return (
|
|
|
|
<View className="flex-1 items-center justify-center p-4 bg-background">
|
|
|
|
<Text className="text-destructive text-center mb-4">
|
|
|
|
{error.message}
|
|
|
|
</Text>
|
|
|
|
<Button onPress={refreshExercises}>
|
|
|
|
<Text>Retry</Text>
|
|
|
|
</Button>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-02-09 20:38:38 -05:00
|
|
|
return (
|
|
|
|
<View className="flex-1 bg-background">
|
2025-02-18 12:15:19 -05:00
|
|
|
{/* Stats Bar */}
|
|
|
|
<View className="flex-row justify-between items-center p-4 bg-card border-b border-border">
|
|
|
|
<View>
|
|
|
|
<Text className="text-sm text-muted-foreground">Total Exercises</Text>
|
|
|
|
<Text className="text-2xl font-bold">{stats.totalCount}</Text>
|
|
|
|
</View>
|
|
|
|
<View className="flex-row gap-4">
|
|
|
|
<View>
|
|
|
|
<Text className="text-xs text-muted-foreground">Push</Text>
|
|
|
|
<Text className="text-base font-medium">{stats.byCategory['Push'] || 0}</Text>
|
|
|
|
</View>
|
|
|
|
<View>
|
|
|
|
<Text className="text-xs text-muted-foreground">Pull</Text>
|
|
|
|
<Text className="text-base font-medium">{stats.byCategory['Pull'] || 0}</Text>
|
|
|
|
</View>
|
|
|
|
<View>
|
|
|
|
<Text className="text-xs text-muted-foreground">Legs</Text>
|
|
|
|
<Text className="text-base font-medium">{stats.byCategory['Legs'] || 0}</Text>
|
|
|
|
</View>
|
|
|
|
<View>
|
|
|
|
<Text className="text-xs text-muted-foreground">Core</Text>
|
|
|
|
<Text className="text-base font-medium">{stats.byCategory['Core'] || 0}</Text>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
{/* Exercise List with Alphabet Scroll */}
|
|
|
|
<View className="flex-1 flex-row">
|
|
|
|
{/* Main List */}
|
|
|
|
<View className="flex-1">
|
|
|
|
<SectionList
|
|
|
|
ref={sectionListRef}
|
|
|
|
sections={sections}
|
|
|
|
keyExtractor={(item) => item.id}
|
|
|
|
getItemLayout={getItemLayout}
|
|
|
|
renderSectionHeader={({ section }) => (
|
|
|
|
<View className="py-2 px-4 bg-background/80">
|
|
|
|
<Text className="text-lg font-semibold text-foreground">
|
|
|
|
{section.title}
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
renderItem={({ item }) => (
|
|
|
|
<View className="px-4 py-1">
|
|
|
|
<ExerciseCard
|
|
|
|
{...item}
|
|
|
|
onPress={() => handleExercisePress(item.id)}
|
|
|
|
onDelete={() => handleDelete(item.id)}
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
stickySectionHeadersEnabled
|
|
|
|
initialNumToRender={10}
|
|
|
|
maxToRenderPerBatch={10}
|
|
|
|
windowSize={5}
|
|
|
|
onViewableItemsChanged={handleViewableItemsChanged}
|
|
|
|
viewabilityConfig={{
|
|
|
|
itemVisiblePercentThreshold: 50
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
{/* Alphabet List */}
|
|
|
|
<View
|
|
|
|
className="w-8 justify-center bg-transparent px-1"
|
|
|
|
onStartShouldSetResponder={() => true}
|
|
|
|
onResponderMove={(evt) => {
|
|
|
|
const touch = evt.nativeEvent;
|
|
|
|
const element = evt.target;
|
|
|
|
|
|
|
|
// Get the layout of the alphabet bar
|
|
|
|
if (element) {
|
|
|
|
const elementPosition = (element as any).measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
|
|
|
|
// Calculate which letter we're touching based on position
|
|
|
|
const totalHeight = height;
|
|
|
|
const letterHeight = totalHeight / alphabet.length;
|
|
|
|
const touchY = touch.pageY - pageY;
|
|
|
|
const index = Math.min(
|
|
|
|
Math.max(Math.floor(touchY / letterHeight), 0),
|
|
|
|
alphabet.length - 1
|
|
|
|
);
|
|
|
|
|
|
|
|
const letter = alphabet[index];
|
|
|
|
if (availableLetters.has(letter)) {
|
|
|
|
scrollToSection(letter);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{alphabet.map((letter) => (
|
2025-02-17 11:24:17 -05:00
|
|
|
<Text
|
2025-02-18 12:15:19 -05:00
|
|
|
key={letter}
|
|
|
|
className={
|
|
|
|
letter === currentSection
|
|
|
|
? 'text-xs text-center text-primary font-bold py-0.5'
|
|
|
|
: availableLetters.has(letter)
|
|
|
|
? 'text-xs text-center text-primary font-medium py-0.5'
|
|
|
|
: 'text-xs text-center text-muted-foreground py-0.5'
|
|
|
|
}
|
2025-02-17 11:24:17 -05:00
|
|
|
>
|
|
|
|
{letter}
|
|
|
|
</Text>
|
2025-02-18 12:15:19 -05:00
|
|
|
))}
|
|
|
|
</View>
|
2025-02-16 23:53:28 -05:00
|
|
|
</View>
|
2025-02-09 20:38:38 -05:00
|
|
|
|
|
|
|
<FloatingActionButton
|
|
|
|
icon={Dumbbell}
|
|
|
|
onPress={() => setShowNewExercise(true)}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<NewExerciseSheet
|
|
|
|
isOpen={showNewExercise}
|
|
|
|
onClose={() => setShowNewExercise(false)}
|
|
|
|
onSubmit={handleAddExercise}
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|