Fix calendar date highlighting shape and improve workout date detection

This commit is contained in:
DocNR 2025-03-23 11:07:26 -04:00
parent 3004bcd4f8
commit a5d98ba251
2 changed files with 143 additions and 81 deletions

View File

@ -5,6 +5,39 @@ All notable changes to the POWR project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Changelog - March 24, 2025
## Added
- Unified workout history service and hook
- Created `UnifiedWorkoutHistoryService` that combines functionality from multiple services
- Implemented `useWorkoutHistory` hook for simplified data access
- Added real-time Nostr updates with subscription support
- Improved filtering capabilities across local and Nostr workouts
- Enhanced type safety with better interfaces
- Added comprehensive migration guide for developers
## Improved
- Consolidated workout history architecture
- Reduced code duplication across services
- Simplified API for accessing workout data
- Enhanced performance with optimized database queries
- Better error handling throughout the system
- Improved documentation with migration examples
- Enhanced calendar view with fallback filtering for dates
## Fixed
- Calendar view now properly shows workouts when clicking on dates
- Added fallback mechanism to filter workouts manually if database query returns no results
- Improved logging for better debugging
- Fixed edge cases where workouts wouldn't appear in the calendar view
## Removed
- Legacy workout history services
- Removed `EnhancedWorkoutHistoryService`
- Removed `NostrWorkoutHistoryService`
- Removed `useNostrWorkoutHistory` hook
- Completed migration to unified workout history API
# Changelog - March 23, 2025 # Changelog - March 23, 2025
## Fixed ## Fixed
@ -22,12 +55,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added checks to verify if exercises exist in the database - Added checks to verify if exercises exist in the database
- Fixed TypeScript errors in exercise existence checks - Fixed TypeScript errors in exercise existence checks
- Improved error handling throughout the service - Improved error handling throughout the service
- Calendar view UI and functionality
- Fixed date highlighting shape to use clean circles instead of hexagons
- Removed shadow effects causing visual distortion in calendar dates
- Improved workout date detection with better fallback mechanisms
- Enhanced exercise name display in workout cards
## Improved ## Improved
- Enhanced debugging capabilities - Enhanced debugging capabilities
- Added comprehensive logging in EnhancedWorkoutHistoryService - Added comprehensive logging in EnhancedWorkoutHistoryService
- Improved error state handling in workout detail screen - Improved error state handling in workout detail screen
- Better error messages for troubleshooting - Better error messages for troubleshooting
- Calendar view reliability
- Added fallback date filtering when database queries return no results
- Improved workout date detection with combined data sources
- Enhanced visual consistency of date highlighting
# Changelog - March 22, 2025 # Changelog - March 22, 2025

View File

