POWR/docs/features/profile/tabs/progress_tab.md
DocNR 969163313a fix(auth): Improve authentication state handling and avatar display
* Add style prop to UserAvatar component for better customization
* Refactor UserAvatar to use getAvatarSeed utility for consistent avatar generation
* Fix React hook ordering issues in profile/overview.tsx to prevent crashes during auth state changes
* Add proper state initialization and cleanup during authentication transitions
* Ensure consistent fallback avatar display for unauthenticated users

These changes improve stability during login/logout operations and provide better visual continuity with Robohash avatars when profile images aren't available.
2025-04-02 21:11:25 -04:00

9.7 KiB

Progress Tab

Last Updated: 2025-04-02
Status: Implemented
Related To: Profile Tab Overview, Activity Tab

Introduction

The Progress tab provides users with detailed analytics and visualizations of their workout progress over time. It offers period-based filtering, comprehensive workout statistics, and graphical representations of performance data. This tab serves as the central hub for tracking fitness improvements and identifying trends.

Features

Feature Status Notes
Period Filtering Implemented Filter analytics by week, month, year, or all time
Workout Summary Implemented Display key metrics like workout count, duration, and volume
Workout Frequency Chart Implemented Visualization of workout frequency by day of week
Exercise Distribution Chart Implemented Breakdown of exercise types and categories
Personal Records List Implemented Comprehensive list of personal records with comparisons
Nostr Integration Toggle Implemented Option to include or exclude Nostr workout data
Authentication Detection Implemented Login prompt for unauthenticated users

Implementation Details

The Progress tab is implemented in app/(tabs)/profile/progress.tsx. It integrates with the AnalyticsService to compute and display workout statistics and visualizations.

Period Selector Implementation

The tab includes a period selector to filter data by time range:

<View className="flex-row justify-center my-4">
  <Button 
    variant={period === 'week' ? 'purple' : 'outline'} 
    size="sm" 
    className="mx-1"
    onPress={() => setPeriod('week')}
  >
    <Text className={period === 'week' ? 'text-white' : 'text-foreground'}>Week</Text>
  </Button>
  <Button 
    variant={period === 'month' ? 'purple' : 'outline'} 
    size="sm" 
    className="mx-1"
    onPress={() => setPeriod('month')}
  >
    <Text className={period === 'month' ? 'text-white' : 'text-foreground'}>Month</Text>
  </Button>
  <Button 
    variant={period === 'year' ? 'purple' : 'outline'} 
    size="sm" 
    className="mx-1"
    onPress={() => setPeriod('year')}
  >
    <Text className={period === 'year' ? 'text-white' : 'text-foreground'}>Year</Text>
  </Button>
  <Button 
    variant={period === 'all' ? 'purple' : 'outline'} 
    size="sm" 
    className="mx-1"
    onPress={() => setPeriod('all')}
  >
    <Text className={period === 'all' ? 'text-white' : 'text-foreground'}>All</Text>
  </Button>
</View>

Nostr Integration Toggle

The Progress tab allows users to include or exclude Nostr workout data in their analytics:

{isAuthenticated && (
  <TouchableOpacity 
    onPress={() => setIncludeNostr(!includeNostr)}
    className="flex-row items-center"
  >
    <CloudIcon 
      size={16} 
      className={includeNostr ? "text-primary" : "text-muted-foreground"} 
    />
    <Text 
      className={`ml-1 text-sm ${includeNostr ? "text-primary" : "text-muted-foreground"}`}
    >
      Nostr
    </Text>
    <Switch 
      value={includeNostr} 
      onValueChange={setIncludeNostr}
      trackColor={{ false: '#767577', true: 'hsl(var(--purple))' }}
      thumbColor={'#f4f3f4'}
      className="ml-1"
    />
  </TouchableOpacity>
)}

Workout Frequency Chart Implementation

The tab visualizes workout frequency by day of the week:

// Workout frequency chart
const WorkoutFrequencyChart = () => {
  const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  
  return (
    <View className="h-40 bg-muted rounded-lg items-center justify-center">
      <Text className="text-muted-foreground">Workout Frequency Chart</Text>
      <View className="flex-row justify-evenly w-full mt-2">
        {stats?.frequencyByDay.map((count, index) => (
          <View key={index} className="items-center">
            <View 
              style={{ 
                height: count * 8, 
                width: 20, 
                backgroundColor: 'hsl(var(--purple))',
                borderRadius: 4
              }} 
            />
            <Text className="text-xs text-muted-foreground mt-1">{days[index]}</Text>
          </View>
        ))}
      </View>
    </View>
  );
};