@ -7,9 +7,10 @@ import { Workout } from '@/types/workout';
import { format, isSameDay } from 'date-fns'; import { format, isSameDay } from 'date-fns';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { WorkoutHistoryService } from '@/lib/db/services/WorkoutHistoryService';
import WorkoutCard from '@/components/workout/WorkoutCard'; import WorkoutCard from '@/components/workout/WorkoutCard';
import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react-native'; import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react-native';
import { useNDKCurrentUser } from '@/lib/hooks/useNDK';
import { useWorkoutHistory } from '@/lib/hooks/useWorkoutHistory';
// Add custom styles for 1/7 width (for calendar days) // Add custom styles for 1/7 width (for calendar days)
const styles = { const styles = {
@ -19,6 +20,11 @@ const styles = {
// Week days for the calendar view - Fixed the duplicate key issue // Week days for the calendar view - Fixed the duplicate key issue
const WEEK_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const WEEK_DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// Define colors for icons and styling
const primaryColor = "#8b5cf6"; // Purple color
const mutedColor = "#9ca3af"; // Gray color
const primaryBgColor = "rgba(139, 92, 246, 0.2)"; // Semi-transparent purple for date highlights
// Mock data for when database tables aren't yet created // Mock data for when database tables aren't yet created
const mockWorkouts: Workout[] = [ const mockWorkouts: Workout[] = [
{ {
@ -57,50 +63,50 @@ export default function CalendarScreen() {
const [useMockData, setUseMockData] = useState(false); const [useMockData, setUseMockData] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
// Initialize workout history service const { isAuthenticated } = useNDKCurrentUser();
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]); const [includeNostr, setIncludeNostr] = useState(true);
// Load workouts function // Use the unified workout history hook
const loadWorkouts = async () => { const {
try { workouts: allWorkouts,
loading,
refresh,
getWorkoutsByDate,
service: workoutHistoryService
} = useWorkoutHistory({
includeNostr: includeNostr,
realtime: true
});
// Set workouts from the hook
useEffect(() => {
if (loading) {
setIsLoading(true); setIsLoading(true);
const allWorkouts = await workoutHistoryService.getAllWorkouts(); } else {
setWorkouts(allWorkouts); setWorkouts(allWorkouts);
setUseMockData(false); setIsLoading(false);
} catch (error) { setRefreshing(false);
console.error('Error loading workouts:', error);
// Check if the error is about missing tables // Check if we need to use mock data (empty workouts)
const errorMsg = error instanceof Error ? error.message : String(error); if (allWorkouts.length === 0) {
if (errorMsg.includes('no such table')) { console.log('No workouts found, using mock data');
console.log('Using mock data because workout tables not yet created');
setWorkouts(mockWorkouts); setWorkouts(mockWorkouts);
setUseMockData(true); setUseMockData(true);
} else { } else {
// For other errors, just show empty state
setWorkouts([]);
setUseMockData(false); setUseMockData(false);
} }
} finally {
setIsLoading(false);
setRefreshing(false);
} }
}; }, [allWorkouts, loading]);
// Initial load workouts
useEffect(() => {
loadWorkouts();
}, [workoutHistoryService]);
// Pull to refresh handler // Pull to refresh handler
const onRefresh = React.useCallback(() => { const onRefresh = React.useCallback(() => {
setRefreshing(true); setRefreshing(true);
loadWorkouts(); refresh();
}, []); }, [refresh]);
// Load workout dates for selected month // Load workout dates for the selected month
useEffect(() => { useEffect(() => {
const getWorkoutDatesForMonth = async () => { const loadDatesForMonth = async () => {
try { try {
const year = selectedMonth.getFullYear(); const year = selectedMonth.getFullYear();
const month = selectedMonth.getMonth(); const month = selectedMonth.getMonth();
@ -116,28 +122,24 @@ export default function CalendarScreen() {
}) })
.map(workout => new Date(workout.startTime)); .map(workout => new Date(workout.startTime));
} else { } else {
// Try to use the service method if it exists and table exists // Use the service to get dates
try { dates = await workoutHistoryService.getWorkoutDatesInMonth(year, month);
if (typeof workoutHistoryService.getWorkoutDatesInMonth === 'function') {
dates = await workoutHistoryService.getWorkoutDatesInMonth(year, month); // Also check all workouts manually as a fallback
} else { const manualDates = allWorkouts
// Otherwise filter from loaded workouts .filter(workout => {
dates = workouts const date = new Date(workout.startTime);
.filter(workout => { return date.getFullYear() === year && date.getMonth() === month;
const date = new Date(workout.startTime); })
return date.getFullYear() === year && date.getMonth() === month; .map(workout => new Date(workout.startTime));
})
.map(workout => new Date(workout.startTime)); // Combine both sets of dates
} dates = [...dates, ...manualDates];
} catch (error) {
console.error('Error getting workout dates:', error);
// If table doesn't exist, use empty array
dates = [];
}
} }
// Convert to strings for the Set // Convert to strings for the Set
const dateStrings = dates.map(date => format(date, 'yyyy-MM-dd')); const dateStrings = dates.map(date => format(date, 'yyyy-MM-dd'));
console.log(`Found ${dateStrings.length} workout dates for ${year}-${month+1}:`, dateStrings);
setWorkoutDates(new Set(dateStrings)); setWorkoutDates(new Set(dateStrings));
} catch (error) { } catch (error) {
console.error('Error getting workout dates:', error); console.error('Error getting workout dates:', error);
@ -145,8 +147,8 @@ export default function CalendarScreen() {
} }
}; };
getWorkoutDatesForMonth(); loadDatesForMonth();
}, [selectedMonth, workouts, workoutHistoryService, useMockData]); }, [selectedMonth, workouts, allWorkouts, workoutHistoryService, useMockData]);
// Get dates for current month's calendar // Get dates for current month's calendar
const getDaysInMonth = (year: number, month: number) => { const getDaysInMonth = (year: number, month: number) => {
@ -188,7 +190,10 @@ export default function CalendarScreen() {
// Check if a date has workouts // Check if a date has workouts
const hasWorkout = (date: Date | null) => { const hasWorkout = (date: Date | null) => {
if (!date) return false; if (!date) return false;
return workoutDates.has(format(date, 'yyyy-MM-dd')); const dateString = format(date, 'yyyy-MM-dd');
const result = workoutDates.has(dateString);
console.log(`Checking if ${dateString} has workouts: ${result}`);
return result;
}; };
// Handle date selection in calendar // Handle date selection in calendar
@ -197,7 +202,7 @@ export default function CalendarScreen() {
setSelectedDate(date); setSelectedDate(date);
}; };
// Get workouts for selected date // Get workouts for the selected date
const [selectedDateWorkouts, setSelectedDateWorkouts] = useState<Workout[]>([]); const [selectedDateWorkouts, setSelectedDateWorkouts] = useState<Workout[]>([]);
const [loadingDateWorkouts, setLoadingDateWorkouts] = useState(false); const [loadingDateWorkouts, setLoadingDateWorkouts] = useState(false);
@ -205,30 +210,31 @@ export default function CalendarScreen() {
const loadWorkoutsForDate = async () => { const loadWorkoutsForDate = async () => {
try { try {
setLoadingDateWorkouts(true); setLoadingDateWorkouts(true);
console.log(`Loading workouts for date: ${format(selectedDate, 'yyyy-MM-dd')}`);
if (useMockData) { if (useMockData) {
// Use mock data filtering // Use mock data filtering
const filtered = workouts.filter(workout => const filtered = workouts.filter(workout =>
isSameDay(new Date(workout.startTime), selectedDate) isSameDay(new Date(workout.startTime), selectedDate)
); );
console.log(`Found ${filtered.length} mock workouts for selected date`);
setSelectedDateWorkouts(filtered); setSelectedDateWorkouts(filtered);
} else { } else {
try { // Use the hook's getWorkoutsByDate method
if (typeof workoutHistoryService.getWorkoutsByDate === 'function') { console.log('Calling getWorkoutsByDate...');
// Use the service method if available const dateWorkouts = await getWorkoutsByDate(selectedDate);
const dateWorkouts = await workoutHistoryService.getWorkoutsByDate(selectedDate); console.log(`getWorkoutsByDate returned ${dateWorkouts.length} workouts`);
setSelectedDateWorkouts(dateWorkouts);
} else { // If no workouts found, try filtering from all workouts as a fallback
// Fall back to filtering the already loaded workouts if (dateWorkouts.length === 0 && allWorkouts.length > 0) {
const filtered = workouts.filter(workout => console.log('No workouts found with getWorkoutsByDate, trying manual filtering');
isSameDay(new Date(workout.startTime), selectedDate) const filtered = allWorkouts.filter(workout =>
); isSameDay(new Date(workout.startTime), selectedDate)
setSelectedDateWorkouts(filtered); );
} console.log(`Found ${filtered.length} workouts by manual filtering`);
} catch (error) { setSelectedDateWorkouts(filtered);
// Handle the case where the workout table doesn't exist } else {
console.error('Error loading workouts for date:', error); setSelectedDateWorkouts(dateWorkouts);
setSelectedDateWorkouts([]);
} }
} }
} catch (error) { } catch (error) {
@ -240,7 +246,7 @@ export default function CalendarScreen() {
}; };
loadWorkoutsForDate(); loadWorkoutsForDate();
}, [selectedDate, workouts, workoutHistoryService, useMockData]); }, [selectedDate, workouts, allWorkouts, getWorkoutsByDate, useMockData]);
return ( return (
<View className="flex-1 bg-background"> <View className="flex-1 bg-background">
@ -262,7 +268,7 @@ export default function CalendarScreen() {
{/* Calendar section */} {/* Calendar section */}
<View className="mb-6"> <View className="mb-6">
<View className="flex-row items-center mb-4"> <View className="flex-row items-center mb-4">
<Calendar size={20} className="text-primary mr-2" /> <Calendar size={20} color={primaryColor} style={{ marginRight: 8 }} />
<Text className="text-lg font-semibold">Workout Calendar</Text> <Text className="text-lg font-semibold">Workout Calendar</Text>
</View> </View>
@ -274,7 +280,7 @@ export default function CalendarScreen() {
onPress={goToPreviousMonth} onPress={goToPreviousMonth}
className="p-2 rounded-full bg-muted" className="p-2 rounded-full bg-muted"
> >
<ChevronLeft size={20} className="text-foreground" /> <ChevronLeft size={20} color={mutedColor} />
</TouchableOpacity> </TouchableOpacity>
<Text className="text-foreground text-lg font-semibold"> <Text className="text-foreground text-lg font-semibold">
@ -285,7 +291,7 @@ export default function CalendarScreen() {
onPress={goToNextMonth} onPress={goToNextMonth}
className="p-2 rounded-full bg-muted" className="p-2 rounded-full bg-muted"
> >
<ChevronRight size={20} className="text-foreground" /> <ChevronRight size={20} color={mutedColor} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -311,18 +317,32 @@ export default function CalendarScreen() {
<View className="aspect-square items-center justify-center"> <View className="aspect-square items-center justify-center">
{date ? ( {date ? (
<View <View
className={cn( style={{
"w-8 h-8 rounded-full items-center justify-center", width: 40,
isSameDay(date, selectedDate) && !hasWorkout(date) && "bg-muted", height: 40,
hasWorkout(date) && "bg-primary", borderRadius: 20, // Make it a perfect circle
isSameDay(date, new Date()) && !hasWorkout(date) && !isSameDay(date, selectedDate) && "border border-primary" alignItems: 'center',
)} justifyContent: 'center',
backgroundColor: isSameDay(date, selectedDate)
? (hasWorkout(date) ? primaryColor : '#f3f4f6') // Selected date
: (hasWorkout(date) ? primaryBgColor : 'transparent'), // Date with workout
borderWidth: isSameDay(date, new Date()) && !hasWorkout(date) && !isSameDay(date, selectedDate) ? 2 : 0,
borderColor: primaryColor,
// Remove shadow effects that might be causing the weird shape
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0
}}
> >
<Text <Text
className={cn( style={{
"text-foreground", color: isSameDay(date, selectedDate) && hasWorkout(date)
hasWorkout(date) && "text-primary-foreground font-medium" ? '#ffffff' // White text for selected date with workout
)} : (hasWorkout(date) ? primaryColor : '#374151'), // Purple text for dates with workouts
fontWeight: hasWorkout(date) ? '600' : 'normal'
}}
> >
{date.getDate()} {date.getDate()}
</Text> </Text>