Exercise Distribution Chart Implementation

The tab also displays a breakdown of exercise types:

// Exercise distribution chart
const ExerciseDistributionChart = () => {
  // Sample exercise names for demonstration
  const exerciseNames = [
    'Bench Press', 'Squat', 'Deadlift', 'Pull-up', 'Shoulder Press'
  ];
  
  // Convert exercise distribution to percentages
  const exerciseDistribution = stats?.exerciseDistribution || {};
  const total = Object.values(exerciseDistribution).reduce((sum, count) => sum + count, 0) || 1;
  const percentages = Object.entries(exerciseDistribution).reduce((acc, [id, count]) => {
    acc[id] = Math.round((count / total) * 100);
    return acc;
  }, {} as Record<string, number>);
  
  // Take top 5 exercises
  const topExercises = Object.entries(percentages)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 5);
  
  return (
    <View className="h-40 bg-muted rounded-lg items-center justify-center">
      <Text className="text-muted-foreground">Exercise Distribution</Text>
      <View className="flex-row justify-evenly w-full mt-2">
        {topExercises.map(([id, percentage], index) => (
          <View key={index} className="items-center mx-1">
            <View 
              style={{ 
                height: percentage * 1.5, 
                width: 20, 
                backgroundColor: `hsl(${index * 50}, 70%, 50%)`,
                borderRadius: 4
              }} 
            />
            <Text className="text-xs text-muted-foreground mt-1 text-center">
              {exerciseNames[index % exerciseNames.length].substring(0, 8)}
            </Text>
          </View>
        ))}
      </View>
    </View>
  );
};

Analytics Integration

The Progress tab integrates with the AnalyticsService to fetch workout statistics:

// Load workout statistics when period or includeNostr changes
useEffect(() => {
  async function loadStats() {
    if (!isAuthenticated) return;
    
    try {
      setLoading(true);
      
      // Pass includeNostr flag to analytics service
      analyticsService.setIncludeNostr(includeNostr);
      
      const workoutStats = await analytics.getWorkoutStats(period);
      setStats(workoutStats);
      
      // Load personal records
      const personalRecords = await analytics.getPersonalRecords(undefined, 5);
      setRecords(personalRecords);
    } catch (error) {
      console.error('Error loading analytics data:', error);
    } finally {
      setLoading(false);
    }
  }
  
  loadStats();
}, [isAuthenticated, period, includeNostr, analytics]);

Technical Considerations

Authentication Handling

Like other profile tabs, the Progress tab handles authentication with a consistent pattern:

if (!isAuthenticated) {
  return <NostrProfileLogin message="Login with your Nostr private key to view your progress." />;
}

Data Loading

The component manages loading states and provides appropriate UI feedback:

if (loading) {
  return (
    <View className="flex-1 items-center justify-center">
      <ActivityIndicator />
    </View>
  );
}

Nostr Integration Indicator

The tab visually indicates when Nostr data is included in the analytics:

{/* Nostr integration note */}
{isAuthenticated && includeNostr && (
  <Card className="mb-4 border-primary">
    <CardContent className="p-4 flex-row items-center">
      <CloudIcon size={16} className="text-primary mr-2" />
      <Text className="text-muted-foreground flex-1">
        Analytics include workouts from Nostr. Toggle the switch above to view only local workouts.
      </Text>
    </CardContent>
  </Card>
)}

Helper Functions

The tab includes helper functions such as formatDuration for consistent data presentation:

// Format duration in hours and minutes
function formatDuration(milliseconds: number): string {
  const hours = Math.floor(milliseconds / (1000 * 60 * 60));
  const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
  return `${hours}h ${minutes}m`;
}

User Experience Flow

  1. Authentication Check:

    • If user is not authenticated, display NostrProfileLogin component
    • If authenticated, proceed to load progress data
  2. Period Selection:

    • User selects desired time period for analysis (week, month, year, all)
    • Analytics are recalculated and displayed based on selection
  3. Nostr Toggle:

    • User can toggle inclusion of Nostr workout data in analytics
    • UI updates to indicate when Nostr data is included
  4. Data Display:

    • Show workout summary statistics
    • Display visualizations for workout frequency and exercise distribution
    • List personal records with previous values for comparison

Future Enhancements

  1. Enhanced Visualizations: More sophisticated chart types for deeper analysis
  2. Goal Setting: Allow users to set and track fitness goals
  3. Progress Trends: Show improvement trends over time for key metrics
  4. Body Measurements: Add tracking for weight, body measurements, and photos
  5. Export Functionality: Allow users to export analytics data
  6. Custom Time Periods: Enable custom date range selection