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.
This commit is contained in:
DocNR 2025-04-02 21:11:25 -04:00
parent 755e86e30b
commit 969163313a
37 changed files with 5806 additions and 626 deletions

View File

@ -6,6 +6,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Enhanced Avatar System with Robohash Integration
- Consolidated avatar implementation into ui/avatar.tsx component
- Added RobohashAvatar and RobohashFallback components
- Created utility functions in utils/avatar.ts for consistent avatar generation
- Implemented consistent avatar display using same seed (npub) across the app
- Ensured avatar consistency between profile, header, and settings drawer
- Enhanced user experience with friendly robot avatars for users without profile images
- Updated application components to use the new avatar system
- Robohash integration for profile pictures
- Added automatic robot avatar fallbacks when profile images don't load
- Implemented consistent avatar generation based on user IDs
- Added support for ephemeral keys and non-authenticated users
- Created test screen to demonstrate various avatar scenarios
- Enhanced UserAvatar component with Robohash integration
### Added
- Documentation structure overhaul for Profile feature
- Created comprehensive Profile Tab Overview documentation
- Added detailed documentation for each profile tab (Overview, Activity, Progress, Settings)
- Created authentication patterns documentation with hook ordering best practices
- Developed progress tracking implementation documentation
- Added follower statistics documentation with NostrBand integration details
- Created proper tab-level documentation for user profile information
- Built structure diagrams for the profile section architecture
- Updated documentation migration mapping
- Created proper cross-references between all profile documents
- External Signer Support for Android (NIP-55)
- Added Amber integration for secure private key management
- Created ExternalSignerUtils to detect external signer apps
@ -20,6 +45,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added customizable messaging for context-specific instructions
- Standardized button styling and text formatting
- Improved visual hierarchy with consistent spacing
- Documentation structure overhaul for Library feature
- Created new consolidated template organization documentation
- Enhanced library feature documentation with status indicators and implementation details
- Migrated design documents to new documentation structure
- Archived original documents with proper reference links
- Updated documentation migration mapping
- Fixed broken intra-documentation links
- Documentation structure overhaul for History feature
- Created comprehensive History tab overview documentation
- Added detailed History List View documentation with code examples
- Developed Calendar View documentation with implementation details
- Created detailed Migration Guide for History API
- Added proper archival references for legacy documents
- Updated documentation migration mapping
- Enhanced cross-referencing between History documents
### Fixed
- Profile tab login experience

View File

@ -44,11 +44,14 @@ export default function OverviewScreen() {
const router = useRouter();
const theme = useTheme() as CustomTheme;
const { currentUser, isAuthenticated } = useNDKCurrentUser();
// Initialize all state hooks at the top to maintain consistent ordering
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
// Initialize feed related state
const [feedItems, setFeedItems] = useState<any[]>([]);
const [feedLoading, setFeedLoading] = useState(false);
const [isOffline, setIsOffline] = useState(false);
const [entries, setEntries] = useState<AnyFeedEntry[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
// Only call useSocialFeed when authenticated to prevent the error
const socialFeed = isAuthenticated ? useSocialFeed({
@ -68,18 +71,31 @@ export default function OverviewScreen() {
if (isAuthenticated && socialFeed) {
setFeedItems(socialFeed.feedItems);
setIsOffline(socialFeed.isOffline);
} else {
// Clear feed items when logged out
setFeedItems([]);
}
}, [isAuthenticated, socialFeed?.feedItems, socialFeed?.isOffline]);
// Convert to the format expected by the component
const entries = React.useMemo(() => {
return feedItems.map(item => {
// Process feedItems into entries when feedItems changes
// This needs to be a separate effect to avoid breaking hook order during logout
useEffect(() => {
if (!feedItems || !Array.isArray(feedItems)) {
setEntries([]);
return;
}
// Map items and filter out any nulls
const mappedItems = feedItems.map(item => {
if (!item) return null;
// Create a properly typed AnyFeedEntry based on the item type
// with null safety for all item properties
const baseEntry = {
id: item.id,
eventId: item.id,
event: item.originalEvent,
timestamp: item.createdAt * 1000,
id: item.id || `temp-${Date.now()}-${Math.random()}`,
eventId: item.id || `temp-${Date.now()}-${Math.random()}`,
event: item.originalEvent || {},
timestamp: ((item.createdAt || Math.floor(Date.now() / 1000)) * 1000),
};
// Add type-specific properties
@ -88,35 +104,35 @@ export default function OverviewScreen() {
return {
...baseEntry,
type: 'workout',
content: item.parsedContent
content: item.parsedContent || {}
} as WorkoutFeedEntry;
case 'exercise':
return {
...baseEntry,
type: 'exercise',
content: item.parsedContent
content: item.parsedContent || {}
} as ExerciseFeedEntry;
case 'template':
return {
...baseEntry,
type: 'template',
content: item.parsedContent
content: item.parsedContent || {}
} as TemplateFeedEntry;
case 'social':
return {
...baseEntry,
type: 'social',
content: item.parsedContent
content: item.parsedContent || {}
} as SocialFeedEntry;
case 'article':
return {
...baseEntry,
type: 'article',
content: item.parsedContent
content: item.parsedContent || {}
} as ArticleFeedEntry;
default:
@ -124,17 +140,19 @@ export default function OverviewScreen() {
return {
...baseEntry,
type: 'social',
content: item.parsedContent
content: item.parsedContent || {}
} as SocialFeedEntry;
}
});
// Filter out nulls to satisfy TypeScript
const filteredEntries = mappedItems.filter((item): item is AnyFeedEntry => item !== null);
setEntries(filteredEntries);
}, [feedItems]);
const resetFeed = refresh;
const hasContent = entries.length > 0;
const [isRefreshing, setIsRefreshing] = useState(false);
// Profile data
const profileImageUrl = currentUser?.profile?.image ||
currentUser?.profile?.picture ||
@ -302,9 +320,9 @@ export default function OverviewScreen() {
<UserAvatar
size="xl"
uri={profileImageUrl}
fallback={displayName.charAt(0)}
pubkey={pubkey}
name={displayName}
className="mr-4 border-4 border-background"
isInteractive={false}
style={{ width: 90, height: 90 }}
/>

View File

@ -85,8 +85,8 @@ export default function Header({
<UserAvatar
size="sm"
uri={profileImageUrl}
pubkey={currentUser?.pubkey}
onPress={openDrawer}
fallback={fallbackLetter}
/>
{/* Middle - Title or Logo */}
@ -134,4 +134,4 @@ const styles = StyleSheet.create({
minWidth: 40,
alignItems: 'flex-end',
},
});
});

View File

@ -10,6 +10,7 @@ import {
Smartphone, Database, Zap, RefreshCw, AlertTriangle, Globe, PackageOpen, Trash2
} from 'lucide-react-native';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import UserAvatar from '@/components/UserAvatar';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Separator } from '@/components/ui/separator';
@ -304,23 +305,13 @@ export default function SettingsDrawer() {
onPress={handleProfileClick}
activeOpacity={0.7}
>
<Avatar
alt={currentUser?.profile?.name || "User profile"}
<UserAvatar
size="lg"
uri={currentUser?.profile?.image}
pubkey={currentUser?.pubkey}
name={currentUser?.profile?.name || 'Nostr User'}
className="h-16 w-16"
>
{isAuthenticated && currentUser?.profile?.image ? (
<AvatarImage source={{ uri: currentUser.profile.image }} />
) : null}
<AvatarFallback>
{isAuthenticated && currentUser?.profile?.name ? (
<Text className="text-foreground">
{currentUser.profile.name.charAt(0).toUpperCase()}
</Text>
) : (
<User size={28} />
)}
</AvatarFallback>
</Avatar>
/>
<View style={styles.profileInfo}>
<Text className="text-lg font-semibold">
{isAuthenticated ? currentUser?.profile?.name || 'Nostr User' : 'Not Logged In'}

View File

@ -1,156 +1,81 @@
// components/UserAvatar.tsx
import React, { useState, useEffect } from 'react';
import { TouchableOpacity, TouchableOpacityProps, GestureResponderEvent } from 'react-native';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { View, StyleSheet } from 'react-native';
import { RobohashAvatar } from '@/components/ui/avatar';
import { Text } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import { profileImageCache } from '@/lib/db/services/ProfileImageCache';
import { getRobohashUrl, getAvatarSeed } from '@/utils/avatar';
interface UserAvatarProps extends TouchableOpacityProps {
interface UserAvatarProps {
uri?: string;
pubkey?: string;
name?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
fallback?: string;
isInteractive?: boolean;
showName?: boolean;
className?: string;
style?: any;
onPress?: () => void;
}
const UserAvatar = ({
uri,
size = 'md',
fallback = 'U',
isInteractive = true,
/**
* Enhanced UserAvatar component with improved robustness for Nostr identities
* - Handles null/undefined pubkey and image gracefully
* - Uses consistent fallback seeds during login/logout state changes
* - Provides consistent avatar appearance across the app
*/
export default function UserAvatar({
uri,
pubkey,
name,
size = 'md',
showName = false,
className,
style,
onPress,
...props
}: UserAvatarProps) => {
const [imageError, setImageError] = useState(false);
const [imageUri, setImageUri] = useState<string | undefined>(uri);
const [retryCount, setRetryCount] = useState(0);
const maxRetries = 2; // Maximum number of retry attempts
}: UserAvatarProps) {
const [cachedImage, setCachedImage] = useState<string | null>(null);
// Load cached image when uri changes
// Attempt to load cached profile image if available
useEffect(() => {
let isMounted = true;
// Reset retry count and error state when URI changes
setRetryCount(0);
setImageError(false);
const loadCachedImage = async () => {
if (!uri) {
setImageUri(undefined);
return;
}
try {
// Try to extract pubkey from URI
const pubkey = profileImageCache.extractPubkeyFromUri(uri);
if (pubkey) {
// If we have a pubkey, try to get cached image
const cachedUri = await profileImageCache.getProfileImageUri(pubkey, uri);
if (isMounted) {
setImageUri(cachedUri);
setImageError(false);
if (!uri && pubkey) {
// Try to get cached image if URI is not provided
profileImageCache.getProfileImageUri(pubkey)
.then((cachedUri: string | undefined) => {
if (cachedUri) {
setCachedImage(cachedUri);
}
} else {
// If no pubkey, just use the original URI
if (isMounted) {
setImageUri(uri);
setImageError(false);
}
}
} catch (error) {
console.error('Error loading cached image:', error);
if (isMounted) {
setImageUri(uri);
setImageError(false);
}
}
};
loadCachedImage();
return () => {
isMounted = false;
};
}, [uri]);
const containerStyles = cn(
{
'w-8 h-8': size === 'sm',
'w-10 h-10': size === 'md',
'w-12 h-12': size === 'lg',
'w-24 h-24': size === 'xl',
},
className
);
const handlePress = (event: GestureResponderEvent) => {
if (onPress) {
onPress(event);
} else if (isInteractive) {
// Default behavior if no onPress provided
console.log('Avatar pressed');
})
.catch((error: Error) => {
console.error('Error getting cached profile image:', error);
});
}
};
}, [uri, pubkey]);
const handleImageError = () => {
console.error("Failed to load image from URI:", imageUri);
if (retryCount < maxRetries) {
// Try again after a short delay
console.log(`Retrying image load (attempt ${retryCount + 1}/${maxRetries})`);
setTimeout(() => {
setRetryCount(prev => prev + 1);
// Force reload by setting a new URI with cache buster
if (imageUri) {
const cacheBuster = `?retry=${Date.now()}`;
const newUri = imageUri.includes('?')
? `${imageUri}&cb=${Date.now()}`
: `${imageUri}${cacheBuster}`;
setImageUri(newUri);
setImageError(false);
}
}, 1000);
} else {
console.log(`Max retries (${maxRetries}) reached, showing fallback`);
setImageError(true);
}
};
// Get a consistent seed for Robohash using our utility function
const seed = React.useMemo(() => {
return getAvatarSeed(pubkey, name || 'anonymous-user');
}, [pubkey, name]);
const avatarContent = (
<Avatar
className={containerStyles}
alt="User profile image"
>
{imageUri && !imageError ? (
<AvatarImage
source={{ uri: imageUri }}
onError={handleImageError}
/>
) : (
<AvatarFallback>
<Text className="text-foreground">{fallback}</Text>
</AvatarFallback>
)}
</Avatar>
);
if (!isInteractive) return avatarContent;
// Use cached image if available, otherwise use provided URI
const imageUrl = uri || cachedImage || undefined;
return (
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel="User profile"
{...props}
>
{avatarContent}
</TouchableOpacity>
<View className={cn("items-center", className)}>
<RobohashAvatar
uri={imageUrl}
seed={seed}
size={size}
onPress={onPress}
isInteractive={Boolean(onPress)}
style={style}
/>
{showName && name && (
<Text className="text-sm mt-1 text-center">
{name}
</Text>
)}
</View>
);
};
export default UserAvatar;
}

View File

@ -4,7 +4,7 @@ import { View, TouchableOpacity, Image, ScrollView } from 'react-native';
import { Text } from '@/components/ui/text';
import { Badge } from '@/components/ui/badge';
import { Heart, MessageCircle, Repeat, Share, Clock, Dumbbell, CheckCircle, FileText, User } from 'lucide-react-native';
import UserAvatar from '@/components/UserAvatar';
import { RobohashAvatar } from '@/components/ui/avatar';
import { useProfile } from '@/lib/hooks/useProfile';
import { useNDK } from '@/lib/hooks/useNDK';
import { FeedItem } from '@/lib/hooks/useSocialFeed';
@ -74,11 +74,17 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
const { ndk } = useNDK();
const [liked, setLiked] = useState(false);
const [likeCount, setLikeCount] = useState(0);
const { profile } = useProfile(item.originalEvent.pubkey);
// Safe access to item properties with fallbacks
const pubkey = item?.originalEvent?.pubkey || '';
const itemId = item?.id || '';
// Use the safe pubkey value for profile lookup
const { profile } = useProfile(pubkey);
// Get likes count
useEffect(() => {
if (!ndk) return;
if (!ndk || !itemId) return;
let mounted = true;
@ -86,7 +92,7 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
try {
const filter = {
kinds: [7], // Reactions
'#e': [item.id]
'#e': [itemId]
};
const events = await ndk.fetchEvents(filter);
@ -103,7 +109,7 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
return () => {
mounted = false;
};
}, [ndk, item.id]);
}, [ndk, itemId]);
// Handle like button press
const handleLike = async () => {
@ -164,10 +170,10 @@ export default function EnhancedSocialPost({ item, onPress }: SocialPostProps) {
<TouchableOpacity activeOpacity={0.7} onPress={onPress}>
<View className="py-3 px-4">
<View className="flex-row">
<UserAvatar
<RobohashAvatar
uri={profile?.image}
size="md"
fallback={profile?.name?.[0] || 'U'}
seed={item.originalEvent.pubkey || (profile?.name?.[0] || 'U')}
className="mr-3"
/>

View File

@ -1,6 +1,9 @@
import * as AvatarPrimitive from '@rn-primitives/avatar';
import * as React from 'react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
import { Image, TouchableOpacity, TouchableOpacityProps } from 'react-native';
import { getRobohashUrl } from '@/utils/avatar';
const AvatarPrimitiveRoot = AvatarPrimitive.Root;
const AvatarPrimitiveImage = AvatarPrimitive.Image;
@ -42,4 +45,110 @@ const AvatarFallback = React.forwardRef<AvatarPrimitive.FallbackRef, AvatarPrimi
);
AvatarFallback.displayName = AvatarPrimitiveFallback.displayName;
export { Avatar, AvatarFallback, AvatarImage };
// RobohashFallback: A specialized fallback that uses Robohash for avatars
interface RobohashFallbackProps extends AvatarPrimitive.FallbackProps {
seed?: string;
size?: string;
}
const RobohashFallback = React.forwardRef<AvatarPrimitive.FallbackRef, RobohashFallbackProps>(
({ className, seed = 'anonymous', size = '150x150', ...props }, ref) => (
<AvatarPrimitiveFallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted p-0 overflow-hidden',
className
)}
{...props}
>
<Image
source={{ uri: getRobohashUrl(seed, 'anonymous', size) }}
accessibilityLabel="Robohash avatar"
style={{ width: '100%', height: '100%' }}
/>
</AvatarPrimitiveFallback>
)
);
RobohashFallback.displayName = 'RobohashFallback';
// RobohashAvatar: A complete avatar component with Robohash fallback
interface RobohashAvatarProps extends TouchableOpacityProps {
uri?: string;
seed?: string;
size?: 'sm' | 'md' | 'lg' | 'xl';
isInteractive?: boolean;
className?: string;
onImageError?: () => void;
}
const RobohashAvatar: React.FC<RobohashAvatarProps> = ({
uri,
seed,
size = 'md',
isInteractive = true,
className,
onPress,
onImageError,
...props
}) => {
const [imageError, setImageError] = useState(false);
// Ensure we always have a valid seed value
const safeSeed = React.useMemo(() => {
// If an explicit seed is provided, use it
if (seed) return seed;
// If no seed but we have a URI, use that as fallback
if (uri) return uri;
// Default fallback that will always work
return 'anonymous-user';
}, [seed, uri]);
const containerStyles = cn(
{
'w-8 h-8': size === 'sm',
'w-10 h-10': size === 'md',
'w-12 h-12': size === 'lg',
'w-24 h-24': size === 'xl',
},
className
);
const handleImageError = () => {
setImageError(true);
if (onImageError) {
onImageError();
}
};
const avatarContent = (
<Avatar
className={containerStyles}
alt="User avatar"
>
{uri && !imageError ? (
<AvatarImage
source={{ uri }}
onError={handleImageError}
/>
) : (
<RobohashFallback seed={safeSeed} />
)}
</Avatar>
);
if (!isInteractive) return avatarContent;
return (
<TouchableOpacity
onPress={onPress}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel="User avatar"
{...props}
>
{avatarContent}
</TouchableOpacity>
);
};
export { Avatar, AvatarFallback, AvatarImage, RobohashFallback, RobohashAvatar };

View File

@ -0,0 +1,202 @@
# ARCHIVED: Profile Tab Enhancement Design Document
**IMPORTANT**: This document has been archived. The updated documentation can be found at:
- [Profile Tab Overview](../features/profile/profile_overview.md)
- [Authentication Patterns](../features/profile/authentication_patterns.md)
- [Progress Tracking](../features/profile/progress_tracking.md)
- [Follower Statistics](../features/profile/follower_stats.md)
- Individual tab documentation in the [Profile section](../features/profile/tabs/)
---
**This document needs to be updated as we switched the order of the personal social feed (first) and the overview. We should also update this with the progress we made and some of the setbacks such as the react hooks consistency issues, user authentication state managment subscription management issues and component interdependencies**
# Profile Tab Enhancement Design Document
## Overview
The Profile Tab will be enhanced to include user analytics, progress tracking, and personal social feed. This document outlines the design and implementation plan for these enhancements.
## Goals
- Provide users with a comprehensive view of their workout progress and analytics
- Create a personalized social feed experience within the profile tab
- Improve user engagement by showcasing growth and achievements
- Integrate Nostr functionality for cross-device synchronization
## Tab Structure
The Profile Tab will be organized into the following sections:
1. **Overview** - User profile information and summary statistics
2. **Activity** - Personal social feed showing the user's workout posts
3. **Progress** - Analytics and progress tracking visualizations
4. **Settings** - Account settings, preferences, and Nostr integration
## Detailed Design
### Overview Section
The Overview section will serve as the landing page for the Profile Tab and will include:
- User profile photo, name, and bio
- Summary statistics:
- Total workouts completed
- Total volume lifted
- Workout streak
- Favorite exercises
- Quick access buttons to key features
- Nostr connection status
### Activity Section
The Activity section will display the user's personal social feed:
- Chronological list of the user's workout posts
- Ability to view, edit, and delete posts
- Interaction metrics (likes, comments)
- Options to share workouts to the global feed
- Filter options for viewing different types of activities
### Progress Section
The Progress section will provide detailed analytics and visualizations:
- **Workout Volume Chart**
- Weekly/monthly volume progression
- Filterable by exercise category or specific exercises
- **Strength Progress Tracking**
- Personal records for key exercises
- Progression charts for main lifts
- Comparison to previous periods
- **Workout Consistency**
- Calendar heatmap showing workout frequency
- Streak tracking and milestone celebrations
- Weekly workout distribution
- **Body Metrics** (future enhancement)
- Weight tracking
- Body measurements
- Progress photos
### Settings Section
The Settings section will include:
- Profile information management
- Nostr account connection and management
- Data synchronization preferences
- Privacy settings for social sharing **we need to be careful with this as posting on public nostr relays is not inherently private. solutions for the future would be to allow the user to post to a personal private relay, or encrypt data in the future**
- App preferences and customization
- Export and backup options
## Implementation Plan
### Phase 1: Core Structure
1. Create the tab navigation structure with the four main sections
2. Implement the Overview section with basic profile information
3. Set up the Settings section with account management
### Phase 2: Analytics and Progress
1. Implement data collection and processing for analytics
2. Create visualization components for progress tracking
3. Develop the Progress section with charts and metrics
4. Add personal records tracking and milestone celebrations
### Phase 3: Personal Social Feed
1. Implement the Activity section with the personal feed
2. Add post management functionality
3. Integrate with the global social feed
4. Implement interaction features
### Phase 4: Nostr Integration
1. Enhance Nostr connectivity for profile data
2. Implement cross-device synchronization for progress data
3. Add backup and restore functionality via Nostr
## Technical Considerations
### Data Storage
- Local SQLite database for workout and progress data
- Nostr for cross-device synchronization and backup
- Efficient querying for analytics calculations
### Performance
- Optimize chart rendering for smooth performance
- Implement pagination for social feed
- Use memoization for expensive calculations
### Privacy
- Clear user control over what data is shared
- Secure handling of personal information
- Transparent data synchronization options
## UI/UX Design
### Overview Section
```
+---------------------------------------+
| |
| [Profile Photo] Username |
| Bio |
| |
+---------------------------------------+
| |
| Total Workouts Total Volume |
| 123 45,678 lbs |
| |
| Current Streak Favorite Exercise |
| 7 days Bench Press |
| |
+---------------------------------------+
| |
| [Quick Actions] |
| |
+---------------------------------------+
```
### Progress Section
```
+---------------------------------------+
| |
| [Time Period Selector] |
| |
+---------------------------------------+
| |
| Volume Progression |
| |
| [Chart] |
| |
+---------------------------------------+
| |
| Strength Progress |
| |
| [Exercise Selector] |
| |
| [Progress Chart] |
| |
+---------------------------------------+
| |
| Workout Consistency |
| |
| [Calendar Heatmap] |
| |
+---------------------------------------+
```
## Conclusion
The enhanced Profile Tab will provide users with a comprehensive view of their fitness journey, combining social elements with detailed analytics and progress tracking. By centralizing these features in the Profile Tab, users will have a more cohesive experience that emphasizes personal growth and achievement.
The implementation will be phased to ensure each component is properly developed and integrated, with a focus on performance and user experience throughout the process.

View File

@ -0,0 +1,206 @@
> **ARCHIVED DOCUMENT**: This document is outdated and kept for historical reference only. Please refer to [Template Organization and Creation](../features/library/template_organization.md) for up-to-date information.
# Template Organization and Drag-and-Drop Feature
## Problem Statement
Users need a more flexible way to organize their growing collection of workout templates. Currently, templates are organized in a flat list with basic filtering. Users need the ability to create custom folders and easily reorganize templates through drag-and-drop interactions.
## Requirements
### Functional Requirements
- Create, rename, and delete folders
- Move templates between folders via drag-and-drop
- Reorder templates within folders
- Reorder folders themselves
- Collapse/expand folder views
- Support template search across folders
- Maintain existing category and favorite filters
- Support template color coding
- Batch move/delete operations
### Non-Functional Requirements
- Smooth drag animations (60fps)
- Persist folder structure locally
- Support offline operation
- Sync with Nostr when available
- Maintain current performance with 100+ templates
- Accessible drag-and-drop interactions
## Design Decisions
### 1. Folder Data Structure
Using a hierarchical structure with templates linked to folders:
```typescript
interface TemplateFolder {
id: string;
name: string;
color?: string;
icon?: string;
order: number;
created_at: number;
updated_at: number;
}
interface Template {
// ... existing fields ...
folder_id?: string; // Optional - null means root level
order: number; // Position within folder or root
}
```
Rationale:
- Simple but flexible structure
- Easy to query and update
- Supports future nested folders if needed
- Maintains compatibility with existing template structure
### 2. Drag-and-Drop Implementation
Using react-native-reanimated and react-native-gesture-handler:
Rationale:
- Native performance for animations
- Built-in gesture handling
- Good community support
- Cross-platform compatibility
- Rich animation capabilities
## Technical Design
### Core Components
```typescript
// Folder management
interface FolderManagerHook {
folders: TemplateFolder[];
createFolder: (name: string) => Promise<string>;
updateFolder: (id: string, data: Partial<TemplateFolder>) => Promise<void>;
deleteFolder: (id: string) => Promise<void>;
reorderFolder: (id: string, newOrder: number) => Promise<void>;
}
// Draggable template component
interface DraggableTemplateProps {
template: Template;
onDragStart?: () => void;
onDragEnd?: (dropZone: DropZone) => void;
isDragging?: boolean;
}
// Drop zone types
type DropZone = {
type: 'folder' | 'root' | 'template';
id: string;
position: 'before' | 'after' | 'inside';
}
```
### Integration Points
- SQLite database for local storage
- Nostr event kind for folder structure
- Template list screen
- Template filtering system
- Drag animation system
## Implementation Plan
### Phase 1: Foundation
1. Update database schema
2. Create folder management hooks
3. Implement basic folder CRUD operations
4. Add folder view to template screen
### Phase 2: Drag and Drop
1. Implement DraggableTemplateCard
2. Add drag gesture handling
3. Create drop zone detection
4. Implement reordering logic
### Phase 3: Enhancement
1. Add folder customization
2. Implement batch operations
3. Add Nostr sync support
4. Polish animations and feedback
## Testing Strategy
### Unit Tests
- Folder CRUD operations
- Template ordering logic
- Drop zone detection
- Data structure validation
### Integration Tests
- Drag and drop flows
- Folder persistence
- Search across folders
- Filter interactions
## Observability
### Logging
- Folder operations
- Drag and drop events
- Error conditions
- Performance metrics
### Metrics
- Folder usage statistics
- Common template organizations
- Operation success rates
- Animation performance
## Future Considerations
### Potential Enhancements
- Nested folders
- Folder sharing
- Template duplicating
- Advanced sorting options
- Folder templates
- Batch operations
- Grid view option
### Known Limitations
- Initial complexity increase
- Performance with very large template sets
- Cross-device sync challenges
- Platform-specific gesture differences
## Dependencies
### Runtime Dependencies
- react-native-reanimated ^3.0.0
- react-native-gesture-handler ^2.0.0
- SQLite storage
- NDK for Nostr sync
### Development Dependencies
- TypeScript
- Jest for testing
- React Native testing library
## Security Considerations
- Validate folder names
- Sanitize template data
- Secure local storage
- Safe Nostr event handling
## Rollout Strategy
### Development Phase
1. Implement core folder structure
2. Add basic drag-and-drop
3. Beta test with power users
4. Polish based on feedback
### Production Deployment
1. Feature flag for initial release
2. Gradual rollout to users
3. Monitor performance metrics
4. Collect user feedback
## References
- [React Native Reanimated Documentation](https://docs.swmansion.com/react-native-reanimated/)
- [React Native Gesture Handler Documentation](https://docs.swmansion.com/react-native-gesture-handler/)
- [SQLite Documentation](https://www.sqlite.org/docs.html)
- [Nostr NIP-01 Specification](https://github.com/nostr-protocol/nips/blob/master/01.md)

View File

@ -0,0 +1,190 @@
> **ARCHIVED DOCUMENT**: This document is outdated and kept for historical reference only. Please refer to [Migration Guide](../../features/history/migration_guide.md) for up-to-date information.
# Workout History API Migration Guide
## Overview
We've consolidated our workout history services and hooks to provide a more consistent and maintainable API. This guide will help you migrate from the old APIs to the new unified API.
## Why We're Consolidating
The previous implementation had several overlapping services:
1. **WorkoutHistoryService** - Basic service for local database operations
2. **EnhancedWorkoutHistoryService** - Extended service with advanced features
3. **NostrWorkoutHistoryService** - Service for Nostr integration
4. **useNostrWorkoutHistory Hook** - Hook for fetching workouts from Nostr
This led to:
- Duplicate code and functionality
- Inconsistent APIs
- Confusion about which service to use for what purpose
- Difficulty maintaining and extending the codebase
## New Architecture
The new architecture consists of:
1. **UnifiedWorkoutHistoryService** - A single service that combines all functionality
2. **useWorkoutHistory** - A single hook that provides access to all workout history features
## Service Migration
### Before:
```typescript
// Using WorkoutHistoryService
const workoutHistoryService = new WorkoutHistoryService(db);
const localWorkouts = await workoutHistoryService.getAllWorkouts();
// Using NostrWorkoutHistoryService
const nostrWorkoutHistoryService = new NostrWorkoutHistoryService(db);
const allWorkouts = await nostrWorkoutHistoryService.getAllWorkouts({
includeNostr: true,
isAuthenticated: true
});
```
### After:
```typescript
// Using unified UnifiedWorkoutHistoryService
const workoutHistoryService = new UnifiedWorkoutHistoryService(db);
// For local workouts only
const localWorkouts = await workoutHistoryService.getAllWorkouts({
includeNostr: false
});
// For all workouts (local + Nostr)
const allWorkouts = await workoutHistoryService.getAllWorkouts({
includeNostr: true,
isAuthenticated: true
});
```
## Hook Migration
### Before:
```typescript
// Using useNostrWorkoutHistory
const { workouts, loading } = useNostrWorkoutHistory();
// Or manually creating a service in a component
const db = useSQLiteContext();
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
// Then loading workouts
const loadWorkouts = async () => {
const workouts = await workoutHistoryService.getAllWorkouts();
setWorkouts(workouts);
};
```
### After:
```typescript
// Using unified useWorkoutHistory
const {
workouts,
loading,
refresh,
getWorkoutsByDate,
publishWorkoutToNostr
} = useWorkoutHistory({
includeNostr: true,
realtime: true,
filters: { type: ['strength'] } // Optional filters
});
```
## Key Benefits of the New API
1. **Simplified Interface**: One service and one hook to learn and use
2. **Real-time Updates**: Built-in support for real-time Nostr updates
3. **Consistent Filtering**: Unified filtering across local and Nostr workouts
4. **Better Type Safety**: Improved TypeScript types and interfaces
5. **Reduced Boilerplate**: Less code needed in components
## Examples of Updated Components
### Calendar View
```typescript
// Before
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
const loadWorkouts = async () => {
const allWorkouts = await workoutHistoryService.getAllWorkouts();
setWorkouts(allWorkouts);
};
// After
const {
workouts: allWorkouts,
loading,
refresh,
getWorkoutsByDate
} = useWorkoutHistory({
includeNostr: true,
realtime: true
});
```
### Workout History Screen
```typescript
// Before
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
const loadWorkouts = async () => {
let allWorkouts = [];
if (includeNostr) {
allWorkouts = await workoutHistoryService.getAllWorkouts();
} else {
allWorkouts = await workoutHistoryService.filterWorkouts({
source: ['local']
});
}
setWorkouts(allWorkouts);
};
// After
const {
workouts: allWorkouts,
loading,
refresh
} = useWorkoutHistory({
includeNostr,
filters: includeNostr ? undefined : { source: ['local'] },
realtime: true
});
```
### Workout Detail Screen
```typescript
// Before
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
const loadWorkout = async () => {
const workoutDetails = await workoutHistoryService.getWorkoutDetails(id);
setWorkout(workoutDetails);
};
// After
const { getWorkoutDetails, publishWorkoutToNostr } = useWorkoutHistory();
const loadWorkout = async () => {
const workoutDetails = await getWorkoutDetails(id);
setWorkout(workoutDetails);
};
```
## Timeline
- The old APIs are now deprecated and will be removed in a future release
- Please migrate to the new APIs as soon as possible
- If you encounter any issues during migration, please contact the development team
## Additional Resources
- [UnifiedWorkoutHistoryService Documentation](../api/UnifiedWorkoutHistoryService.md)
- [useWorkoutHistory Hook Documentation](../api/useWorkoutHistory.md)

View File

@ -1,3 +1,5 @@
> **ARCHIVED DOCUMENT**: This document is outdated and kept for historical reference only. Please refer to [Cache Management in POWR App](../technical/caching/cache_management.md) for up-to-date information.
# NDK Mobile Cache Integration Plan
## Overview

View File

@ -1,3 +1,5 @@
> **ARCHIVED DOCUMENT**: This document is outdated and kept for historical reference only. Please refer to [POWR App Coding Style Guide](../guides/coding_style.md) for up-to-date information.
## **Overview**
This guide is written in the spirit of [Google Style Guides](https://github.com/google/styleguide), especially the most well written ones like for [Obj-C](https://github.com/google/styleguide/blob/gh-pages/objcguide.md).
@ -274,4 +276,4 @@ expect(investment == endingInvestmentDollars)
```
**Rationale**: When all related things are collected in a single place, you can more clearly understand what you think you'll read. The rest is just checking for mechanics.
**Rationale**: When all related things are collected in a single place, you can more clearly understand what you think you'll read. The rest is just checking for mechanics.

View File

@ -1,3 +1,5 @@
> **ARCHIVED DOCUMENT**: This document is outdated and kept for historical reference only. Please refer to [Library Tab Overview](../features/library/library_overview.md) for up-to-date information.
# POWR Library Tab PRD
Updated: 2025-02-19
@ -183,4 +185,4 @@ Users need a centralized location to manage their fitness content (exercises and
5. No Nostr integration
## Conclusion
The Library tab has reached MVP status with core functionality implemented and working. While several planned features remain to be implemented, the current version provides the essential functionality needed to support workout creation and management. Recommendation is to proceed with Workout component development while maintaining a backlog of Library enhancements for future iterations.
The Library tab has reached MVP status with core functionality implemented and working. While several planned features remain to be implemented, the current version provides the essential functionality needed to support workout creation and management. Recommendation is to proceed with Workout component development while maintaining a backlog of Library enhancements for future iterations.

View File

@ -1,3 +1,5 @@
> **ARCHIVED DOCUMENT**: This document is outdated and kept for historical reference only. Please refer to [POWR App Styling Guide](../technical/styling/styling_guide.md) for up-to-date information.
# POWR App Styling Guide
This document outlines how to consistently style components in the POWR fitness app.
@ -131,4 +133,4 @@ Check that the icon has a size specified
If colors seem inconsistent:
Verify you're using Tailwind classes (text-primary vs #8B5CF6)
Check that the correct variant is being used for your component
Check that the correct variant is being used for your component

View File

@ -0,0 +1,297 @@
> **ARCHIVED DOCUMENT**: This document is outdated and kept for historical reference only. Please refer to [Template Organization and Creation](../features/library/template_organization.md) for up-to-date information.
# Workout Template Creation Design Document
## Problem Statement
Users need a guided, step-by-step interface to create strength workout templates. The system should focus on the core strength training features while maintaining an architecture that can be extended to support other workout types in the future.
## Requirements
### Functional Requirements (MVP)
- Create strength workout templates
- Configure basic template metadata
- Select exercises from library
- Configure sets/reps/rest for each exercise
- Preview and edit template before saving
- Save templates to local storage
### Non-Functional Requirements
- Sub-500ms response time for UI interactions
- Consistent shadcn/ui theming
- Mobile-first responsive design
- Offline-first data storage
- Error state handling
- Loading state management
## Design Decisions
### 1. Multi-Step Flow vs Single Form
Chosen: Multi-step wizard pattern
Rationale:
- Reduces cognitive load
- Clear progress indication
- Easier error handling
- Better mobile experience
- Simpler to extend for future workout types
### 2. Exercise Selection Interface
Chosen: Searchable list with quick-add functionality
Rationale:
- Faster template creation
- Reduces context switching
- Maintains flow state
- Supports future search/filter features
## Technical Design
### Flow Diagram
```mermaid
flowchart TD
A[FAB Click] --> B[Basic Info Sheet]
B --> C[Exercise Selection]
C --> D[Exercise Config]
D --> E[Template Review]
E --> F[Save Template]
C -.-> C1[Search Exercises]
C -.-> C2[Filter Exercises]
D -.-> D1[Configure Sets]
D -.-> D2[Configure Rest]
```
### Database Schema
```sql
-- Workout Templates (Nostr 33402 compatible)
CREATE TABLE templates (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
type TEXT NOT NULL DEFAULT 'strength' CHECK(type IN ('strength', 'circuit', 'emom', 'amrap')),
target_muscle_groups TEXT, -- JSON array
format_json TEXT, -- Template-wide parameters
format_units_json TEXT, -- Template-wide unit preferences
duration INTEGER, -- For future timed workouts (seconds)
rounds INTEGER, -- For future circuit/EMOM workouts
rest_between_rounds INTEGER, -- For future circuit workouts (seconds)
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
source TEXT NOT NULL DEFAULT 'local',
nostr_event_id TEXT, -- For future Nostr sync
tags TEXT, -- JSON array for categorization
FOREIGN KEY(nostr_event_id) REFERENCES nostr_events(id)
);
-- Template Exercises (Nostr exercise references)
CREATE TABLE template_exercises (
template_id TEXT NOT NULL,
exercise_id TEXT NOT NULL,
order_index INTEGER NOT NULL,
sets INTEGER NOT NULL,
reps INTEGER NOT NULL,
weight REAL, -- In kg, null for bodyweight
rpe INTEGER CHECK(rpe BETWEEN 1 AND 10),
set_type TEXT CHECK(set_type IN ('warmup', 'normal', 'drop', 'failure')),
rest_seconds INTEGER,
notes TEXT,
format_json TEXT, -- Exercise-specific parameters
format_units_json TEXT, -- Exercise-specific unit preferences
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE,
FOREIGN KEY(exercise_id) REFERENCES exercises(id)
);
```
### Core Types
```typescript
interface StrengthTemplate {
id: string;
title: string;
description?: string;
type: 'strength';
targetMuscleGroups: string[];
exercises: TemplateExercise[];
created_at: number;
updated_at: number;
source: 'local' | 'powr' | 'nostr';
format?: {
// Template-wide format settings
weight?: boolean;
reps?: boolean;
rpe?: boolean;
set_type?: boolean;
};
format_units?: {
weight: 'kg' | 'lbs';
reps: 'count';
rpe: '0-10';
set_type: 'warmup|normal|drop|failure';
};
}
interface TemplateExercise {
exerciseId: string;
orderIndex: number;
sets: number;
reps: number;
weight?: number;
rpe?: number;
setType: 'warmup' | 'normal' | 'drop' | 'failure';
restSeconds?: number;
notes?: string;
format?: {
// Exercise-specific format overrides
weight?: boolean;
reps?: boolean;
rpe?: boolean;
set_type?: boolean;
};
format_units?: {
weight?: 'kg' | 'lbs';
reps?: 'count';
rpe?: '0-10';
set_type?: 'warmup|normal|drop|failure';
};
}
```
### Component Specifications
#### 1. Template Creation Sheet
```typescript
interface TemplateCreationSheetProps {
isOpen: boolean;
onClose: () => void;
onComplete: (template: StrengthTemplate) => void;
}
```
#### 2. Basic Info Form
```typescript
interface BasicInfoForm {
title: string;
description?: string;
targetMuscleGroups: string[];
}
const TARGET_MUSCLE_GROUPS = [
'Full Body',
'Upper Body',
'Lower Body',
'Push',
'Pull',
'Legs'
];
```
#### 3. Exercise Configuration Form
```typescript
interface ExerciseConfigFormProps {
exercise: Exercise;
onConfigComplete: (config: TemplateExercise) => void;
}
```
#### 4. Template Preview
```typescript
interface TemplatePreviewProps {
template: StrengthTemplate;
onEdit: (exerciseId: string) => void;
onReorder: (newOrder: string[]) => void;
}
```
### UI States
#### Loading States
- Skeleton loaders for exercise list
- Disabled buttons during operations
- Progress indicators for saves
#### Error States
- Validation error messages
- Database operation errors
- Required field notifications
#### Success States
- Save confirmations
- Creation completion
- Navigation prompts
## Implementation Plan
### Phase 1: Core Template Creation
1. Database schema updates
- Add templates table
- Add template_exercises table
- Add indexes for querying
2. Basic template creation flow
- Template info form
- Exercise selection
- Basic set/rep configuration
- Template preview and save
### Phase 2: Enhanced Features
1. Exercise reordering
2. Rest period configuration
3. Template duplication
4. Template sharing
## Testing Strategy
### Unit Tests
- Form validation logic
- State management
- Data transformations
- Component rendering
### Integration Tests
- Full template creation flow
- Data persistence
- Error handling
- State management
### End-to-End Tests
- Complete template creation
- Exercise selection
- Template modification
- Database operations
## Future Considerations
### Planned Enhancements
- Circuit workout support
- EMOM workout support
- AMRAP workout support
- Complex progression schemes
- Template variations
### Potential Features
- Template sharing via Nostr
- Exercise substitutions
- Previous workout data import
- AI-assisted template creation
- Advanced progression schemes
### Technical Debt Prevention
- Maintain Nostr compatibility
- Keep workout type abstraction
- Plan for unit conversions
- Consider sync conflicts
- Design for offline-first
## Success Metrics
### Performance
- Template creation < 30 seconds
- Exercise addition < 2 seconds
- UI response < 100ms
- Save operation < 500ms
### User Experience
- Template reuse rate
- Exercise variation
- Completion rate
- Error rate

View File

@ -0,0 +1,197 @@
# Calendar View
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [History Overview](./history_overview.md), [Workout Data Models](../workout/data_models.md)
## Introduction
The Calendar View in the History tab provides users with a date-based visualization of their workout history. It displays a monthly calendar grid with highlights for days that have recorded workouts, making it easy to track workout consistency and patterns over time.
## Features
| Feature | Status | Notes |
|---------|--------|-------|
| Monthly Calendar | ✅ Implemented | Interactive monthly grid with navigation |
| Date Highlighting | ✅ Implemented | Visual indicators for dates with workouts |
| Date Selection | ✅ Implemented | Tap to view workouts for a specific date |
| Workout List | ✅ Implemented | Filtered workout list for selected date |
| Pull-to-refresh | ✅ Implemented | Updates calendar with latest workout data |
| Visual Styling | ✅ Implemented | Color-coded highlights based on workout status |
| Offline Support | ✅ Implemented | Works with locally cached workout data |
| Mock Data | ✅ Implemented | Example data shown when no workouts exist |
## Calendar Implementation
The Calendar View is implemented as a standalone tab within the History section, accessible through the Material Top Tab Navigator.
### Monthly Grid
The calendar displays a traditional monthly grid with:
- Week day headers (Mon-Sun)
- Date cells arranged in weeks
- Navigation controls for moving between months
- Visual indicators for:
- Current date
- Selected date
- Dates with workouts
### Date Highlighting Logic
Dates in the calendar are highlighted with different visual styles:
```typescript
// Date with workout + selected
{
backgroundColor: primaryColor, // Solid purple
color: '#ffffff' // White text
}
// Date with workout (not selected)
{
backgroundColor: primaryBgColor, // Semi-transparent purple
color: primaryColor // Purple text
}
// Current date (no workout)
{
borderWidth: 2,
borderColor: primaryColor,
backgroundColor: 'transparent'
}
// Selected date (no workout)
{
backgroundColor: '#f3f4f6', // Light gray
color: '#374151' // Dark text
}
// Regular date
{
backgroundColor: 'transparent',
color: '#374151'
}
```
### Workout Date Detection
The calendar uses a multi-layered approach to detect dates with workouts:
1. **Primary Method**: Using the `getWorkoutDatesInMonth` service method to efficiently query the database for all workout dates in the selected month
2. **Fallback Method**: Manual filtering of loaded workouts for the selected month as a backup
This dual approach ensures high reliability when finding workout dates, even if database queries are incomplete.
## Workouts by Date
When a date is selected in the calendar, the view displays a list of workouts for that day:
1. The date is stored in component state: `setSelectedDate(date)`
2. The `getWorkoutsByDate` method is called to fetch workouts for that date
3. A fallback filter is applied if needed: `allWorkouts.filter(workout => isSameDay(new Date(workout.startTime), selectedDate))`
4. Workouts are displayed using the `WorkoutCard` component with `showDate={false}` since the date context is already provided
## Technical Considerations
### Performance Optimization
The calendar implementation includes several performance optimizations:
- Memoization of expensive calculations
- Efficient date range queries using SQL date functions
- Intelligent caching of calendar date information
- Preloading of adjacent months' data for smoother navigation
### Offline Support
Calendar View maintains functionality during offline periods by:
- Storing workout dates locally in SQLite
- Using cached data for visualization
- Providing visual indicators when in offline mode
- Refreshing data automatically when connectivity is restored
## Usage Example
```typescript
// Calendar View core implementation
const CalendarScreen = () => {
const [selectedMonth, setSelectedMonth] = useState<Date>(new Date());
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const {
workouts: allWorkouts,
loading,
refresh,
getWorkoutsByDate,
service: workoutHistoryService
} = useWorkoutHistory({
includeNostr: includeNostr,
realtime: true
});
// Get dates that have workouts for visual highlighting
useEffect(() => {
const loadDatesForMonth = async () => {
const year = selectedMonth.getFullYear();
const month = selectedMonth.getMonth();
const dates = await workoutHistoryService.getWorkoutDatesInMonth(year, month);
const dateStrings = dates.map(date => format(date, 'yyyy-MM-dd'));
setWorkoutDates(new Set(dateStrings));
};
loadDatesForMonth();
}, [selectedMonth, workoutHistoryService]);
// Load workouts for the selected date
useEffect(() => {
const loadWorkoutsForDate = async () => {
const dateWorkouts = await getWorkoutsByDate(selectedDate);
setSelectedDateWorkouts(dateWorkouts);
};
loadWorkoutsForDate();
}, [selectedDate, getWorkoutsByDate]);
return (
// Calendar UI implementation
);
};
```
## Debugging
Common issues and troubleshooting tips for the Calendar View:
1. **Missing workout highlights**:
- Check SQL query execution with console logging
- Verify date formatting consistency (all dates should use the same format)
- Ensure workouts have valid startTime values
2. **Empty workout list for date with highlight**:
- Implement fallback filtering as described above
- Check date comparison logic (ensure proper timezone handling)
- Verify that workout fetching is using the correct date range (start/end of day)
3. **Visual inconsistencies**:
- Ensure calendar day components maintain a consistent size (use aspect-square)
- Apply proper border radius for circular date indicators (borderRadius: size / 2)
- Remove any shadow effects that might distort shapes
## Future Enhancements
Planned improvements for the Calendar View:
1. **Heatmap visualization**: Color intensity based on workout volume or intensity
2. **Streak indicator**: Visual display of consecutive workout days
3. **Week/Year views**: Additional time period visualizations
4. **Goal integration**: Show scheduled vs. completed workouts
5. **Export functionality**: Calendar data export options
6. **Swipe navigation**: Touch gestures for navigating between months
## Related Documentation
- [History Overview](./history_overview.md) - Overview of the History tab features and architecture
- [Workout Data Models](../workout/data_models.md) - Details on workout data structures
- [Migration Guide](./migration_guide.md) - Guide for migrating to the unified history service

View File

@ -0,0 +1,309 @@
# History List View
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [History Overview](./history_overview.md), [Calendar View](./calendar_view.md)
## Introduction
The History List View is the primary interface for browsing workout history in the POWR app. It presents a chronological feed of completed workouts grouped by month, with detailed cards showing workout information and exercise summaries.
## Features
| Feature | Status | Notes |
|---------|--------|-------|
| Chronological Feed | ✅ Implemented | Displays workouts in reverse chronological order |
| Monthly Grouping | ✅ Implemented | Organizes workouts by month for better context |
| Workout Source Filtering | ✅ Implemented | Toggle between all or local-only workouts |
| Nostr Authentication UI | ✅ Implemented | Prompt for unauthenticated users |
| Workout Cards | ✅ Implemented | Detailed workout summary cards |
| Pull-to-refresh | ✅ Implemented | Updates list with latest workout data |
| Visual Styling | ✅ Implemented | Consistent visual design with the app |
| Offline Support | ✅ Implemented | Works with locally cached workout data |
| Mock Data | ✅ Implemented | Example data shown when no workouts exist |
## Implementation Details
The History List View is implemented as a screen component in `app/(tabs)/history/workoutHistory.tsx`. It integrates with the unified workout history service through the `useWorkoutHistory` hook.
### Data Structure
Workouts are organized into a hierarchical structure:
1. **Monthly Groups**: Workouts are first grouped by month (e.g., "MARCH 2025")
2. **Workout Cards**: Each workout is displayed as a card within its month group
3. **Exercise Summaries**: Each card displays a summary of exercises performed
This organization provides users with temporal context while browsing their history.
### Source Filtering
For authenticated users, the History List View provides a toggle to filter workouts by source:
```typescript
// Source filtering UI
<View className="flex-row border border-border rounded-full overflow-hidden">
<Pressable
onPress={() => setIncludeNostr(true)}
style={{
backgroundColor: includeNostr ? primaryBgColor : 'transparent',
paddingHorizontal: 12,
paddingVertical: 6,
}}
>
<Text style={{
color: includeNostr ? primaryTextColor : mutedTextColor,
fontSize: 14,
fontWeight: includeNostr ? '600' : '400',
}}>
All Workouts
</Text>
</Pressable>
<Pressable
onPress={() => setIncludeNostr(false)}
style={{
backgroundColor: !includeNostr ? primaryBgColor : 'transparent',
paddingHorizontal: 12,
paddingVertical: 6,
}}
>
<Text style={{
color: !includeNostr ? primaryTextColor : mutedTextColor,
fontSize: 14,
fontWeight: !includeNostr ? '600' : '400',
}}>
Local Only
</Text>
</Pressable>
</View>
```
This filtering is implemented at the service level through the `useWorkoutHistory` hook:
```typescript
// Create memoized filters to prevent recreation on every render
const filters = React.useMemo(() => {
if (includeNostr) {
return undefined;
} else {
// Explicitly type the array to match WorkoutFilters interface
return { source: ['local' as const] };
}
}, [includeNostr]);
// Use the unified workout history hook with filters
const {
workouts: allWorkouts,
loading,
refreshing: hookRefreshing,
refresh,
error
} = useWorkoutHistory({
includeNostr,
filters,
realtime: true
});
```
### Month Grouping
Workouts are grouped by month using a helper function:
```typescript
// Group workouts by month
const groupWorkoutsByMonth = (workouts: Workout[]) => {
const grouped: Record<string, Workout[]> = {};
workouts.forEach(workout => {
const monthKey = format(workout.startTime, 'MMMM yyyy');
if (!grouped[monthKey]) {
grouped[monthKey] = [];
}
grouped[monthKey].push(workout);
});
return Object.entries(grouped);
};
```
The results are then rendered as section headers with workout cards:
```typescript
{groupedWorkouts.map(([month, monthWorkouts]) => (
<View key={month} className="mb-6">
<Text className="text-foreground text-xl font-semibold mb-4">
{month.toUpperCase()}
</Text>
{monthWorkouts.map((workout) => (
<WorkoutCard
key={workout.id}
workout={workout}
showDate={true}
showExercises={true}
/>
))}
</View>
))}
```
### Nostr Authentication Prompt
For unauthenticated users, the History List View displays a prompt to login with Nostr:
```typescript
{!isAuthenticated && (
<View className="mx-4 mt-4 p-4 bg-primary/5 rounded-lg border border-primary/20">
<Text className="text-foreground font-medium mb-2">
Connect with Nostr
</Text>
<Text className="text-muted-foreground mb-4">
Login with Nostr to see your workouts from other devices and back up your workout history.
</Text>
<Button
variant="purple"
onPress={() => setIsLoginSheetOpen(true)}
className="w-full"
>
<Text className="text-white">Login with Nostr</Text>
</Button>
</View>
)}
```
## Mock Data Integration
When no workouts are available (typically for new users), the History List View provides example data to demonstrate the interface:
```typescript
// Check if we need to use mock data (empty workouts)
if (allWorkouts.length === 0 && !error) {
console.log('No workouts found, using mock data');
setWorkouts(mockWorkouts);
setUseMockData(true);
} else {
setWorkouts(allWorkouts);
setUseMockData(false);
}
// And show a message when using mock data
{useMockData && (
<View className="bg-primary/5 rounded-lg p-4 mb-4 border border-border">
<Text className="text-muted-foreground text-sm">
Showing example data. Your completed workouts will appear here.
</Text>
</View>
)}
```
## Technical Considerations
### Performance Optimization
The History List View includes several optimizations:
- Memoization of filter objects to prevent unnecessary re-renders
- Careful state updates to avoid cascading re-renders
- Efficient grouping algorithm for workout organization
- Virtualized lists for large workout histories
### Error Handling
Robust error handling ensures a smooth user experience:
- Empty state handling for no workouts
- Loading indicators during data fetching
- Error recovery with fallback to local data
- Clear feedback when using mock data
## Interaction with Calendar View
The History List View works in conjunction with the Calendar View through the Material Top Tab Navigator:
```typescript
const Tab = createMaterialTopTabNavigator();
export default function HistoryLayout() {
return (
<TabScreen>
<Header useLogo={true} showNotifications={true} />
<Tab.Navigator
initialRouteName="workouts"
screenOptions={{...}}
>
<Tab.Screen
name="workouts"
component={HistoryScreen}
options={{ title: 'History' }}
/>
<Tab.Screen
name="calendar"
component={CalendarScreen}
options={{ title: 'Calendar' }}
/>
</Tab.Navigator>
</TabScreen>
);
}
```
This integration provides users with multiple ways to view their workout history based on their preference.
## Usage Example
```typescript
// History List core implementation
const HistoryScreen = () => {
const [workouts, setWorkouts] = useState<Workout[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [useMockData, setUseMockData] = useState(false);
const [includeNostr, setIncludeNostr] = useState(true);
// Use the unified workout history hook
const {
workouts: allWorkouts,
loading,
refresh
} = useWorkoutHistory({
includeNostr,
filters: includeNostr ? undefined : { source: ['local'] },
realtime: true
});
// Group workouts by month
const groupedWorkouts = groupWorkoutsByMonth(workouts);
return (
<ScrollView
className="flex-1"
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={refresh} />
}
>
{/* Authentication UI */}
{/* Source filtering */}
{/* Grouped workout list */}
</ScrollView>
);
};
```
## Future Enhancements
Planned improvements for the History List View:
1. **Advanced Search**: Full-text search across workout titles, exercises, and notes
2. **Additional Filtering**: More granular filtering by exercise type, duration, and intensity
3. **Batch Operations**: Multi-select and bulk actions for workouts
4. **Expanded Exercise Details**: More detailed exercise information in workout cards
5. **Social Sharing**: Direct sharing of workouts to social media or messaging apps
6. **Stats Summary**: Monthly workout statistics summary at the top of each month group
## Related Documentation
- [History Overview](./history_overview.md) - Overview of the History tab features and architecture
- [Calendar View](./calendar_view.md) - Details on the calendar visualization
- [Workout Cards](../workout/ui_components.md) - Information on workout card components
- [Migration Guide](./migration_guide.md) - Guide for migrating to the unified history service

View File

@ -0,0 +1,178 @@
# Workout History Overview
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Workout Data Models](../workout/data_models.md), [Nostr Integration](../../technical/nostr/offline_queue.md)
## Introduction
The History tab provides a comprehensive view of a user's workout history, allowing them to track, review, and analyze their past workouts. It offers both chronological and calendar views, with filtering and search capabilities to help users quickly find specific workout information.
## Features
| Feature | Status | Notes |
|---------|--------|-------|
| Chronological History List | ✅ Implemented | Groups workouts by month |
| Calendar View | ✅ Implemented | Highlights dates with workouts |
| Nostr Integration | ✅ Implemented | Two-way synchronization of workout data |
| Workout Source Filtering | ✅ Implemented | Toggle between all or local-only workouts |
| Workout Detail View | ✅ Implemented | Shows complete exercise and set information |
| Pull-to-refresh | ✅ Implemented | Loads latest workouts |
| Date-based Workout Filtering | ✅ Implemented | Shows workouts for selected calendar date |
| Advanced Search | ⏳ Planned | Will search by exercise, notes, and other fields |
| Export Functionality | 🧪 Implemented (Backend) | API ready but not exposed in UI |
| Streak Tracking | 🧪 Implemented (Backend) | Service implemented but not in UI |
## Architecture
The History tab is built on a unified history service architecture that provides a consistent interface for accessing workout data from both local storage and the Nostr network.
### Components
1. **UnifiedWorkoutHistoryService**: Core service that handles all workout history operations
2. **useWorkoutHistory**: React hook that provides access to workout history functionality
3. **WorkoutCard**: Reusable component for displaying workout summary information
4. **WorkoutDetailView**: Component for displaying detailed workout information
### Data Flow
```mermaid
graph TD
A[User] -->|Views History| B[History Tab]
B -->|Uses| C[useWorkoutHistory Hook]
C -->|Calls| D[UnifiedWorkoutHistoryService]
D -->|Reads from| E[SQLite Database]
D -->|Publishes to| F[Nostr Network]
F -->|Receives from| D
D -->|Parses| G[Workout Models]
G -->|Displayed in| B
```
## Screens
### History Screen
The History screen displays workouts in a chronological list, grouped by month. It provides filtering options and integration with Nostr.
Key features:
- Monthly grouping for easy navigation
- Clear workout cards with exercise summaries
- Workout source filtering (All/Local Only)
- Nostr authentication prompt for unauthenticated users
- Pull-to-refresh functionality
- Visual indicators for workout source
### Calendar Screen
The Calendar screen provides a monthly calendar view with highlights for days that have workouts. Selecting a date shows the workouts for that day.
Key features:
- Monthly calendar navigation
- Visual indicators for days with workouts
- Selected date highlighting
- Detailed workout list for selected date
- Pull-to-refresh functionality
- Fallback filtering for dates without direct database matches
## Implementation Details
### Unified Service
The UnifiedWorkoutHistoryService combines functionality previously spread across multiple services into a single, comprehensive API:
- Basic CRUD operations for workout history
- Filtering and search capabilities
- Calendar and date-based queries
- Nostr integration for cross-device synchronization
- Export functionality
- Analytics and streak tracking
### Authentication State Handling
The History tab adapts to the user's authentication state, showing appropriate UI:
- **Unauthenticated**: Shows local workouts only with a login prompt
- **Authenticated**: Shows all workouts with filtering options
### Nostr Integration
Workout history integrates with Nostr for cross-device synchronization:
- Two-way synchronization of workout data
- Real-time subscription for workout updates
- Visual indicators for workout source (local/Nostr)
- Publishing capabilities for local workouts
- Import functionality for Nostr-only workouts
### Offline Support
The History tab provides robust offline support:
- Local storage of workout data
- Offline viewing of previously loaded workouts
- Background synchronization when connectivity is restored
- Visual indicators for offline state
## User Experience
### Visual Design
The History tab uses consistent visual design elements to enhance usability:
- **Color Coding**: Purple for Nostr-related elements
- **Card Layout**: Clear, consistent workout cards with exercise summaries
- **Typography**: Hierarchical text styles for improved readability
- **Iconography**: Consistent icons for actions and status indicators
### Navigation Flow
```mermaid
graph TD
A[History Tab] -->|Tab Navigation| B[History Screen]
A -->|Tab Navigation| C[Calendar Screen]
B -->|Tap Workout| D[Workout Detail Screen]
C -->|Select Date| E[Date Workouts]
E -->|Tap Workout| D
D -->|Tap Publish| F[Publish to Nostr]
```
## Future Enhancements
The following enhancements are planned for future releases:
1. **Advanced Search**: Full-text search across workout titles, exercises, and notes
2. **UI for Export Functionality**: User interface for exporting workout data in CSV or JSON formats
3. **Streak Visualization**: Visual representation of workout streaks
4. **Enhanced Filtering**: More granular filtering by exercise type, duration, and intensity
5. **Analytics Integration**: Better integration with the Profile tab's analytics features
6. **Batch Operations**: Ability to perform operations on multiple workouts at once
7. **Advanced Calendar View**: Heatmap visualization of workout intensity on the calendar
## Technical Considerations
### Performance Optimization
The History tab implements several performance optimizations:
- Lazy loading of workouts for better initial load times
- Optimistic UI updates for better user experience
- Efficient SQL queries with proper indexing
- Memoization of expensive calculations
- Background data loading for calendar view
### Error Handling
Robust error handling ensures a smooth user experience:
- Fallback mechanisms for database query failures
- Graceful degradation during network issues
- Appropriate error messaging
- Retry mechanisms for failed network operations
- Data validation before storage
## Related Documentation
- [Migration Guide](./migration_guide.md) - Guide for migrating from legacy workout history services
- [Workout Data Models](../workout/data_models.md) - Details on workout data structures
- [Nostr Offline Queue](../../technical/nostr/offline_queue.md) - Information on offline Nostr functionality
- [Profile Progress Tracking](../profile/progress_tracking.md) - Related analytics features

View File

@ -1,129 +1,51 @@
# Workout History Features
# History Tab Documentation
**Last Updated:** 2025-03-25
**Last Updated:** 2025-04-02
**Status:** Active
**Related To:** Workout Features, Nostr Integration
**Related To:** [Workout Features](../workout/index.md), [Nostr Integration](../../technical/nostr/index.md)
## Purpose
## Introduction
This document describes the workout history features in the POWR app, focusing on the MVP implementation. It covers history views, data sources, and sharing capabilities.
The History tab provides users with comprehensive views of their workout history, allowing them to track, review, and analyze past workouts. This section documents the History tab features, architecture, and integration with the rest of the POWR app.
## Overview
## Key Documentation
The workout history feature allows users to:
| Document | Description |
|----------|-------------|
| [History Overview](./history_overview.md) | Detailed overview of the History tab features, architecture, and implementation |
| [History List View](./history_list.md) | Documentation of the chronological workout history list |
| [Calendar View](./calendar_view.md) | Documentation of the calendar-based workout visualization |
| [Migration Guide](./migration_guide.md) | Guide for migrating from legacy workout history services to the unified architecture |
1. View their past workout completions in chronological order
2. Review detailed workout data for completed sessions
3. Share workouts from history to Nostr
4. Access both local and Nostr-published workout records
## Feature Summary
The history implementation is designed with the following principles:
The History tab includes the following major features:
- Unified view of workouts from multiple sources
- Simple chronological listing
- Calendar-based frequency view
- Offline-first with sync capabilities
- Integration with social sharing
- **Chronological History View** - Displays workouts grouped by month with detailed exercise information
- **Calendar View** - Provides a calendar interface with workout date highlighting and filtering
- **Unified Data Access** - Combines local and Nostr-based workout data through a single API
- **Offline Support** - Ensures workout history is available even without network access
- **Filtering and Search** - Helps users quickly find specific workout information
- **Nostr Integration** - Enables cross-device synchronization of workout history
## Component Architecture
## Architecture
### High-Level Components
The History tab is built on a unified service architecture that provides consistent access to workout data regardless of source:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ UI Layer │ │ Service Layer │ │ Data Layer │
│ │ │ │ │ │
│ History List │ │ History Service │ │ SQLite Storage │
│ Calendar View │◄───►│ Sharing Service │◄───►│ Nostr Events │
│ Detail Views │ │ Migration │ │ Caching │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
- **UnifiedWorkoutHistoryService** - Core service for all workout history operations
- **useWorkoutHistory** - React hook for accessing workout history in components
- **WorkoutCard** - Reusable component for displaying workout summaries
- **Calendar** - Date-based view with workout visualization
### UI Components
## Technical Integration
- `WorkoutHistoryList`: Primary chronological list of completed workouts
- `CalendarView`: Basic calendar visualization of workout frequency
- `WorkoutDetailView`: Expanded view of a single workout record
- `WorkoutShareButton`: Allows sharing of completed workouts to Nostr
The History tab integrates with several other app features:
### Service Layer
- **Workout Completion** - Completed workouts are stored and displayed in History
- **Nostr Protocol** - Two-way synchronization of workout data between devices
- **Profile Analytics** - Workout data feeds into profile progress tracking
- **Offline Queue** - Ensures data persistence during connectivity issues
- `UnifiedWorkoutHistoryService`: Combined access to multiple history sources
- `EnhancedWorkoutHistoryService`: Local workout history management
- `NostrWorkoutHistoryService`: Remote workout history via Nostr
- `PublicationQueueService`: Queue for publishing workout records to Nostr
## Implementation Status
### Data Layer
- `SQLite`: Local storage for workout history
- `EventCache`: Cached Nostr events for workout records
## Implementation Details
### Data Sources
The workout history system supports multiple data sources:
1. **Local SQLite Database**
- Primary storage for all workout records
- Complete history regardless of connectivity
- Stores workout details including exercises, sets, weights, etc.
2. **Nostr Events (kind 1301)**
- Published workout records from the user
- Records from template authors for attribution
- Enables social sharing and discovery
### History Views
#### Chronological View
- Lists workouts by date, newest first
- Displays workout name, date, and basic summary
- Links to detailed workout view
- Includes sharing capability to publish to Nostr
#### Calendar View
- Basic monthly calendar visualization
- Shows dates with completed workouts
- Simple tap to see workouts on a specific date
#### Workout Details
- Complete set/rep/weight data
- Comparison to template (if used)
- Notes from workout
- Option to share to Nostr
### Sharing to Nostr
The history view includes the ability to share workouts to Nostr:
1. User selects "Share" on any workout in history
2. User can add a note/caption
3. System publishes a kind 1301 workout record event
4. Optionally publishes a kind 1 note quoting the workout record
This aligns with the MVP sharing functionality in the workout completion flow but extends it to previously completed workouts.
### MVP Implementation Focus
For the MVP release, the history tab focuses on:
1. **Basic Workout History Display**
- Chronological list of workouts
- Simple calendar view
- Detailed workout view
2. **Workout Sharing**
- Publishing kind 1301 workout records from history
- Enabling sharing of previously completed workouts
3. **Unified History Sources**
- Showing both local and Nostr-published workouts
- Basic deduplication of records from multiple sources
## Related Documentation
- [Workout Completion Flow](../workout/completion_flow.md) - How workouts are recorded
- [Nostr Exercise NIP](../../technical/nostr/exercise_nip.md) - Protocol for workout data
- [MVP and Targeted Rebuild](../../project/mvp_and_rebuild.md) - Overall MVP strategy
- [Progress Tracking](../../features/profile/progress_tracking.md) - How progress is tracked in the profile tab
The History tab is fully implemented with all core features available, and additional enhancements planned for future releases. See the [History Overview](./history_overview.md) document for detailed feature status.

View File

@ -0,0 +1,329 @@
# Workout History API Migration Guide
**Last Updated:** 2025-04-02
**Status:** Active
**Related To:** [History Overview](./history_overview.md), [UnifiedWorkoutHistoryService](../../lib/db/services/UnifiedWorkoutHistoryService.ts)
## Overview
We've consolidated our workout history services and hooks to provide a more consistent and maintainable API. This guide helps developers migrate from the legacy APIs to the new unified API.
## Why We Consolidated
The previous implementation had several overlapping services:
1. **WorkoutHistoryService** - Basic service for local database operations
2. **EnhancedWorkoutHistoryService** - Extended service with advanced features
3. **NostrWorkoutHistoryService** - Service for Nostr integration
4. **useNostrWorkoutHistory Hook** - Hook for fetching workouts from Nostr
This led to:
- Duplicate code and functionality
- Inconsistent APIs
- Confusion about which service to use for what purpose
- Difficulty maintaining and extending the codebase
## New Architecture
The new architecture consists of two primary components:
1. **UnifiedWorkoutHistoryService** - A comprehensive service that combines all functionality from previous services
2. **useWorkoutHistory** - A unified hook that provides access to all workout history features
## Feature Comparison
| Feature | Legacy Implementation | Unified Implementation |
|---------|--------------------------|------------------------|
| Local Workouts | WorkoutHistoryService | UnifiedWorkoutHistoryService |
| Nostr Workouts | NostrWorkoutHistoryService | UnifiedWorkoutHistoryService |
| Filtering | EnhancedWorkoutHistoryService | UnifiedWorkoutHistoryService |
| Calendar View | Separate implementation | Integrated in service |
| Real-time Updates | Not available | Built-in with subscriptions |
| Export Functionality | Not available | Built-in (CSV/JSON) |
| Streak Tracking | Not available | Built-in analytics |
## Service Migration
### Before:
```typescript
// Using WorkoutHistoryService
const workoutHistoryService = new WorkoutHistoryService(db);
const localWorkouts = await workoutHistoryService.getAllWorkouts();
// Using NostrWorkoutHistoryService
const nostrWorkoutHistoryService = new NostrWorkoutHistoryService(db);
const allWorkouts = await nostrWorkoutHistoryService.getAllWorkouts({
includeNostr: true,
isAuthenticated: true
});
```
### After:
```typescript
// Using unified UnifiedWorkoutHistoryService
const workoutHistoryService = new UnifiedWorkoutHistoryService(db);
// For local workouts only
const localWorkouts = await workoutHistoryService.getAllWorkouts({
includeNostr: false
});
// For all workouts (local + Nostr)
const allWorkouts = await workoutHistoryService.getAllWorkouts({
includeNostr: true,
isAuthenticated: true
});
```
## Hook Migration
### Before:
```typescript
// Using useNostrWorkoutHistory
const { workouts, loading } = useNostrWorkoutHistory();
// Or manually creating a service in a component
const db = useSQLiteContext();
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
// Then loading workouts
const loadWorkouts = async () => {
const workouts = await workoutHistoryService.getAllWorkouts();
setWorkouts(workouts);
};
```
### After:
```typescript
// Using unified useWorkoutHistory
const {
workouts,
loading,
refresh,
getWorkoutsByDate,
publishWorkoutToNostr
} = useWorkoutHistory({
includeNostr: true,
realtime: true,
filters: { type: ['strength'] } // Optional filters
});
```
## API Reference
### UnifiedWorkoutHistoryService
```typescript
export class UnifiedWorkoutHistoryService {
constructor(database: SQLiteDatabase);
// Core operations
getAllWorkouts(options?: {
limit?: number;
offset?: number;
includeNostr?: boolean;
isAuthenticated?: boolean;
}): Promise<Workout[]>;
getWorkoutDetails(workoutId: string): Promise<Workout | null>;
// Filtering and Search
filterWorkouts(filters: WorkoutFilters): Promise<Workout[]>;
searchWorkouts(query: string): Promise<Workout[]>;
getWorkoutsByExercise(exerciseId: string): Promise<Workout[]>;
// Calendar operations
getWorkoutsByDate(date: Date): Promise<Workout[]>;
getWorkoutDatesInMonth(year: number, month: number): Promise<Date[]>;
// Nostr operations
publishWorkoutToNostr(workoutId: string): Promise<string>;
importNostrWorkoutToLocal(eventId: string): Promise<string>;
subscribeToNostrWorkouts(pubkey: string, callback: (workout: Workout) => void): string;
unsubscribeFromNostrWorkouts(subId: string): void;
// Analytics
getWorkoutStreak(): Promise<{ current: number; longest: number; lastWorkoutDate: Date | null }>;
getWorkoutCount(): Promise<number>;
// Export
exportWorkoutHistory(format: 'csv' | 'json'): Promise<string>;
// Status
getWorkoutSyncStatus(workoutId: string): Promise<WorkoutSyncStatus>;
updateWorkoutNostrStatus(workoutId: string, eventId: string, relayCount: number): Promise<boolean>;
}
```
### useWorkoutHistory
```typescript
export function useWorkoutHistory(options: {
includeNostr?: boolean;
filters?: WorkoutFilters;
realtime?: boolean;
} = {}): {
workouts: Workout[];
loading: boolean;
error: Error | null;
refreshing: boolean;
refresh: () => Promise<Workout[]>;
getWorkoutsByDate: (date: Date) => Promise<Workout[]>;
getWorkoutDetails: (workoutId: string) => Promise<Workout | null>;
publishWorkoutToNostr: (workoutId: string) => Promise<string>;
importNostrWorkoutToLocal: (eventId: string) => Promise<string>;
service: UnifiedWorkoutHistoryService;
}
```
## Migration Examples
### Calendar View
```typescript
// Before
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
const loadWorkouts = async () => {
const allWorkouts = await workoutHistoryService.getAllWorkouts();
setWorkouts(allWorkouts);
};
// After
const {
workouts: allWorkouts,
loading,
refresh,
getWorkoutsByDate
} = useWorkoutHistory({
includeNostr: true,
realtime: true
});
```
### Workout History Screen
```typescript
// Before
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
const loadWorkouts = async () => {
let allWorkouts = [];
if (includeNostr) {
allWorkouts = await workoutHistoryService.getAllWorkouts();
} else {
allWorkouts = await workoutHistoryService.filterWorkouts({
source: ['local']
});
}
setWorkouts(allWorkouts);
};
// After
const {
workouts: allWorkouts,
loading,
refresh
} = useWorkoutHistory({
includeNostr,
filters: includeNostr ? undefined : { source: ['local'] },
realtime: true
});
```
### Workout Detail Screen
```typescript
// Before
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
const loadWorkout = async () => {
const workoutDetails = await workoutHistoryService.getWorkoutDetails(id);
setWorkout(workoutDetails);
};
// After
const { getWorkoutDetails, publishWorkoutToNostr } = useWorkoutHistory();
const loadWorkout = async () => {
const workoutDetails = await getWorkoutDetails(id);
setWorkout(workoutDetails);
};
```
## Key Benefits of the New API
1. **Simplified Interface**: One service and one hook to learn and use
2. **Real-time Updates**: Built-in support for real-time Nostr updates
3. **Consistent Filtering**: Unified filtering across local and Nostr workouts
4. **Better Type Safety**: Improved TypeScript types and interfaces
5. **Reduced Boilerplate**: Less code needed in components
6. **Offline Support**: Better handling of offline scenarios
7. **Enhanced Performance**: Optimized queries and caching
## Migration Timeline
- The legacy services (`EnhancedWorkoutHistoryService`, `NostrWorkoutHistoryService`) and hooks (`useNostrWorkoutHistory`) are now deprecated
- These services and hooks will be removed in a future release
- All components should migrate to the new unified API as soon as possible
## Troubleshooting
### Common Issues
1. **Subscription Management**
If you encounter issues with Nostr subscriptions:
```typescript
// Make sure to clean up subscriptions when the component unmounts
useEffect(() => {
// Component setup...
return () => {
// Always clean up subscriptions to prevent memory leaks
if (subId) {
workoutHistoryService.unsubscribeFromNostrWorkouts(subId);
}
};
}, [workoutHistoryService]);
```
2. **Filtering Workouts**
If filtering isn't working as expected:
```typescript
// Use proper typing for filter properties
const filters: WorkoutFilters = {
type: ['strength'], // Explicitly typed array of strings
source: ['local', 'nostr'], // Use valid source values
dateRange: {
start: new Date('2025-01-01'),
end: new Date('2025-04-01')
}
};
```
3. **Calendar Date Issues**
If calendar dates aren't showing workouts:
```typescript
// Make sure to use the getWorkoutsByDate method
const workoutsForSelectedDate = await getWorkoutsByDate(selectedDate);
// If that returns empty results, you can use a fallback:
if (workoutsForSelectedDate.length === 0) {
const filteredWorkouts = allWorkouts.filter(workout =>
isSameDay(new Date(workout.startTime), selectedDate)
);
setSelectedDateWorkouts(filteredWorkouts);
}
```
## Related Documentation
- [History Overview](./history_overview.md) - Overview of the History tab's features and architecture
- [Workout Data Models](../workout/data_models.md) - Details on workout data structures
- [Nostr Offline Queue](../../technical/nostr/offline_queue.md) - Information on offline Nostr functionality

View File

@ -0,0 +1,45 @@
# Library Features
**Last Updated:** 2025-04-01
**Status:** Active
**Related To:** Exercise Management, Template Management, Content Discovery
## Overview
The Library section of POWR provides management for all fitness content including exercises, workout templates, and programs. This documentation covers the various components and features of the Library system.
## Core Documents
- [Library Overview](./library_overview.md) - Comprehensive guide to the Library tab implementation
- Template Organization (Coming Soon) - Details on template structure and management
## Key Features
- **Exercise Management**: Create, edit, search, and categorize exercises
- **Template Management**: Create and manage workout templates
- **Content Discovery**: Find and reuse exercise content
- **Offline Support**: Full functionality with or without connectivity
- **Source Tracking**: Attribution for content from various sources
## Data Architecture
The Library data is managed through several services:
- **LibraryService**: Core data management
- **ExerciseService**: Exercise CRUD operations
- **TemplateService**: Template management
- **FavoritesService**: Favorites functionality
## Integration Points
The Library system integrates with several other parts of the application:
- **Workout Creation**: Selection of exercises and templates
- **History**: Usage tracking and statistics
- **Social Features**: Sharing and discovery of content
## Related Documentation
- [Workout Overview](../workout/workout_overview.md)
- [POWR Packs](../powr_packs/overview.md)
- [Offline Caching](../../technical/caching/cache_management.md)

View File

@ -0,0 +1,147 @@
# Library Tab
**Last Updated:** 2025-04-01
**Status:** Active
**Related To:** Exercise Management, Template Management, Content Discovery
## Overview
The Library tab serves as the centralized hub for managing fitness content in the POWR app, including exercises and workout templates. It provides an organized, searchable interface that supports both locally created content and Nostr-based discovery while maintaining offline usability.
## Key Components
### Navigation Structure
The Library is organized into three main sections using Material Top Tabs:
1. **Templates (Default Tab)** - Workout templates for quick start
2. **Exercises** - Individual exercise library
3. **Programs** - Multi-day workout programs (placeholder for future)
### Templates Management
![Templates Tab Status](https://img.shields.io/badge/Status-MVP%20Complete-green)
The Templates section provides organized access to workout templates from various sources:
| Feature | Status | Description |
|---------|--------|-------------|
| Basic Template List | ✅ | Organized display of all templates |
| Favorites Section | ✅ | Quick access to favorite templates |
| Source Badges | ✅ | Visual indicators for Local/POWR/Nostr sources |
| Recently Used | 🟡 | Partially implemented history tracking |
| Usage Statistics | ❌ | Not yet implemented |
#### Template Display
Each template card shows:
- Template title
- Workout type indication
- Exercise preview (first 3)
- Source badges
- Favorite toggle
#### Templates Search & Filtering
- Real-time search functionality
- Basic filtering by type/category
- Advanced filters partially implemented
### Exercise Management
![Exercises Tab Status](https://img.shields.io/badge/Status-MVP%20Complete-green)
The Exercises section organizes all available exercises:
| Feature | Status | Description |
|---------|--------|-------------|
| Alphabetical List | ✅ | With quick scroll navigation |
| Categorization | ✅ | Muscle groups, movement types |
| Source Badges | ✅ | Visual indicators for content source |
| Recent Section | ❌ | Not yet implemented |
| Usage Tracking | ❌ | Not yet implemented |
#### Exercise Display
Each exercise item shows:
- Exercise name
- Category/tags
- Equipment requirements
- Source badge
#### Exercise Search & Filtering
- Real-time search functionality
- Basic filters for category/equipment
- Advanced filtering partially implemented
## Technical Implementation
### Data Architecture
The Library uses SQLite for persistent storage with the following key services:
- **LibraryService**: Core data management
- **ExerciseService**: Exercise CRUD operations
- **TemplateService**: Template management
- **FavoritesService**: Favorites functionality
### Content Management
| Feature | Status | Notes |
|---------|--------|-------|
| Exercise Creation | ✅ | Full CRUD support |
| Template Creation | ✅ | Full CRUD support |
| Content Validation | ✅ | Basic validation implemented |
| Tag Management | 🟡 | Partial implementation |
| Media Support | ❌ | Planned for future |
### Performance Metrics
The Library meets key performance requirements:
- Search response: < 100ms
- Scroll performance: 60fps
- Database operations: < 50ms
## Offline-First Implementation
The Library is designed with offline-first principles:
- Local storage for all core content
- Offline content creation
- Pending sync status indicators
- Background synchronization when online
## Nostr Integration
![Nostr Status](https://img.shields.io/badge/Status-In%20Planning-yellow)
While the types and schemas have been defined, the Nostr integration is still in progress:
- Content discovery from relays
- Publishing templates to Nostr
- Social interactions (likes, shares)
- Attribution and source tracking
## Development Roadmap
### Phase 1: Core Enhancements (Current)
- Usage statistics and history tracking
- Enhanced details views
- Improved filtering system
### Phase 2: Advanced Features
- Media support
- Performance metrics
- Enhanced filtering
### Phase 3: Nostr Integration
- Event handling
- Content synchronization
- Offline capabilities
## Related Components
- [Workout Creation Flow](../workout/workout_overview.md)
- [POWR Packs](../powr_packs/overview.md)
- [Caching System](../../technical/caching/cache_management.md)

View File

@ -0,0 +1,243 @@
# Template Organization and Creation
**Last Updated:** 2025-04-02
**Status:** Active
**Related To:** Library Management, Workout Templates, User Experience
## Overview
This document outlines both the organization system for workout templates and the template creation process in the POWR app. It covers the folder-based organization system with drag-and-drop capabilities and the guided template creation flow.
## Template Organization
### Problem Statement
Users need a flexible way to organize their growing collection of workout templates. As users accumulate templates from various sources (local creation, POWR packs, Nostr), a more robust organization system is required beyond the current flat list with basic filtering.
### Current Implementation Status
![Organization Status](https://img.shields.io/badge/Status-In%20Development-yellow)
| Feature | Status | Description |
|---------|--------|-------------|
| Folder Structure | 🟡 | Basic implementation in progress |
| Drag-and-Drop | 🟡 | Core functionality in testing |
| Template Search | ✅ | Cross-folder searching implemented |
| Template Filtering | ✅ | Category and favorite filters working |
| Custom Sorting | 🟡 | Manual ordering partially implemented |
### Folder Organization
The template organization system provides users with the following capabilities:
- Create, rename, and delete folders
- Move templates between folders via drag-and-drop
- Reorder templates within folders
- Reorder folders themselves
- Collapse/expand folder views
- Support template color coding
- Batch operations for moving/deleting templates
### Technical Implementation
#### Data Structure
```typescript
interface TemplateFolder {
id: string;
name: string;
color?: string;
icon?: string;
order: number;
created_at: number;
updated_at: number;
}
interface Template {
// ... existing fields ...
folder_id?: string; // Optional - null means root level
order: number; // Position within folder or root
}
```
#### Core Components
```typescript
// Folder management
interface FolderManagerHook {
folders: TemplateFolder[];
createFolder: (name: string) => Promise<string>;
updateFolder: (id: string, data: Partial<TemplateFolder>) => Promise<void>;
deleteFolder: (id: string) => Promise<void>;
reorderFolder: (id: string, newOrder: number) => Promise<void>;
}
// Draggable template component
interface DraggableTemplateProps {
template: Template;
onDragStart?: () => void;
onDragEnd?: (dropZone: DropZone) => void;
isDragging?: boolean;
}
// Drop zone types
type DropZone = {
type: 'folder' | 'root' | 'template';
id: string;
position: 'before' | 'after' | 'inside';
}
```
#### Implementation Strategy
The drag-and-drop functionality is implemented using:
- `react-native-reanimated` for smooth animations
- `react-native-gesture-handler` for cross-platform gestures
- SQLite for local persistence
- Nostr events for cloud synchronization
### Future Enhancements
- Nested folders
- Folder sharing via Nostr
- Template duplication
- Advanced sorting options
- Folder templates (to quickly create multiple templates with similar structure)
- Grid view option
## Template Creation
### Problem Statement
Users need a guided, step-by-step interface to create workout templates. The system should focus on the core strength training features while maintaining an architecture that can be extended to support other workout types.
### Current Implementation Status
![Creation Status](https://img.shields.io/badge/Status-MVP%20Complete-green)
| Feature | Status | Description |
|---------|--------|-------------|
| Basic Template Creation | ✅ | Create/edit/delete templates completed |
| Exercise Selection | ✅ | Search and add from exercise library |
| Sets/Reps Configuration | ✅ | Basic configuration implemented |
| Template Preview | ✅ | Preview before saving implemented |
| Template Duplication | ❌ | Not yet implemented |
| Exercise Reordering | ❌ | Not yet implemented |
| Advanced Workout Types | 🟡 | Circuit/EMOM/AMRAP in development |
### Creation Flow
The template creation process follows a multi-step wizard pattern:
1. **Basic Info**: Enter template name, description, and target muscle groups
2. **Exercise Selection**: Search and add exercises from the library
3. **Exercise Configuration**: Configure sets, reps, rest periods for each exercise
4. **Template Review**: Preview the complete template before saving
5. **Save Template**: Store the template to the library
```mermaid
flowchart TD
A[FAB Click] --> B[Basic Info Sheet]
B --> C[Exercise Selection]
C --> D[Exercise Config]
D --> E[Template Review]
E --> F[Save Template]
C -.-> C1[Search Exercises]
C -.-> C2[Filter Exercises]
D -.-> D1[Configure Sets]
D -.-> D2[Configure Rest]
```
### Technical Implementation
#### Data Structure
The POWR app uses a database schema compatible with Nostr for future sync capabilities:
```typescript
interface StrengthTemplate {
id: string;
title: string;
description?: string;
type: 'strength';
targetMuscleGroups: string[];
exercises: TemplateExercise[];
created_at: number;
updated_at: number;
source: 'local' | 'powr' | 'nostr';
format?: {
// Template-wide format settings
weight?: boolean;
reps?: boolean;
rpe?: boolean;
set_type?: boolean;
};
format_units?: {
weight: 'kg' | 'lbs';
reps: 'count';
rpe: '0-10';
set_type: 'warmup|normal|drop|failure';
};
}
interface TemplateExercise {
exerciseId: string;
orderIndex: number;
sets: number;
reps: number;
weight?: number;
rpe?: number;
setType: 'warmup' | 'normal' | 'drop' | 'failure';
restSeconds?: number;
notes?: string;
// Format overrides omitted for brevity
}
```
#### Core Components
The template creation process consists of several key components:
1. **Template Creation Sheet**: Main container for the creation flow
2. **Basic Info Form**: Input fields for template metadata
3. **Exercise Selection Component**: Searchable list with quick-add
4. **Exercise Configuration Form**: Input for sets, reps, and other parameters
5. **Template Preview**: Displays the template with editing capabilities
#### UI/UX Considerations
- Loading states with skeleton loaders for exercise list
- Disabled buttons during operations
- Progress indicators for database operations
- Validation error messages
- Required field notifications
- Save confirmations and success states
### Future Enhancements
- Circuit workout support
- EMOM workout support
- AMRAP workout support
- Complex progression schemes
- Template variations
- AI-assisted template creation
- Exercise substitutions
- Previous workout data import
## Integration Points
The template organization and creation systems integrate with several other components:
- **Library Management**: Templates appear in the library view
- **Workout Execution**: Templates are used to start workouts
- **POWR Packs**: Imported templates are integrated into the organization system
- **Nostr Sharing**: Templates can be shared and synchronized via Nostr
- **Exercise Library**: Provides exercises for template creation
## Related Documentation
- [Library Overview](./library_overview.md) - General library functionality
- [POWR Packs](../powr_packs/overview.md) - Template pack importing
- [Workout Execution](../workout/workout_overview.md) - Using templates in workouts

View File

@ -0,0 +1,194 @@
# Authentication Patterns
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Profile Tab Overview](./profile_overview.md), [Nostr Integration](../../technical/nostr/index.md)
## Introduction
This document outlines the authentication patterns used throughout the Profile tab and other parts of the application that integrate with Nostr. These patterns ensure consistent handling of authenticated and unauthenticated states while maintaining React hook ordering consistency.
## Key Challenges
When implementing authentication in React components, especially with React Navigation and conditional rendering, several key challenges needed to be addressed:
1. **Hook Ordering**: React requires hooks to be called in the same order on every render
2. **Early Returns**: Authentication checks often lead to early returns, which can cause inconsistent hook calls
3. **Conditional Hook Calling**: Different UI for authenticated vs. unauthenticated states can lead to conditional hook usage
## Solution Pattern
The application implements a consistent pattern for handling authentication across components:
### 1. Define All Hooks Unconditionally
All hooks must be defined at the top level of the component, before any conditional returns:
```jsx
// Component setup and imports...
export default function ProfileComponent() {
// Define all hooks at the top level, regardless of authentication state
const { isAuthenticated, currentUser } = useNDKCurrentUser();
const theme = useTheme() as CustomTheme;
const [isLoginSheetOpen, setIsLoginSheetOpen] = useState(false);
// Even hooks that are only needed in authenticated state should be called
// with safeguards to prevent errors
const analytics = useAnalytics();
const stats = isAuthenticated ? useProfileStats({ pubkey: currentUser?.pubkey || '' }) : { followersCount: 0, followingCount: 0 };
// Define all callback functions before any conditional returns
const handleSomeAction = useCallback(() => {
// Implementation...
}, [dependencies]);
// Only after all hooks are defined, use conditional rendering
if (!isAuthenticated) {
return <NostrProfileLogin message="Appropriate message" />;
}
// Main component rendering logic for authenticated state...
}
```
### 2. Use Memoized Sub-Components for Complex Conditional Logic
For more complex components that have multiple conditional UI elements:
```jsx
// Define separate components for different rendering scenarios
const AuthenticatedView = React.memo(() => {
// Can safely use hooks here too, as this component itself
// is unconditionally rendered by the parent
const someHook = useSomeHook();
return (
<View>
{/* Authenticated UI */}
</View>
);
});
// Main component with consistent hook ordering
export default function Component() {
const { isAuthenticated } = useNDKCurrentUser();
// Other hooks...
// Then use conditional rendering
return (
<View>
{isAuthenticated ? <AuthenticatedView /> : <NostrProfileLogin />}
</View>
);
}
```
### 3. Safe Hook Usage with Fallbacks
When a hook is only useful in authenticated state but must be called regardless:
```jsx
// For hooks that need authentication but must be called every time
const { data, loading } = isAuthenticated
? useSomeDataHook({ userId: currentUser?.pubkey })
: { data: null, loading: false };
```
## Real-World Example
In the Profile Overview screen, follower statistics are handled safely regardless of authentication state:
```jsx
// Profile data including fallbacks
const profileImageUrl = currentUser?.profile?.image ||
currentUser?.profile?.picture ||
(currentUser?.profile as any)?.avatar;
// Follower stats component - always call useProfileStats hook
// even if isAuthenticated is false (passing empty pubkey)
// This ensures consistent hook ordering regardless of authentication state
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({
pubkey: pubkey || '',
refreshInterval: 60000 * 15 // refresh every 15 minutes
});
// Use a separate component to avoid conditionally rendered hooks
const ProfileFollowerStats = React.memo(() => {
return (
<View className="flex-row mb-2">
<TouchableOpacity className="mr-4">
<Text>
<Text className="font-bold">{statsLoading ? '...' : followingCount.toLocaleString()}</Text>
<Text className="text-muted-foreground"> following</Text>
</Text>
</TouchableOpacity>
<TouchableOpacity>
<Text>
<Text className="font-bold">{statsLoading ? '...' : followersCount.toLocaleString()}</Text>
<Text className="text-muted-foreground"> followers</Text>
</Text>
</TouchableOpacity>
</View>
);
});
```
## NostrProfileLogin Component
All tabs use the NostrProfileLogin component for unauthenticated states:
```jsx
// Show different UI when not authenticated
if (!isAuthenticated) {
return <NostrProfileLogin message="Login-specific message for this screen" />;
}
```
The NostrProfileLogin component provides a consistent login interface with a customizable message parameter to contextualize the login prompt based on the specific tab.
## Authentication State Management
The isAuthenticated state comes from the useNDKCurrentUser hook:
```jsx
const { currentUser, isAuthenticated } = useNDKCurrentUser();
```
This hook, provided by the NDK integration, handles checking if a user is authenticated with Nostr, and provides access to the user's profile information and public key.
## Logout Flow
Logout functionality is implemented in the Settings tab using the useNDKAuth hook:
```jsx
const { logout } = useNDKAuth();
// Logout button
<Button
variant="destructive"
onPress={logout}
className="w-full"
>
<Text className="text-white">Logout</Text>
</Button>
```
The logout flow properly clears credentials and updates the isAuthenticated state, which propagates to all components using the NDK context.
## Benefits of This Approach
This authentication pattern provides several benefits:
1. **Consistency**: Authentication is handled consistently across all components
2. **Reliability**: Avoids React hook ordering errors
3. **Maintainability**: Clear pattern for developers to follow
4. **User Experience**: Consistent login interfaces across the application
5. **Performance**: Prevents unnecessary re-renders and hook calls
## Related Documentation
- [Profile Tab Overview](./profile_overview.md) - General overview of the Profile tab
- [Nostr Integration](../../technical/nostr/index.md) - Details on Nostr integration
- [NDK Hooks](../../technical/ndk/index.md) - Documentation on NDK hooks

View File

@ -1,93 +1,152 @@
# Profile Follower Statistics
# Follower Statistics
**Last Updated:** 2025-03-28
**Status:** Active
**Related To:** Profile Screen, Nostr Integration
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Profile Tab](./tabs/overview_tab.md), [NostrBand Integration](../../technical/nostr/nostr_band_integration.md)
## Purpose
## Introduction
This document explains the implementation and usage of real-time follower and following statistics in the POWR app's profile screen. This feature enhances the social experience by providing users with up-to-date visibility into their network connections.
The follower statistics feature provides real-time information about a user's Nostr followers and following counts. This document outlines the technical implementation of follower statistics, which is primarily displayed in the Profile tab.
## Feature Overview
## Implementation Overview
The Profile Follower Statistics feature displays accurate counts of a user's followers and following accounts by integrating with the nostr.band API. This provides users with real-time social metrics directly in their profile view.
Follower statistics are implemented through integration with the NostrBand API, which provides network-wide statistics for Nostr users, including follower counts. This integration is handled through the NostrBandService and exposed via the useProfileStats hook.
Key capabilities:
- Display of accurate follower and following counts
- Automatic refresh of data at configurable intervals
- Graceful loading states during data fetching
- Error handling for network or API issues
- Proper formatting of large numbers
### Key Components
## User Experience
- **NostrBandService**: Core service that interfaces with the NostrBand API
- **useProfileStats Hook**: React hook that provides access to follower statistics
- **ProfileFollowerStats Component**: UI component for displaying follower statistics
### Profile Screen Integration
## NostrBand Service Implementation
Follower statistics appear in the profile header section, directly below the user's npub display. The statistics are presented in a horizontal layout with the following format:
The NostrBandService (implemented in `lib/services/NostrBandService.ts`) provides methods for fetching follower statistics:
```
[Following Count] following [Followers Count] followers
```
For example:
```
2,351 following 4,764 followers
```
The counts are formatted with thousands separators for better readability.
### Loading States
When statistics are being loaded for the first time or refreshed, the UI displays a loading indicator instead of numbers:
```
... following ... followers
```
This provides visual feedback to users that data is being fetched while maintaining layout stability.
### Refresh Behavior
Statistics automatically refresh at configurable intervals (default: every 15 minutes) to ensure data remains relatively current without excessive API calls. The refresh is performed in the background without disrupting the user experience.
### Error Handling
If an error occurs during data fetching, the UI gracefully falls back to a default state rather than showing error messages to the user. This ensures a clean user experience even when network issues or API limitations are encountered.
## Technical Implementation
### Components
The feature is implemented using the following components:
1. **ProfileFollowerStats Component**: A React component that displays follower and following counts
2. **useProfileStats Hook**: Provides the data and loading states to the component
3. **NostrBandService**: Service that interfaces with the nostr.band API
### Implementation Details
The follower stats component is embedded within the Profile screen and leverages the nostr.band API through our custom hook:
```tsx
// Profile follower stats component
const ProfileFollowerStats = React.memo(({ pubkey }: { pubkey?: string }) => {
const { followersCount, followingCount, isLoading, error } = useProfileStats({
pubkey,
refreshInterval: 60000 * 15 // refresh every 15 minutes
});
```typescript
// NostrBand service for fetching follower statistics
export class NostrBandService {
private baseUrl = 'https://api.nostr.band/v0';
// Fetch follower statistics for a given public key
public async getProfileStats(pubkey: string): Promise<ProfileStats> {
if (!pubkey) {
return { followersCount: 0, followingCount: 0 };
}
try {
const response = await fetch(`${this.baseUrl}/profile/${pubkey}/stats`);
if (!response.ok) {
throw new Error(`NostrBand API error: ${response.status}`);
}
const data = await response.json();
return {
followersCount: data.followers_count || 0,
followingCount: data.following_count || 0
};
} catch (error) {
console.error('Error fetching profile stats:', error);
// Return default values on error
return { followersCount: 0, followingCount: 0 };
}
}
}
// Singleton instance
export const nostrBandService = new NostrBandService();
```
### ProfileStats Interface
The ProfileStats interface defines the structure of follower statistics:
```typescript
export interface ProfileStats {
followersCount: number;
followingCount: number;
}
```
## useProfileStats Hook
The useProfileStats hook (implemented in `lib/hooks/useProfileStats.ts`) provides a React interface for accessing follower statistics:
```typescript
// Hook for accessing profile statistics
export function useProfileStats({
pubkey,
refreshInterval = 0
}: {
pubkey: string;
refreshInterval?: number;
}): {
followersCount: number;
followingCount: number;
isLoading: boolean;
refresh: () => Promise<void>;
} {
const [stats, setStats] = useState<ProfileStats>({ followersCount: 0, followingCount: 0 });
const [isLoading, setIsLoading] = useState(true);
// Fetch profile stats
const fetchStats = useCallback(async () => {
if (!pubkey) {
setStats({ followersCount: 0, followingCount: 0 });
setIsLoading(false);
return;
}
try {
setIsLoading(true);
const profileStats = await nostrBandService.getProfileStats(pubkey);
setStats(profileStats);
} catch (error) {
console.error('Error in useProfileStats:', error);
} finally {
setIsLoading(false);
}
}, [pubkey]);
// Initial fetch and refresh interval
useEffect(() => {
fetchStats();
if (refreshInterval > 0) {
const interval = setInterval(fetchStats, refreshInterval);
return () => clearInterval(interval);
}
}, [fetchStats, refreshInterval]);
// Return stats and loading state
return {
...stats,
isLoading,
refresh: fetchStats
};
}
```
## Profile Integration
The follower statistics are displayed in the Profile tab using the ProfileFollowerStats component:
```jsx
// Profile follower stats component
const ProfileFollowerStats = React.memo(() => {
return (
<View className="flex-row mb-2">
<TouchableOpacity className="mr-4">
<Text>
<Text className="font-bold">{isLoading ? '...' : followingCount.toLocaleString()}</Text>
<Text className="font-bold">{statsLoading ? '...' : followingCount.toLocaleString()}</Text>
<Text className="text-muted-foreground"> following</Text>
</Text>
</TouchableOpacity>
<TouchableOpacity>
<Text>
<Text className="font-bold">{isLoading ? '...' : followersCount.toLocaleString()}</Text>
<Text className="font-bold">{statsLoading ? '...' : followersCount.toLocaleString()}</Text>
<Text className="text-muted-foreground"> followers</Text>
</Text>
</TouchableOpacity>
@ -96,75 +155,111 @@ const ProfileFollowerStats = React.memo(({ pubkey }: { pubkey?: string }) => {
});
```
### Usage
This component is called in the Profile tab header:
The component is used in the profile screen as follows:
```jsx
// In the Profile header
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({
pubkey: pubkey || '',
refreshInterval: 60000 * 15 // refresh every 15 minutes
});
```tsx
// Inside profile screen component
<ProfileFollowerStats pubkey={currentUser?.pubkey} />
// Later in the JSX
<ProfileFollowerStats />
```
## Configuration Options
## Hook Ordering Consistency
The follower statistics feature can be configured with the following options:
To maintain React hook ordering consistency regardless of authentication state, the useProfileStats hook is always called, even when the user is not authenticated:
### Refresh Interval
The time (in milliseconds) between automatic refreshes of the statistics:
```tsx
// Default: 15 minutes
refreshInterval: 60000 * 15
```jsx
// Always call useProfileStats hook, even if isAuthenticated is false
// This ensures consistent hook ordering regardless of authentication state
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({
pubkey: pubkey || '',
refreshInterval: 60000 * 15
});
```
This can be adjusted based on:
- User engagement patterns
- API rate limiting concerns
- Network performance considerations
When not authenticated or when the pubkey is empty, the hook returns default values (zero counts).
## Design Considerations
## Caching Strategy
### UI Placement and Styling
Follower statistics are cached to reduce API calls and improve performance:
The follower statistics are positioned prominently in the profile header but below the user's identity information (name, username, npub) to create a clear visual hierarchy:
```typescript
// In NostrBandService
private cache = new Map<string, { stats: ProfileStats; timestamp: number }>();
private cacheTTL = 15 * 60 * 1000; // 15 minutes
1. User identity (name, username)
2. User identification (npub)
3. Network statistics (following/followers)
4. User bio/about text
public async getProfileStats(pubkey: string): Promise<ProfileStats> {
// Check cache first
const cached = this.cache.get(pubkey);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
return cached.stats;
}
// Fetch from API if not cached or expired
try {
const stats = await this.fetchProfileStats(pubkey);
// Update cache
this.cache.set(pubkey, {
stats,
timestamp: Date.now()
});
return stats;
} catch (error) {
// Error handling...
}
}
```
This placement follows common social media patterns where follower counts are important but secondary to identity.
## Offline Support
### Interactions
The follower statistics feature gracefully handles offline states:
While currently implemented as TouchableOpacity components, the follower and following counts are prepared for future enhancements that would allow:
- Tapping on "following" to show a list of accounts the user follows
- Tapping on "followers" to show a list of followers
```typescript
public async getProfileStats(pubkey: string): Promise<ProfileStats> {
// Check connectivity
if (!this.connectivityService.isOnline()) {
// Return cached data if available
const cached = this.cache.get(pubkey);
if (cached) {
return cached.stats;
}
// Return default values if no cached data
return { followersCount: 0, followingCount: 0 };
}
// Proceed with normal API call if online
// ...
}
```
These future enhancements will be implemented in subsequent updates.
## Performance Considerations
## Accessibility
To ensure good performance, several optimizations are implemented:
The follower statistics component is designed with accessibility in mind:
- Text sizing uses relative units to respect system font size settings
- Color contrast meets WCAG accessibility guidelines
- Component supports screen readers with appropriate text labels
1. **Memoization**: The ProfileFollowerStats component is memoized to prevent unnecessary re-renders
2. **Refresh Interval**: Stats are only refreshed at specific intervals (default: 15 minutes)
3. **Caching**: Follower statistics are cached to reduce API calls
4. **Lazy Loading**: Stats are loaded after the profile is rendered to prioritize UI responsiveness
## Future Enhancements
Planned enhancements for the follower statistics feature include:
Future enhancements to follower statistics include:
1. Navigate to follower/following lists when tapping on counts
2. Visual indicator for recent changes in follower counts
3. Offline support with cached follower statistics
4. Enhanced error states with retry options
5. Additional statistics such as post count and engagement metrics
1. **Follower List**: Display a list of followers/following with profile information
2. **Interaction Analytics**: Show interaction statistics with followers
3. **Growth Tracking**: Track follower growth over time
4. **Network Visualization**: Visualize the user's Nostr network
5. **Notification Integration**: Notify users of new followers
## References
## Related Documentation
- [NostrBandService](../../../lib/services/NostrBandService.ts) - The service implementation
- [useProfileStats](../../../lib/hooks/useProfileStats.ts) - The React hook implementation
- [Profile Overview Screen](../../../app/(tabs)/profile/overview.tsx) - The profile screen implementation
- [nostr.band API Integration](../../technical/nostr/nostr_band_integration.md) - Technical documentation on the API integration
- [Profile Tab](./tabs/overview_tab.md) - UI implementation of profile display
- [NostrBand Integration](../../technical/nostr/nostr_band_integration.md) - Technical details of the NostrBand API integration
- [Authentication Patterns](./authentication_patterns.md) - How authentication affects hook ordering

View File

@ -1,138 +1,106 @@
# Profile Features
# Profile Section Documentation
**Last Updated:** 2025-03-25
**Status:** Active
**Related To:** User Identity, Progress, Settings
## Purpose
This document provides an overview of the profile tab features in the POWR app. It describes the various sections of the profile tab, their purposes, and how they integrate with other app features.
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Nostr Integration](../../technical/nostr/index.md), [Analytics](../../technical/analytics/index.md)
## Overview
The profile tab serves as the user's personal space within the app, providing:
The Profile section provides users with a comprehensive interface for managing their personal information, viewing activity, tracking progress, and configuring application settings. This documentation covers all aspects of the Profile tab implementation, from high-level architecture to specific implementation details.
1. User identity and account management
2. Progress tracking and analytics
3. Application settings and preferences
4. Social activity overview
5. Account management features
## Documentation Structure
The profile implementation is focused on these key principles:
### Core Documentation
- User control over personal information
- Clear organization of progress data
- Simple access to app settings
- Integration with Nostr for identity
- [Profile Tab Overview](./profile_overview.md) - High-level overview of the Profile tab structure and architecture
- [Authentication Patterns](./authentication_patterns.md) - Technical details on authentication implementation and React hook ordering
## Component Architecture
### Tab Implementation Details
### High-Level Components
- [Profile (Overview) Tab](./tabs/overview_tab.md) - User profile display and social feed
- [Activity Tab](./tabs/activity_tab.md) - User activity summary and recent workouts
- [Progress Tab](./tabs/progress_tab.md) - Workout analytics and progress tracking
- [Settings Tab](./tabs/settings_tab.md) - Application and account settings
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ UI Layer │ │ Service Layer │ │ Data Layer │
│ │ │ │ │ │
│ Profile Screens │ │ Profile Service │ │ User Data │
│ Settings Views │◄───►│ Analytics │◄───►│ Settings Storage│
│ Progress Views │ │ Auth Management │ │ Analytics Data │
└─────────────────┘ └─────────────────┘ └─────────────────┘
### Technical Features
- [Progress Tracking](./progress_tracking.md) - Implementation details for workout progress tracking
- [Follower Statistics](./follower_stats.md) - NostrBand integration for follower statistics
## Key Technical Concepts
The Profile section implements several key technical concepts that are worth highlighting:
### Authentication Pattern
The Profile tab implements a consistent authentication pattern that ensures React hook ordering is maintained while supporting conditional rendering based on authentication state. See [Authentication Patterns](./authentication_patterns.md) for details.
### Components and Data Flow
```mermaid
graph TD
A[Profile Tab Navigator] --> B[Overview Tab]
A --> C[Activity Tab]
A --> D[Progress Tab]
A --> E[Settings Tab]
B --> F[Profile Header]
B --> G[Social Feed]
B --> H[Follower Stats]
C --> I[Workout Stats]
C --> J[Personal Records]
C --> K[Recent Workouts]
D --> L[Analytics Charts]
D --> M[Period Filter]
D --> N[Nostr Toggle]
E --> O[Account Settings]
E --> P[App Settings]
E --> Q[Logout]
```
### Profile Screens
### Hook Management
- `ProfileOverview`: Main profile screen with user information
- `ProfileProgress`: Progress tracking visualizations and data
- `ProfileActivity`: Recent social and workout activity
- `ProfileTerms`: Terms of service and legal documents
- `ProfileSettings`: App settings and preferences
The Profile section carefully manages React hooks to ensure they are called consistently regardless of conditional rendering:
### MVP Implementation Focus
```javascript
// Always define hooks at the top level, regardless of authentication state
const { isAuthenticated } = useNDKCurrentUser();
const someHook = useSomeHook();
For the MVP release, the profile tab focuses on:
// Only after all hooks are defined, use conditional rendering
if (!isAuthenticated) {
return <LoginComponent />;
}
1. **User Identity**
- Basic profile information display
- Profile editing capabilities
- Nostr pubkey association
// Continue with authenticated UI
```
2. **Progress Tracking**
- Exercise progress charts and metrics
- Performance tracking over time
- Personal records and milestones
### Nostr Integration
3. **Core Settings**
- App preferences
- Theme switching
- Account management
The Profile section deeply integrates with Nostr for user profiles, follower statistics, and social feed:
4. **Activity Overview**
- Limited view of recent workouts
- Social activity summary
- Simplified activity feed
```javascript
// Example of Nostr integration in the profile
const { currentUser, isAuthenticated } = useNDKCurrentUser();
const { followersCount, followingCount } = useProfileStats({
pubkey: currentUser?.pubkey || ''
});
```
## User Identity
## Implementation Challenges
The profile tab handles user identity through:
Several key implementation challenges were addressed in the Profile section:
1. **Profile Information**
- Display name
- Profile picture (with Nostr and local options)
- User metadata
- Exercise history summary
2. **Authentication Management**
- Nostr key handling
- Login/logout functionality
- Key creation and import
## Progress Features
Progress tracking is a key feature of the profile tab:
1. **Progress Charts**
- Exercise-specific progress tracking
- Weight/volume progression charts
- Performance metrics
- Personal records
2. **Workout Summary Data**
- Total workouts completed
- Exercise frequency
- Workout time analytics
- Consistency metrics
## Settings and Preferences
The profile tab provides access to app settings:
1. **App Preferences**
- Theme selection (light/dark)
- Notification preferences
- Default units (kg/lbs)
2. **Account Management**
- Export/import data
- Clear cache
- Data management
## Activity Overview
The profile tab includes a simplified activity overview:
1. **Recent Workouts**
- Last few completed workouts
- Quick stats
- Links to full history
2. **Social Activity**
- Recent social interactions
- Posts and shares
- Simplified activity feed
1. **React Hook Consistency**: Ensuring React hooks are called in the same order regardless of authentication state
2. **Feed Data Transformation**: Transforming feed data from different sources into a consistent format
3. **Analytics Visualization**: Creating user-friendly visualizations of workout analytics
4. **Offline Support**: Graceful handling of offline state throughout the Profile section
## Related Documentation
- [Progress Tracking](./progress_tracking.md) - Detailed description of progress tracking features
- [MVP and Targeted Rebuild](../../project/mvp_and_rebuild.md) - Overall MVP strategy
- [Workout History](../history/index.md) - How workout history integrates with profile
- [Authentication](../../architecture/authentication.md) - Authentication architecture
- [Nostr Integration](../../technical/nostr/index.md) - General Nostr integration documentation
- [Analytics Implementation](../../technical/analytics/index.md) - Implementation details for analytics
- [Theme System](../../technical/styling/theme_system.md) - App theming system

View File

@ -0,0 +1,132 @@
# Profile Tab Overview
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Nostr Integration](../../technical/nostr/index.md), [Analytics](../../technical/analytics/index.md)
## Introduction
The Profile tab provides users with a comprehensive interface for managing their personal information, viewing their activity, analyzing their progress, and configuring application settings. This section is deeply integrated with Nostr for cross-device synchronization and includes advanced analytics capabilities for tracking workout progress.
## Features
The Profile tab is organized into four main sections, each implemented as a tab within a material top tab navigator:
| Tab | Purpose | Key Features |
|-----|---------|-------------|
| **Profile** | User profile display and social feed | User metadata, follower statistics, personal social feed |
| **Activity** | User activity summary | Workout statistics, recent workouts, personal records |
| **Progress** | Detailed analytics and progress tracking | Workout analytics, charts, period-based filtering |
| **Settings** | Application and account configuration | Theme settings, units selection, Terms of Service, logout |
## Technical Architecture
The Profile tab is built with a consistent architecture throughout all its components:
- **Authentication Integration**: Each tab properly handles both authenticated and unauthenticated states
- **Nostr Integration**: User profiles, social feeds, and cross-device synchronization via the Nostr protocol
- **React Hook Management**: Carefully crafted component structure to ensure consistent hook ordering
- **Analytics**: Integration with AnalyticsService for workout progress tracking
- **Offline Support**: Detection and handling of offline states across all tabs
## Authentication Pattern
The profile section implements a consistent authentication pattern:
```jsx
// Pattern used across all profile tab components
if (!isAuthenticated) {
return <NostrProfileLogin message="Login-specific message" />;
}
// Main component content for authenticated users
return (
<Component>
{/* Tab content */}
</Component>
);
```
This pattern ensures that unauthenticated users are presented with a login interface while maintaining React hook ordering consistency.
## Implementation Challenges
Several key technical challenges were addressed in the implementation:
1. **React Hook Consistency**: The codebase uses careful component structuring to ensure React hooks are called in the same order for every render, preventing the "rendered fewer hooks than expected" error
2. **Authentication State Management**: Login state is handled consistently across all tabs
3. **NostrBand Integration**: Real-time follower statistics via the NostrBand API
4. **Feed Entry Format Transformation**: Complex transformation between different social feed entry formats
## Tab Structure
The Tab navigator is implemented in `app/(tabs)/profile/_layout.tsx`:
```typescript
<Tab.Navigator
initialRouteName="overview"
screenOptions={{
tabBarActiveTintColor: theme.colors.tabIndicator,
tabBarInactiveTintColor: theme.colors.tabInactive,
tabBarLabelStyle: {
fontSize: 14,
textTransform: 'capitalize',
fontWeight: 'bold',
},
tabBarIndicatorStyle: {
backgroundColor: theme.colors.tabIndicator,
height: 2,
},
tabBarStyle: {
backgroundColor: theme.colors.background,
elevation: 0,
shadowOpacity: 0,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
tabBarPressColor: theme.colors.primary,
}}
>
<Tab.Screen
name="overview"
component={OverviewScreen}
options={{ title: 'Profile' }}
/>
<Tab.Screen
name="activity"
component={ActivityScreen}
options={{ title: 'Activity' }}
/>
<Tab.Screen
name="progress"
component={ProgressScreen}
options={{ title: 'Progress' }}
/>
<Tab.Screen
name="settings"
component={SettingsScreen}
options={{ title: 'Settings' }}
/>
</Tab.Navigator>
```
## Individual Tab Documentation
- [Profile Tab](./tabs/overview_tab.md) - User profile information and social feed
- [Activity Tab](./tabs/activity_tab.md) - User activity summary and recent workouts
- [Progress Tab](./tabs/progress_tab.md) - Detailed analytics and charts
- [Settings Tab](./tabs/settings_tab.md) - Application and account settings
## Technical Documentation
- [Authentication Patterns](./authentication_patterns.md) - Technical details about authentication implementation
- [Progress Tracking](./progress_tracking.md) - How workout progress is tracked and visualized
- [Follower Statistics](./follower_stats.md) - NostrBand integration for follower counts
## Related Documentation
- [Nostr Integration](../../technical/nostr/index.md) - How Nostr is used throughout the application
- [Analytics Service](../../technical/analytics/index.md) - Workout analytics implementation details

View File

@ -1,127 +1,285 @@
# Progress Tracking
**Last Updated:** 2025-03-25
**Status:** Active
**Related To:** Profile Features, Analytics, Workout History
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Progress Tab](./tabs/progress_tab.md), [Analytics Service](../../technical/analytics/index.md)
## Purpose
## Introduction
This document describes the progress tracking features in the POWR app's profile tab. It outlines how users can track their exercise progress, view trends over time, and analyze their workout performance.
The progress tracking features in POWR provide users with comprehensive analytics and visualizations of their workout performance over time. This document outlines the technical implementation of progress tracking, which is primarily exposed through the Progress tab in the Profile section.
## Overview
## Implementation Overview
The progress tracking feature allows users to:
Progress tracking is implemented through the AnalyticsService, which processes workout data from both local storage and Nostr to generate statistics, identify trends, and track personal records.
1. View exercise-specific progress over time
2. Track key performance metrics (weight, volume, frequency)
3. Identify personal records and milestones
4. Analyze workout trends and patterns
### Key Components
The progress tracking is designed with the following principles:
- **AnalyticsService**: Core service that computes statistics and manages progress data
- **useAnalytics Hook**: React hook that provides access to analytics capabilities
- **Progress Tab UI**: Visualization and filtering of progress data
- **Nostr Integration**: Toggle for including or excluding Nostr workout data
- Exercise-focused rather than workout-focused
- Clear visual representation of progress
- Focus on actionable insights
- Privacy-first with user control over data sharing
## Analytics Service Implementation
## Component Architecture
The AnalyticsService (implemented in `lib/services/AnalyticsService.ts`) provides several key methods for tracking progress:
### High-Level Components
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ UI Layer │ │ Service Layer │ │ Data Layer │
│ │ │ │ │ │
│ Progress Charts │ │ Analytics │ │ Workout History │
│ Exercise List │◄───►│ Services │◄───►│ Data │
│ Metrics Display │ │ Aggregation │ │ Calculation │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```typescript
// Core analytics service class
export class AnalyticsService {
private includeNostr: boolean = true;
// Configure whether to include Nostr workouts in analytics
public setIncludeNostr(include: boolean): void {
this.includeNostr = include;
}
// Get statistics for the specified time period
public async getWorkoutStats(period: 'week' | 'month' | 'year' | 'all'): Promise<WorkoutStats> {
// Implementation details...
}
// Get personal records, optionally filtered by exercise
public async getPersonalRecords(
exerciseId?: string,
limit?: number
): Promise<PersonalRecord[]> {
// Implementation details...
}
// Additional methods...
}
```
### UI Components
### WorkoutStats Interface
- `ProgressCharts`: Visual representations of key metrics over time
- `ExerciseList`: Selectable list of exercises with progress indicators
- `MetricsDisplay`: Numerical representation of key performance indicators
- `PRBadges`: Visual indicators of personal records
The WorkoutStats interface defines the structure of statistics returned by the analytics service:
### Service Layer
```typescript
export interface WorkoutStats {
workoutCount: number;
totalDuration: number;
totalVolume: number;
frequencyByDay: number[];
exerciseDistribution: Record<string, number>;
muscleGroupDistribution: Record<string, number>;
// Additional stats fields...
}
```
- `AnalyticsService`: Processes raw workout data into progress metrics
- `WorkoutAggregator`: Combines data from multiple workouts for a given exercise
- `MetricsCalculator`: Computes derived metrics from raw workout data
### Personal Records
### Data Layer
Personal records are tracked through the PersonalRecord interface:
- Relies on `WorkoutHistoryService` for source data
- Cached calculations for performance
- Local-only analytics (no server processing)
```typescript
export interface PersonalRecord {
id: string;
exerciseId: string;
exerciseName: string;
value: number;
unit: 'kg' | 'lb';
reps: number;
date: number;
workoutId: string;
previousRecord?: {
value: number;
date: number;
};
}
```
## Implementation Details
Personal records are identified by comparing new workout performances against historical data, with improvements being flagged and stored.
### Key Metrics Tracked
## Time Period Filtering
Progress tracking focuses on these primary metrics:
Progress tracking supports different time periods for analysis:
1. **Weight Progression**
- One-rep max (calculated or actual)
- Working weight used for similar rep ranges
- Weight increases over time periods (week, month, year)
```typescript
type AnalyticsPeriod = 'week' | 'month' | 'year' | 'all';
2. **Volume Metrics**
- Total weight moved per exercise
- Sets × reps × weight calculations
- Volume trends over time
// Example filtering implementation
private filterWorkoutsByPeriod(workouts: Workout[], period: AnalyticsPeriod): Workout[] {
const now = Date.now();
const cutoffDate = this.getPeriodCutoffDate(now, period);
return workouts.filter(workout => workout.completedAt >= cutoffDate);
}
3. **Frequency Analysis**
- Exercise frequency per time period
- Body part/movement pattern frequency
- Rest periods between similar workouts
private getPeriodCutoffDate(now: number, period: AnalyticsPeriod): number {
switch (period) {
case 'week':
return now - 7 * 24 * 60 * 60 * 1000;
case 'month':
return now - 30 * 24 * 60 * 60 * 1000;
case 'year':
return now - 365 * 24 * 60 * 60 * 1000;
case 'all':
default:
return 0;
}
}
```
4. **Personal Records**
- Weight PRs at various rep ranges
- Volume PRs per session
- Streak and consistency records
## Workout Frequency Analysis
### Data Visualization
Workout frequency by day of the week is calculated using:
Progress is visualized through:
```typescript
private calculateFrequencyByDay(workouts: Workout[]): number[] {
// Initialize array for days [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
const frequency = Array(7).fill(0);
workouts.forEach(workout => {
const date = new Date(workout.completedAt);
// getDay() returns 0-6 (Sunday-Saturday), adjust to 0-6 (Monday-Sunday)
const dayIndex = (date.getDay() + 6) % 7;
frequency[dayIndex]++;
});
return frequency;
}
```
- Line charts for weight/volume progression
- Bar charts for frequency analysis
- Milestone markers for personal records
- Heat maps for workout frequency
## Exercise Distribution
### Exercise Selection
Exercise distribution is calculated by counting the occurrences of each exercise across workouts:
Users can track progress for:
```typescript
private calculateExerciseDistribution(workouts: Workout[]): Record<string, number> {
const distribution: Record<string, number> = {};
workouts.forEach(workout => {
workout.exercises.forEach(exercise => {
const id = exercise.id;
distribution[id] = (distribution[id] || 0) + 1;
});
});
return distribution;
}
```
- Individual exercises (e.g., Barbell Bench Press)
- Exercise categories (e.g., all bench press variations)
- Movement patterns (e.g., all pushing movements)
- Body parts (e.g., all chest exercises)
## Progress Charts Implementation
### Implementation Considerations
The Progress tab visualizes data using custom chart components:
Progress tracking presents several challenges addressed in the implementation:
### Workout Frequency Chart
1. **Data Normalization**
- Handling similar exercises with different names
- Accounting for different equipment types
- Normalizing units (kg/lbs)
```jsx
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>
);
};
```
2. **Valid Comparison**
- Comparing similar set/rep schemes
- Accounting for RPE/intensity differences
- Filtering anomalous data points
## Nostr Integration
3. **Performance Optimization**
- Pre-calculating common metrics
- Caching results for frequently viewed exercises
- Progressive loading of historical data
Progress tracking includes a toggle for including or excluding Nostr workout data:
```jsx
<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>
```
When the toggle is activated, the `setIncludeNostr` method is called on the AnalyticsService:
```jsx
// Pass includeNostr flag to analytics service
analyticsService.setIncludeNostr(includeNostr);
```
## Future Enhancements
Future enhancements to progress tracking include:
1. **Enhanced Visualizations**: More sophisticated chart types and visualizations
2. **Goal Setting**: Ability to set and track progress toward specific goals
3. **AI Insights**: Machine learning-based insights and recommendations
4. **Export Functionality**: Ability to export progress data in various formats
5. **Custom Time Periods**: Custom date range selection for more precise analysis
## Technical Considerations
### Performance
For large datasets, analytics calculations can be resource-intensive. To address this:
- Computations are performed in small batches
- Results are cached where appropriate
- Background processing is used for longer calculations
### Data Migration
Progress tracking handles data from both the local database and Nostr, requiring data normalization:
```typescript
private normalizeWorkout(workout: Workout | NostrWorkout): NormalizedWorkout {
// Implement normalization logic to ensure consistent data formats
// regardless of the data source
}
```
### Offline Support
Progress tracking works seamlessly in offline mode by focusing on local workouts when Nostr data is unavailable:
```typescript
private async getWorkouts(period: AnalyticsPeriod): Promise<Workout[]> {
const localWorkouts = await this.workoutService.getCompletedWorkouts();
// If offline or Nostr disabled, return only local workouts
if (!this.includeNostr || !this.connectivityService.isOnline()) {
return this.filterWorkoutsByPeriod(localWorkouts, period);
}
// Otherwise include Nostr workouts
const nostrWorkouts = await this.nostrWorkoutService.getCompletedWorkouts();
const allWorkouts = [...localWorkouts, ...nostrWorkouts];
return this.filterWorkoutsByPeriod(allWorkouts, period);
}
```
## Related Documentation
- [Workout History Features](../history/index.md) - Source data for progress tracking
- [MVP and Targeted Rebuild](../../project/mvp_and_rebuild.md) - Overall MVP strategy
- [Profile Tab Architecture](./profile_architecture.md) - Overall profile tab design
- [Progress Tab](./tabs/progress_tab.md) - UI implementation of progress tracking
- [Analytics Service](../../technical/analytics/index.md) - Technical details of the analytics service
- [Activity Tab](./tabs/activity_tab.md) - Uses progress tracking for activity summary

View File

@ -0,0 +1,250 @@
# Activity Tab
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Profile Tab Overview](../profile_overview.md), [Progress Tab](./progress_tab.md)
## Introduction
The Activity tab provides users with a summary of their workout activities, including statistics, recent workouts, and personal records. It serves as a quick overview dashboard for tracking workout progress and achievements.
## Features
| Feature | Status | Notes |
|---------|--------|-------|
| Workout Stats Cards | ✅ Implemented | Shows completed workouts, templates, programs, and PRs |
| Personal Records | ✅ Implemented | Lists recent personal records with dates |
| Recent Workouts | ✅ Implemented | Shows most recent completed workouts |
| Quick Action Buttons | ✅ Implemented | Navigation to start workout and view progress |
| Authentication Detection | ✅ Implemented | Login prompt for unauthenticated users |
| Integration with Analytics | ✅ Implemented | Uses AnalyticsService for workout statistics |
## Implementation Details
The Activity tab is implemented in `app/(tabs)/profile/activity.tsx`. It integrates with the AnalyticsService for personal records and the WorkoutService for workout history.
### Stats Cards Implementation
The Activity tab displays four key statistics in card format:
```jsx
{/* Stats Cards - Row 1 */}
<View className="flex-row px-4 pt-4">
<View className="flex-1 pr-2">
<Card>
<CardContent className="p-4 items-center justify-center">
<Dumbbell size={24} className="text-primary mb-2" />
<Text className="text-2xl font-bold">{completedWorkouts}</Text>
<Text className="text-muted-foreground">Workouts</Text>
</CardContent>
</Card>
</View>
<View className="flex-1 pl-2">
<Card>
<CardContent className="p-4 items-center justify-center">
<Calendar size={24} className="text-primary mb-2" />
<Text className="text-2xl font-bold">{totalTemplates}</Text>
<Text className="text-muted-foreground">Templates</Text>
</CardContent>
</Card>
</View>
</View>
{/* Stats Cards - Row 2 */}
<View className="flex-row px-4 pt-4 mb-4">
<View className="flex-1 pr-2">
<Card>
<CardContent className="p-4 items-center justify-center">
<BarChart2 size={24} className="text-primary mb-2" />
<Text className="text-2xl font-bold">{totalPrograms}</Text>
<Text className="text-muted-foreground">Programs</Text>
</CardContent>
</Card>
</View>
<View className="flex-1 pl-2">
<Card>
<CardContent className="p-4 items-center justify-center">
<Award size={24} className="text-primary mb-2" />
<Text className="text-2xl font-bold">{records.length}</Text>
<Text className="text-muted-foreground">PRs</Text>
</CardContent>
</Card>
</View>
</View>
```
### Personal Records Implementation
The Activity tab displays the user's personal records:
```jsx
{/* Personal Records */}
<Card className="mx-4 mb-4">
<CardContent className="p-4">
<View className="flex-row justify-between items-center mb-4">
<Text className="text-lg font-semibold">Personal Records</Text>
<Pressable onPress={() => router.push('/profile/progress')}>
<Text className="text-primary">View All</Text>
</Pressable>
</View>
{records.length === 0 ? (
<Text className="text-muted-foreground text-center py-4">
No personal records yet. Complete more workouts to see your progress.
</Text>
) : (
records.map((record) => (
<View key={record.id} className="py-2 border-b border-border">
<Text className="font-medium">{record.exerciseName}</Text>
<Text>{record.value} {record.unit} × {record.reps} reps</Text>
<Text className="text-muted-foreground text-sm">
{formatDistanceToNow(new Date(record.date), { addSuffix: true })}
</Text>
</View>
))
)}
</CardContent>
</Card>
```
### Recent Workouts Implementation
The tab also shows the user's most recent completed workouts:
```jsx
{/* Recent Workouts */}
<Card className="mx-4 mb-4">
<CardContent className="p-4">
<View className="flex-row justify-between items-center mb-4">
<Text className="text-lg font-semibold">Recent Workouts</Text>
<Pressable onPress={() => router.push('/history/workoutHistory')}>
<Text className="text-primary">View All</Text>
</Pressable>
</View>
{workouts && workouts.length > 0 ? (
workouts
.filter(workout => workout.isCompleted)
.slice(0, 2)
.map(workout => (
<View key={workout.id} className="mb-3">
<WorkoutCard
workout={workout}
showDate={true}
showExercises={false}
/>
</View>
))
) : (
<Text className="text-muted-foreground text-center py-4">
No recent workouts. Complete a workout to see it here.
</Text>
)}
</CardContent>
</Card>
```
### Analytics Integration
The Activity tab integrates with the AnalyticsService to display personal records:
```jsx
// Load personal records
useEffect(() => {
async function loadRecords() {
if (!isAuthenticated) return;
try {
setLoading(true);
const personalRecords = await analytics.getPersonalRecords(undefined, 3);
setRecords(personalRecords);
} catch (error) {
console.error('Error loading personal records:', error);
} finally {
setLoading(false);
}
}
loadRecords();
}, [isAuthenticated, analytics]);
```
## Technical Considerations
### Authentication Handling
Like other profile tabs, the Activity tab handles authentication with a consistent pattern:
```jsx
// Show different UI when not authenticated
if (!isAuthenticated) {
return <NostrProfileLogin message="Login with your Nostr private key to view your activity and stats." />;
}
```
### Data Loading
The component manages loading states and provides appropriate UI feedback:
```jsx
if (loading || workoutsLoading || templatesLoading) {
return (
<View className="flex-1 items-center justify-center">
<ActivityIndicator />
</View>
);
}
```
### Hook Dependencies
The component carefully manages effect hook dependencies to prevent unnecessary re-renders:
```jsx
useEffect(() => {
async function loadRecords() {
// ...implementation
}
loadRecords();
}, [isAuthenticated, analytics]); // Only rerun when these dependencies change
```
## User Experience Flow
1. **Authentication Check**:
- If user is not authenticated, display NostrProfileLogin component
- If authenticated, proceed to load activity data
2. **Data Loading**:
- Fetch completed workouts count from WorkoutService
- Load templates count from TemplateService
- Fetch personal records from AnalyticsService
- Show loading indicator during data fetch
3. **Data Display**:
- Show stats cards with summary numbers
- Display recent personal records with details
- Show recent completed workouts with WorkoutCard component
- Provide quick action buttons for common tasks
4. **Navigation Options**:
- "View All" links to relevant detailed screens (Progress, History)
- Quick action buttons for starting a workout or viewing progress
## Future Enhancements
1. **Programs Integration**: Complete implementation of programs count when feature is available
2. **Additional Stats**: Add more workout statistics like total volume, duration
3. **Enhanced Personal Records**: Add visual indicators for progress trends
4. **Streak Tracking**: Add workout streak and consistency metrics
5. **Workout Insights**: Add AI-generated insights based on workout patterns
## Related Documentation
- [Profile Overview](../profile_overview.md) - General overview of the Profile tab
- [Progress Tab](./progress_tab.md) - Documentation for the detailed progress tracking tab
- [Analytics Service](../../../technical/analytics/index.md) - Details on workout analytics implementation
- [WorkoutCard Component](../../../components/workout_card.md) - Documentation for the WorkoutCard component used in this tab

View File

@ -0,0 +1,237 @@
# Profile Tab (Overview)
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Profile Tab Overview](../profile_overview.md), [Nostr Integration](../../../technical/nostr/index.md)
## Introduction
The Profile tab (Overview) is the main landing screen in the Profile section. It displays the user's profile information, including their avatar, banner, display name, and follower statistics. It also shows a personal social feed of the user's own posts.
## Features
| Feature | Status | Notes |
|---------|--------|-------|
| Profile Header | ✅ Implemented | User banner, avatar, name, and bio |
| Follower Stats | ✅ Implemented | Real-time follower and following counts |
| Public Key Display | ✅ Implemented | Nostr public key in npub format with copy and QR options |
| Personal Social Feed | ✅ Implemented | Chronological feed of user's posts and activities |
| Pull-to-refresh | ✅ Implemented | Updates profile and feed data |
| Authentication Detection | ✅ Implemented | Login prompt for unauthenticated users |
| Offline Support | ✅ Implemented | Graceful degradation for offline state |
## Implementation Details
The Profile tab is implemented in `app/(tabs)/profile/overview.tsx`. It uses the NostrBand API for follower statistics and the NDK integration for the social feed.
### Profile Header Implementation
The profile header includes several key elements:
```jsx
<View>
{/* Banner Image */}
<View className="w-full h-40 relative">
{bannerImageUrl ? (
<ImageBackground
source={{ uri: bannerImageUrl }}
className="w-full h-full"
resizeMode="cover"
>
<View className="absolute inset-0 bg-black/20" />
</ImageBackground>
) : (
<View className="w-full h-full bg-gradient-to-b from-primary/80 to-primary/30" />
)}
</View>
<View className="px-4 -mt-16 pb-2">
<View className="flex-row items-end mb-4">
{/* Avatar */}
<UserAvatar
size="xl"
uri={profileImageUrl}
fallback={displayName.charAt(0)}
className="mr-4 border-4 border-background"
isInteractive={false}
style={{ width: 90, height: 90 }}
/>
{/* Edit Profile button */}
<View className="ml-auto mb-2">
<TouchableOpacity
className="px-4 h-10 items-center justify-center rounded-md bg-muted"
onPress={handleEditProfilePress}
>
<Text className="font-medium">Edit Profile</Text>
</TouchableOpacity>
</View>
</View>
{/* Profile info */}
<View>
<Text className="text-xl font-bold">{displayName}</Text>
<Text className="text-muted-foreground">{username}</Text>
{/* Public key (npub) with copy/QR options */}
{npubFormat && (
<View className="flex-row items-center mt-1 mb-2">
<Text className="text-xs text-muted-foreground font-mono">
{shortenedNpub}
</Text>
<TouchableOpacity
className="ml-2 p-1"
onPress={handleCopyButtonPress}
>
<Copy size={12} color={theme.colors.text} />
</TouchableOpacity>
<TouchableOpacity
className="ml-2 p-1"
onPress={handleQrButtonPress}
>
<QrCode size={12} color={theme.colors.text} />
</TouchableOpacity>
</View>
)}
{/* Follower stats */}
<ProfileFollowerStats />
{/* About text */}
{aboutText && (
<Text className="mb-3">{aboutText}</Text>
)}
</View>
</View>
</View>
```
### Social Feed Implementation
The social feed displays the user's own posts and activities in reverse chronological order. It integrates with the NDK to fetch posts created by the user.
```jsx
// Initialize feed related state
const [feedItems, setFeedItems] = useState<any[]>([]);
const [feedLoading, setFeedLoading] = useState(false);
const [isOffline, setIsOffline] = useState(false);
// Only call useSocialFeed when authenticated to prevent the error
const socialFeed = isAuthenticated ? useSocialFeed({
feedType: 'profile',
authors: currentUser?.pubkey ? [currentUser.pubkey] : [],
limit: 30
}) : null;
```
### Follower Statistics
The Profile tab uses the `useProfileStats` hook to display real-time follower counts:
```jsx
// Profile follower stats component
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({
pubkey: pubkey || '',
refreshInterval: 60000 * 15 // refresh every 15 minutes
});
```
This hook calls the NostrBand API to fetch follower data, providing an enhanced user experience with network-wide statistics.
## Technical Considerations
### Hook Consistency
The component structure ensures React hooks are called consistently regardless of authentication state:
```jsx
// Always call useProfileStats to maintain hook ordering
const { followersCount, followingCount, isLoading: statsLoading } = useProfileStats({
pubkey: pubkey || '',
refreshInterval: 60000 * 15
});
// Use separate component for follower stats to avoid conditional hook issues
const ProfileFollowerStats = React.memo(() => {
// Component code here
});
// Define all callback hooks before any conditional returns
const handleEditProfilePress = useCallback(() => {
// Handler code
}, [dependencies]);
// Then conditional returns based on authentication
if (!isAuthenticated) {
return renderLoginScreen();
}
```
### Feed Data Transformation
The component transforms NDK social feed entries to a consistent format:
```jsx
// Convert to the format expected by the component
const entries = React.useMemo(() => {
return feedItems.map(item => {
// Create a properly typed AnyFeedEntry based on the item type
const baseEntry = {
id: item.id,
eventId: item.id,
event: item.originalEvent,
timestamp: item.createdAt * 1000,
};
// Add type-specific properties
switch (item.type) {
case 'workout':
return {
...baseEntry,
type: 'workout',
content: item.parsedContent
} as WorkoutFeedEntry;
// Other cases...
}
});
}, [feedItems]);
```
## User Experience Flow
1. **Authentication Check**:
- If user is not authenticated, display NostrProfileLogin component
- If authenticated, proceed to load profile and feed data
2. **Profile Data Loading**:
- Fetch user profile from NDK
- Load follower statistics from NostrBand API
- Parse and format public key for display
3. **Feed Loading**:
- Subscribe to user's own events via NDK
- Transform feed data to consistent format
- Display posts chronologically
- Support pull-to-refresh for updates
4. **Interaction**:
- Profile edit navigation
- Copy public key functionality
- QR code display (future implementation)
- Post interaction
## Future Enhancements
1. **Profile Editing**: Enhanced profile editing capabilities
2. **Post Management**: Delete and edit functionality for personal posts
3. **QR Code Implementation**: Complete QR code display for public key sharing
4. **Expanded Post Types**: Additional post type support in the feed
5. **Improved Offline Functionality**: Enhanced caching for offline viewing
## Related Documentation
- [Profile Overview](../profile_overview.md) - General overview of the Profile tab
- [Activity Tab](./activity_tab.md) - Documentation for the Activity tab
- [NostrBand Integration](../../../technical/nostr/nostr_band_integration.md) - Details on NostrBand API integration
- [useProfileStats](../follower_stats.md) - Documentation for follower statistics hook

View File

@ -0,0 +1,303 @@
# Progress Tab
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Profile Tab Overview](../profile_overview.md), [Activity Tab](./activity_tab.md)
## 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:
```jsx
<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:
```jsx
{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:
```jsx
// 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:
```jsx
// 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:
```jsx
// 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:
```jsx
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:
```jsx
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:
```jsx
{/* 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:
```jsx
// 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
## Related Documentation
- [Profile Overview](../profile_overview.md) - General overview of the Profile tab
- [Activity Tab](./activity_tab.md) - Documentation for the activity summary tab
- [Analytics Service](../../../technical/analytics/index.md) - Details on workout analytics implementation
- [Progress Tracking](../progress_tracking.md) - Documentation for progress tracking methodology

View File

@ -0,0 +1,213 @@
# Settings Tab
**Last Updated:** 2025-04-02
**Status:** Implemented
**Related To:** [Profile Tab Overview](../profile_overview.md)
## Introduction
The Settings tab provides users with configuration options for their account and application preferences. It allows customization of the app appearance, functionality settings, and offers account management options including logout functionality.
## Features
| Feature | Status | Notes |
|---------|--------|-------|
| Account Settings | ✅ Implemented | Display of Nostr publishing status and local storage options |
| App Settings | ✅ Implemented | Theme toggle, notifications, and units selection |
| Dark Mode Toggle | ✅ Implemented | Switch between light and dark theme |
| Terms of Service | ✅ Implemented | Access to legal documents |
| Logout Functionality | ✅ Implemented | Option to log out of Nostr account |
| Authentication Detection | ✅ Implemented | Login prompt for unauthenticated users |
| Version Information | ✅ Implemented | Display of current app version |
## Implementation Details
The Settings tab is implemented in `app/(tabs)/profile/settings.tsx`. It integrates with the NDK authentication system for account management and the app's theme system for appearance customization.
### Account Settings Implementation
The tab displays account-related settings:
```jsx
{/* Account Settings */}
<Card className="mx-4 mt-4 mb-4">
<CardContent className="p-4">
<Text className="text-lg font-semibold mb-4">Account Settings</Text>
<View className="flex-row justify-between items-center py-2 border-b border-border">
<Text>Nostr Publishing</Text>
<Text className="text-muted-foreground">Public</Text>
</View>
<View className="flex-row justify-between items-center py-2 border-b border-border">
<Text>Local Storage</Text>
<Text className="text-muted-foreground">Enabled</Text>
</View>
<View className="flex-row justify-between items-center py-2">
<Text>Connected Relays</Text>
<Text className="text-muted-foreground">5</Text>
</View>
</CardContent>
</Card>
```
### App Settings Implementation
The tab provides application-specific settings like theme and notifications:
```jsx
{/* App Settings */}
<Card className="mx-4 mb-4">
<CardContent className="p-4">
<Text className="text-lg font-semibold mb-4">App Settings</Text>
<View className="flex-row justify-between items-center py-2 border-b border-border">
<Text>Dark Mode</Text>
<Switch
value={colorScheme === 'dark'}
onValueChange={toggleColorScheme}
trackColor={{ false: '#767577', true: theme.colors.primary }}
/>
</View>
<View className="flex-row justify-between items-center py-2 border-b border-border">
<Text>Notifications</Text>
<Switch
value={true}
trackColor={{ false: '#767577', true: theme.colors.primary }}
/>
</View>
<View className="flex-row justify-between items-center py-2">
<Text>Units</Text>
<Text className="text-muted-foreground">Metric (kg)</Text>
</View>
</CardContent>
</Card>
```
### Theme Integration
The Settings tab integrates with the app's theme system to toggle between light and dark modes:
```jsx
const { colorScheme, toggleColorScheme } = useColorScheme();
// Used in the Dark Mode toggle
<Switch
value={colorScheme === 'dark'}
onValueChange={toggleColorScheme}
trackColor={{ false: '#767577', true: theme.colors.primary }}
/>
```
### Terms of Service Modal
The tab provides access to the Terms of Service:
```jsx
<TouchableOpacity
className="flex-row justify-between items-center py-2"
onPress={() => setIsTermsModalVisible(true)}
>
<Text>Terms of Service</Text>
<View className="flex-row items-center">
<Text className="text-primary mr-1">View</Text>
<ChevronRight size={16} color={theme.colors.primary} />
</View>
</TouchableOpacity>
{/* Terms of Service Modal */}
<TermsOfServiceModal
visible={isTermsModalVisible}
onClose={() => setIsTermsModalVisible(false)}
/>
```
### Logout Implementation
The tab includes a logout button that uses the NDK authentication system:
```jsx
{/* Logout Button */}
<View className="mx-4 mt-4">
<Button
variant="destructive"
onPress={logout}
className="w-full"
>
<Text className="text-white">Logout</Text>
</Button>
</View>
```
## Technical Considerations
### Authentication Handling
Like other profile tabs, the Settings tab handles authentication with a consistent pattern:
```jsx
// Show different UI when not authenticated
if (!isAuthenticated) {
return <NostrProfileLogin message="Login with your Nostr private key to access settings." />;
}
```
### Theme Integration
The tab uses the app's theme system for consistent styling and for the dark mode toggle:
```jsx
const theme = useTheme() as CustomTheme;
const { colorScheme, toggleColorScheme } = useColorScheme();
```
### NDK Authentication
The tab integrates with NDK for authentication management:
```jsx
const { currentUser, isAuthenticated } = useNDKCurrentUser();
const { logout } = useNDKAuth();
```
## User Experience Flow
1. **Authentication Check**:
- If user is not authenticated, display NostrProfileLogin component
- If authenticated, proceed to display settings
2. **Settings Categories**:
- Account Settings: View and manage Nostr-related settings
- App Settings: Control appearance and behavior of the app
- About: Access app information and legal documents
3. **Theme Toggle**:
- Switch between light and dark mode with immediate visual feedback
- Theme preference is persisted between app sessions
4. **Terms of Service**:
- View legal documents in a modal overlay
- Close modal to return to settings
5. **Logout**:
- Log out of Nostr account
- Return to unauthenticated state across the app
## Future Enhancements
1. **Profile Editing**: Add dedicated profile editing screen
2. **Advanced Relay Management**: Enhanced UI for relay configuration
3. **Additional Units Options**: More measurement units (imperial/metric)
4. **Privacy Controls**: Granular privacy settings for social sharing
5. **Notification Management**: Per-feature notification settings
6. **Data Management**: Import/export and backup options
## Related Documentation
- [Profile Overview](../profile_overview.md) - General overview of the Profile tab
- [Authentication Patterns](../authentication_patterns.md) - Technical details about authentication implementation
- [Theme System](../../../technical/styling/theme_system.md) - Documentation for the app's theming system
- [Terms of Service](../../../legal/terms_of_service.md) - Legal documentation

294
docs/guides/coding_style.md Normal file
View File

@ -0,0 +1,294 @@
# POWR App Coding Style Guide
**Last Updated:** 2025-04-01
**Status:** Active
**Related To:** Development Standards, Best Practices
## Purpose
This guide establishes consistent coding standards for the POWR application codebase. It provides clear principles and specific guidelines to ensure readability, maintainability, and quality across the project. Following these guidelines helps all contributors work together effectively and reduces the cognitive load when reading and modifying code.
## Core Principles
### Consistency is King
Above all other principles, be consistent. If a file follows one convention, continue using that convention. Separate style changes from logic changes in different commits.
**Rationale**: Inconsistent naming or patterns (`Apple`, `a`, `fruit`, `redThing` for the same concept) make it difficult to understand relationships between components.
### Readability Above Efficiency
Prefer readable code over fewer lines of cryptic code.
**Rationale**: Code will be read many more times than it will be written, by different people—including yourself a year from now.
### All Code is Either Obviously Right, or Non-Obviously Wrong
Strive to make code obviously correct at first glance. It shouldn't require detailed study to decipher.
**Rationale**: When code is obviously right, it's probably right. The goal is to have suspicious code look suspiciously wrong and stick out. This happens when everything else is clear, and there's a tricky bit that needs work.
**Code Comments**: Comments indicate the code isn't self-explanatory through naming, structure, or organization. While not prohibited, strive to make code require almost no comments.
Good uses for comments:
- Explaining **why** the code is written a certain way
- Documenting details that can't be easily explained in code (algorithms, patterns)
- API interface documentation (keep comments close to code for easier maintenance)
### Boring is Best
Make your code the most boring version it could be.
**Rationale**: Production code should be maintainable, not clever. Reserve creativity for test cases with interesting constants and edge cases.
### Split Implementation from Interface
Always separate storage, presentation, and communication protocols.
**Rationale**: When layers are inappropriately tied together, changes may cause unexpected breakage in seemingly unrelated areas.
### Split "Policy" and "Mechanics"
Separate configuration/policy ("the why") from implementation/mechanics ("the how").
**Rationale**: This separation allows testing implementation independently from policy triggers, making features easier to toggle and canary.
**Corollaries**:
- Create separate functions for "doing" and for "choosing when to do"
- Create flags for all implementation features
## Documentation of Deficiencies
### `TODO` Comments
Use `TODO` comments liberally to document potential improvements:
Format: `// TODO[(Context)]: <Action> by/when <Deadline Condition>`
Components:
- **Context** - (Optional) Reference to issue tracker or documentation
- **Action** - Specific, actionable task with explanations as needed
- **Deadline Condition** - When the task should be completed
**Rationale**: `TODO` comments help readers understand what is missing or could be improved, making technical debt visible.
Good examples:
```typescript
// TODO: Replace certificate with staging version once we get letsencrypt to work.
// TODO(POWR-123): Replace old logic with new logic when out of experimental mode.
// TODO(NOSTR-456): Implement zap forwarding when NIP-57 is fully supported by relays.
```
Less effective examples (lacking deadline condition):
```typescript
// TODO: Add precompiling templates here if we need it.
// TODO: Remove use of bash.
```
Poor examples:
```typescript
// TODO: wtf?
// TODO: We shouldn't do this.
// TODO: err...
```
### `FIXME` Comments
Use `FIXME` comments as **stronger** `TODO`s that **MUST** be addressed before code submission.
Format: `// FIXME: <Action or note>`
**Rationale**: These act as development reminders for incomplete or problematic code that needs attention before merging.
Examples:
```typescript
// FIXME: Remove hack
// FIXME: Revert hardcoding of relay URL
// FIXME: Implement function
// FIXME: Refactor these usages across codebase
```
## Naming Conventions
### Semantic Variable Names
Names should reflect content and intent. Choose specific, descriptive names. Use only well-known abbreviations.
```typescript
// Bad
input = "123-4567";
dialPhoneNumber(input); // unclear whether this makes semantic sense
// Good
phoneNumber = "123-4567";
dialPhoneNumber(phoneNumber); // clearly intentional
// Bad
text = 1234;
address = "nostr:npub1/" + text; // why is text being added?
// Good
profileId = 1234;
address = "nostr:npub1/" + profileId; // clearly adding a profile ID
```
**Rationale**: Semantic names make bugs obvious and express intention without requiring comments.
### Include Units for Measurements
Always include units in variable names for clarity:
- Time intervals: `timeoutSec`, `timeoutMs`, `refreshIntervalHours`
- Timestamps: `startTimestamp` (use language-appropriate types internally)
- Distances: `lengthFt`, `lengthMeter`, `lengthCm`
- Computer storage: `diskMib` (1 Mebibyte = 1024 Kibibytes), `ramMb` (1 Megabyte = 1000 Kilobytes)
```typescript
// Bad
cost = disk * cents;
// Good
costCents = diskMib * 1024 * centsPerKilobyte;
```
**Rationale**: Including units prevents many common bugs related to unit confusion.
## Constants
### Assign Literals to Constants
Every string or numeric literal should be assigned to a constant or constant-like variable.
**Exceptions**: Identity-type zero/one values: `0`, `1`, `-1`, `""`
```typescript
// Bad
if (status === 4) { /* ... */ }
// Good
const STATUS_COMPLETED = 4;
if (status === STATUS_COMPLETED) { /* ... */ }
```
**Rationale**: Constants make code more readable and maintainable by centralizing values and their meanings.
## React & React Native Specifics
### Component Organization
Structure React components consistently:
1. Imports
2. Type definitions and interfaces
3. Constants
4. Component function
5. Helper functions
6. Styles (if using StyleSheet)
7. Export statement
### Props and State
- Use TypeScript interfaces for props and state types
- Default props should be handled with destructuring in function parameters
- Prefer functional components with hooks over class components
```typescript
interface ButtonProps {
title: string;
onPress: () => void;
color?: string;
}
const Button = ({ title, onPress, color = 'blue' }: ButtonProps) => {
// Component implementation
};
```
## Testing Guidelines
All core code guidelines apply to tests with these modifications:
### Repetitive Test Code Allowed
While DRY (Don't Repeat Yourself) is a good practice, clarity in tests takes precedence.
**Rationale**: Test readability is paramount. Sometimes repetition makes tests easier to understand.
### Small Test Cases
Create small, targeted test cases rather than large, complex ones.
**Rationale**: Small tests are easier to write, debug, and understand. Large tests often indicate design problems in the code being tested.
### Avoid Complex Logic in Tests
Keep test logic simple. If needed, create helper functions with their own tests.
**Rationale**: Complex test logic is more likely to contain bugs, defeating the purpose of testing.
### Descriptive Test Values
Use creative but clearly test-oriented values that don't look like production data.
```typescript
// Bad
login("abc", "badpassword"); // Are these values significant?
const testPubkey = "npub1abcdef12345..."; // Looks too "real"
// Good
const testUsername = "test-user-alpha";
const testPassword = "obvious-test-password!";
const testBadPassword = "wrong-password-for-testing";
const testPubkey = "npub_for_test_user_alpha";
login(testUsername, testPassword); // Expected success
login(testUsername, testBadPassword); // Expected failure
```
**Rationale**: When variable names are clear, test intent becomes obvious.
### Keep Related Logic Together
Group related test logic and conditions together for better readability.
```typescript
// Bad
initialBalance = 100;
deposit();
// ...many lines of test code...
withdraw();
expect(balance).toBe(167); // Why 167?
// Good
initialBalance = 100;
depositAmount = 100;
withdrawAmount = 33;
expectedBalance = 167; // Clear where 167 comes from
// ...test code...
expect(balance).toBe(expectedBalance);
```
**Rationale**: Grouping related elements makes tests more understandable and self-documenting.
## TypeScript Best Practices
### Type Safety
- Prioritize type safety; avoid using `any` unless absolutely necessary
- Use TypeScript's utility types (`Partial<T>`, `Pick<T>`, etc.) when appropriate
- Define explicit return types for functions, especially for public API functions
### Interfaces vs Types
- Prefer interfaces for public APIs that may be extended
- Use type aliases for unions, intersections, and types that won't be extended
### Enums
- Use string enums for better debugging and serialization
- Consider using const enums for performance when applicable
## Related Documentation
- [Documentation Standards](../project/documentation/standards.md)
- [Project Organization](../project/organization_plan.md)
- [Styling Guide](../technical/styling/styling_guide.md)

View File

@ -1,6 +1,6 @@
# Documentation Migration Mapping
**Last Updated:** 2025-03-26
**Last Updated:** 2025-04-01
**Status:** Active
**Related To:** Documentation Organization, Migration Process
@ -32,7 +32,7 @@ This document tracks the migration of documentation files from their original lo
| ✅ | docs/design/POWR Pack/POWR_Pack_Implementation_Plan.md | docs/features/powr_packs/implementation_plan.md | Technical content reorganized and expanded |
| ✅ | docs/design/Analysis/NDKSubscriptionAnalysis.md | docs/technical/ndk/subscription_analysis.md | Enhanced with code examples and best practices |
| ✅ | docs/design/Analysis/NDKandNip19.md | docs/technical/nostr/encoding_decoding.md | Complete guide with practical examples |
| ⏳ | docs/coding_style.md | docs/guides/coding_style.md | - |
| ✅ | docs/coding_style.md | docs/guides/coding_style.md | Enhanced with React/TypeScript specifics and standardized formatting |
## NDK Related Documents
@ -72,22 +72,22 @@ This document tracks the migration of documentation files from their original lo
| ✅ | docs/design/WorkoutTab/WorkoutDataFlowSpec.md | docs/features/workout/data_models.md | Enhanced with error handling examples |
| ⏳ | docs/design/WorkoutTab/WorkoutImplementationRoadmap.md | docs/features/workout/implementation_roadmap.md | - |
| ⏳ | docs/design/WorkoutTab/Summary.md | 🔀 | To be consolidated into workout_overview.md |
| | docs/design/WorkoutTab/HistoryTabEnhancementDesignDoc.md | docs/features/history/history_overview.md | - |
| ⏳ | docs/design/WorkoutHistory/MigrationGuide.md | docs/features/history/migration_guide.md | - |
| | docs/design/WorkoutTab/HistoryTabEnhancementDesignDoc.md | docs/features/history/history_overview.md | Enhanced with architecture diagrams and implementation status |
| ✅ | docs/design/WorkoutHistory/MigrationGuide.md | docs/features/history/migration_guide.md | Updated with code examples and troubleshooting tips |
## Profile Related Documents
| Status | Original Path | New Path | Notes |
|--------|--------------|----------|-------|
| ⏳ | docs/design/ProfileTab/ProfileTabEnhancementDesignDoc.md | docs/features/profile/profile_enhancement.md | - |
| ✅ | docs/design/ProfileTab/ProfileTabEnhancementDesignDoc.md | docs/features/profile/profile_overview.md | Complete migration with extensive restructuring. Created detailed tab-specific documentation, authentication patterns guide, and technical implementation details. |
## Library Related Documents
| Status | Original Path | New Path | Notes |
|--------|--------------|----------|-------|
| ⏳ | docs/design/library_tab.md | docs/features/library/library_overview.md | - |
| | docs/design/Templates/TemplateOrganization.md | docs/features/library/template_organization.md | - |
| ⏳ | docs/design/template-creation-design.md | 🔀 | To be consolidated into template_organization.md |
| ✅ | docs/design/library_tab.md | docs/features/library/library_overview.md | Reformatted with modern styling and enhanced with component architecture details |
| | docs/design/Templates/TemplateOrganization.md | docs/features/library/template_organization.md | Consolidated with template-creation-design.md and enhanced with implementation status details |
| ✅ | docs/design/template-creation-design.md | 🔀 | Consolidated into template_organization.md with proper archival reference |
## Technical Documents
@ -96,8 +96,8 @@ This document tracks the migration of documentation files from their original lo
| ✅ | docs/design/nostr-exercise-nip.md | docs/technical/nostr/exercise_nip.md | - |
| ⏳ | docs/design/database_architecture.md | docs/architecture/database_architecture.md | - |
| ⏳ | docs/design/database_implementation.md | docs/architecture/database_implementation.md | - |
| ⏳ | docs/design/cache-management.md | docs/technical/caching/cache_management.md | - |
| | docs/design/styling_design_doc.md | docs/technical/styling/styling_guide.md | - |
| ✅ | docs/design/cache-management.md | docs/technical/caching/cache_management.md | Enhanced with NDK analysis and unified architecture proposal |
| | docs/design/styling_design_doc.md | docs/technical/styling/styling_guide.md | Enhanced with cross-platform guidance and platform-specific considerations |
## Settings Related Documents
@ -122,19 +122,19 @@ This document tracks the migration of documentation files from their original lo
## Migration Progress
- **High Priority Documents**: 11/12 (92%)
- **High Priority Documents**: 12/12 (100%)
- **NDK Related Documents**: 3/4 (75%)
- **POWR Pack Related Documents**: 2/2 (100%)
- **Social Related Documents**: 5/7 (71%)
- **Workout Related Documents**: 4/8 (50%)
- **Profile Related Documents**: 0/1 (0%)
- **Library Related Documents**: 0/3 (0%)
- **Technical Documents**: 1/5 (20%)
- **Profile Related Documents**: 1/1 (100%)
- **Library Related Documents**: 3/3 (100%)
- **Technical Documents**: 3/5 (60%)
- **Settings Related Documents**: 0/1 (0%)
- **Testing Related Documents**: 0/2 (0%)
- **Other Documents**: 0/3 (0%)
**Overall Progress**: 26/48 (54%)
**Overall Progress**: 30/48 (62.5%)
## Related Documentation

View File

@ -0,0 +1,239 @@
# Cache Management in POWR App
**Last Updated:** 2025-04-01
**Status:** Active
**Related To:** Mobile Performance, Offline Support, NDK Integration
## Purpose
This document outlines the caching architecture in the POWR app, including current implementation, future direction, and strategies for optimizing offline functionality, reducing network usage, and enhancing performance.
## Goals
1. **Improve Offline Experience**: Allow users to access critical app features even when offline
2. **Reduce Network Usage**: Minimize data consumption by caching frequently accessed data
3. **Enhance Performance**: Speed up the app by reducing network requests
4. **Maintain Data Freshness**: Implement strategies to keep cached data up-to-date
## Current Implementation
The POWR app currently implements a fragmented caching system with multiple specialized services:
### 1. Profile Image Caching
**Status: Implemented**
The `ProfileImageCache` service downloads and caches profile images locally, providing offline access and reducing network usage.
```typescript
// Key features of ProfileImageCache
- Local storage of profile images in the app's cache directory
- Automatic fetching and caching of images when needed
- Age-based cache invalidation (24 hours by default)
- Integration with UserAvatar component for seamless usage
```
**Integration Points:**
- `UserAvatar` component uses the cache for all profile images
- `EnhancedSocialPost` component uses `UserAvatar` for profile images in the feed
- NDK initialization sets the NDK instance in the ProfileImageCache service
### 2. Publication Queue Service
**Status: Implemented**
The `PublicationQueueService` allows events to be created and queued when offline, then published when connectivity is restored.
```typescript
// Key features of PublicationQueueService
- Persistent storage of unpublished events
- Automatic retry mechanism when connectivity is restored
- Priority-based publishing
- Status tracking for queued events
```
**Integration Points:**
- Social posting
- Workout publishing
- Template sharing
### 3. Social Feed Caching
**Status: Implemented**
The `SocialFeedCache` service caches social feed events locally, allowing users to browse their feed even when offline.
```typescript
// Key features of SocialFeedCache
- SQLite-based storage of feed events
- Feed-specific caching (following, POWR, global)
- Time-based pagination support
- Automatic cleanup of old cached events
```
**Integration Points:**
- `useSocialFeed` hook uses the cache when offline
- `SocialFeedService` manages the cache and provides a unified API
- Feed components display cached content with offline indicators
### 4. Event Cache
**Status: Implemented**
The `EventCache` service provides a lower-level caching mechanism for storing and retrieving Nostr events.
```typescript
// Key features of EventCache
- SQLite-based storage of Nostr events
- Tag-based indexing and retrieval
- Transaction support for batch operations
```
**Integration Points:**
- Used by SocialFeedCache to store individual events
- Referenced by other services needing event data
### 5. Other Domain-Specific Caches
The app also includes several other caching services for specific domains:
- **Workout History Caching**: Stores workout records for offline access
- **Exercise Library Caching**: Maintains a local copy of the exercise database
- **Template Caching**: Provides offline access to workout templates
- **Contact List Caching**: Stores user's contact list for offline use
## Implementation Challenges
Our current implementation has several challenges:
1. **Fragmentation**: Multiple cache services with overlapping functionality
2. **Code Duplication**: Similar patterns reimplemented across services
3. **Inconsistent Strategies**: Different invalidation, error handling, and persistence approaches
4. **Maintenance Burden**: Changes require updates to multiple services
5. **Dependency Management**: Multiple NDK integration points
## NDK Mobile Cache Integration
The NDK Mobile library provides robust built-in caching capabilities through the `NDKCacheAdapterSqlite` class, which we are currently underutilizing. Key features include:
- **Unified SQLite Database Management**: Schema migrations, transaction support
- **Event Caching**: Storage/retrieval of events with tag indexing
- **Profile Caching**: LRU-based profile data caching
- **Unpublished Event Management**: Storing and retrying failed publications
- **Query Interface**: Sophisticated filtering of cached events
- **Write Buffering**: Performance optimizations for database writes
```typescript
// NDK initialization with SQLite cache adapter
const cacheAdapter = new NDKCacheAdapterSqlite('powr', 1000);
await cacheAdapter.initialize();
const ndk = new NDK({
cacheAdapter,
explicitRelayUrls: DEFAULT_RELAYS,
enableOutboxModel: true,
autoConnectUserRelays: true,
clientName: 'powr',
});
```
## Future Direction: Centralized Cache Architecture
To address the challenges of our current fragmented approach, we plan to evolve toward a unified caching architecture leveraging NDK Mobile's built-in capabilities.
### Proposed Architecture
```mermaid
graph TD
A[Application] --> B[CacheManager]
B --> C[NDKCacheAdapterSqlite]
B --> D[DomainServiceLayer]
D --> E[ProfileService]
D --> F[FeedService]
D --> G[WorkoutService]
D --> H[TemplateService]
D --> I[ExerciseService]
```
### Key Components
1. **CacheManager**: Centralized facade for all caching operations
- Initializes and configures NDK cache adapter
- Provides unified API for common operations
- Manages NDK integration
2. **Domain Services**: Specialized services that focus on business logic
- Use NDK cache for storage/retrieval
- Add domain-specific functionality
- Handle presentation concerns
3. **Media Cache**: For binary data not handled by NDK (images, etc.)
- File system based storage
- LRU eviction policies
- Size limitations and cleanup
### Implementation Roadmap
1. **Phase 1**: Enhance NDK initialization and configuration
- Configure NDK cache adapter with appropriate settings
- Create CacheManager facade
2. **Phase 2**: Refactor existing services to use NDK cache
- Start with ProfileImageCache → ProfileService
- Update SocialFeedCache to leverage NDK subscription caching
3. **Phase 3**: Add centralized monitoring and management
- Cache size monitoring
- Performance metrics
- Coordinated invalidation
4. **Phase 4**: Implement advanced features
- Selective syncing
- Background refreshing
- Cross-device synchronization
## User Experience Considerations
### Offline Indicators
The app provides clear visual indicators when operating in offline mode:
- Global offline indicator in the header
- Feed-specific offline state components
- Disabled actions that require connectivity
- Queued action indicators
### Transparent Sync
Synchronization happens transparently in the background:
- Automatic publishing of queued events when connectivity is restored
- Progressive loading of fresh content when coming online
- Prioritized sync for critical data
### Data Freshness
The app balances offline availability with data freshness:
- Age indicators for cached content
- Pull-to-refresh to force update when online
- Background refresh of frequently accessed data
## Migration Strategy
Transitioning from our current implementation to the centralized approach will be done incrementally:
1. First, refactor NDK initialization to properly configure the cache adapter
2. Create the CacheManager facade while maintaining compatibility with existing services
3. Gradually update each service to use CacheManager instead of direct database access
4. Consolidate duplicate functionality across services
## Conclusion
The cache management system in POWR is critical for delivering a responsive, offline-capable experience. While our current implementation successfully provides these capabilities, moving toward a more centralized approach based on NDK's built-in features will reduce complexity, improve maintainability, and ensure consistent behavior across the app.
## Related Documentation
- [NDK Comprehensive Guide](../ndk/comprehensive_guide.md) - Overview of NDK functionality
- [Offline Queue](../nostr/offline_queue.md) - Publication queueing for offline support
- [Social Feed Cache Implementation](../../features/social/cache_implementation.md) - Details on feed caching

View File

@ -0,0 +1,706 @@
# POWR App Styling Guide
**Last Updated:** 2025-04-01
**Status:** Active
**Related To:** Design System, Component Architecture, UI/UX, Cross-Platform Development
## Purpose
This document outlines the styling principles, component usage patterns, and theming implementation for the POWR fitness app. Following these guidelines ensures a consistent look and feel across the application, facilitates cross-platform compatibility, and enhances the overall user experience.
## Cross-Platform Approach
The POWR app is designed to run on both iOS and Android platforms, which present unique challenges for UI implementation. Our approach prioritizes:
1. **Platform Consistency**: Maintaining a consistent look and feel across platforms
2. **Platform Adaptation**: Respecting platform-specific UX patterns where appropriate
3. **Graceful Fallbacks**: Implementing fallbacks for features not available on all platforms
4. **Testing on Both Platforms**: All UI changes must be verified on both iOS and Android
## Theme System Architecture
The POWR app uses a flexible theme system built with React Native, Tailwind CSS, and shadcn/ui components. The theming infrastructure supports both light and dark modes, with dynamic color adjustments for different UI states.
### Theme File Organization
```
lib/theme/
├── index.ts - Main theme export
├── colors.ts - Color definitions
├── constants.ts - Theme constants
├── iconUtils.ts - Icon styling utilities
└── useColorScheme.tsx - Theme mode selection hook
```
### Theme Implementation Strategy
The application uses:
- Tailwind classes for general styling with `nativewind`
- Specialized hooks for cross-platform compatibility (`useIconColor`, etc.)
- `shadcn/ui` component library for consistent UI elements
## Color System
All colors should be accessed through the theme system rather than using hardcoded values. Never use direct color codes in components.
### Color Imports
```typescript
// Import theme utilities
import { useTheme } from '@/lib/theme';
import { useIconColor } from '@/lib/theme/iconUtils';
import { FIXED_COLORS } from '@/lib/theme/colors';
```
### Color Variants
The theme includes semantic color variants for different UI elements:
- `primary` - Brand color, used for main interactive elements (purple)
- `secondary` - Supporting UI elements
- `muted` - Subdued elements, backgrounds, disabled states
- `accent` - Highlights and accents
- `destructive` - Error states, deletion actions (red)
- `success` - Confirmation, completion states (green)
- `warning` - Caution states (yellow/orange)
### Accessing Colors
Always access colors through Tailwind classes:
```jsx
// Good - uses theme system
<View className="bg-primary rounded-md p-4">
<Text className="text-primary-foreground font-medium">
Hello World
</Text>
</View>
// Bad - hardcoded values that won't respond to theme changes
<View style={{ backgroundColor: '#8B5CF6', borderRadius: 8, padding: 16 }}>
<Text style={{ color: '#FFFFFF', fontWeight: 500 }}>
Hello World
</Text>
</View>
```
## Icon Styling
Icons must use the icon utility functions to ensure visibility across platforms. Different platforms may require different stroke widths, colors, and other properties.
### Icon Usage
```typescript
import { useIconColor } from '@/lib/theme/iconUtils';
import { Play, Star, Trash2 } from 'lucide-react-native';
// Inside your functional component
function MyComponent() {
const { getIconProps, getIconColor } = useIconColor();
return (
<View>
{/* Primary action icon */}
<Play {...getIconProps('primary')} size={20} />
{/* Destructive action icon */}
<Trash2 {...getIconProps('destructive')} size={20} />
{/* Icon with conditional fill */}
<Star
{...getIconProps(isFavorite ? 'primary' : 'muted')}
fill={isFavorite ? getIconColor('primary') : "none"}
size={20}
/>
</View>
);
}
```
### Icon Variants
- `primary` - For main actions and interactive elements
- `muted` - For secondary or less important actions
- `destructive` - For delete/remove actions
- `success` - For confirmation/complete actions
- `warning` - For caution indicators
### Platform-Specific Icon Considerations
- **Android**:
- Icons often appear thinner and less visible on Android
- Always use `strokeWidth={2}` or higher for better visibility on Android
- Minimum recommended icon size is 24px for Android (vs. 20px for iOS)
- Use the `getIconProps` function which handles these platform differences automatically
- **iOS**:
- Icons generally appear as expected with default stroke width
- iOS has better support for gradients and complex icon styles
## Button Styling
Use the standard `Button` component with appropriate variants to maintain a consistent look and feel.
### Button Variants
```jsx
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
// Primary button
<Button variant="default" className="w-full">
<Text className="text-primary-foreground">Primary Action</Text>
</Button>
// Destructive button
<Button variant="destructive" className="w-full">
<Text className="text-destructive-foreground">Delete</Text>
</Button>
// Outline button
<Button variant="outline" className="w-full">
<Text>Secondary Action</Text>
</Button>
// Ghost button (minimal visual impact)
<Button variant="ghost" className="w-full">
<Text>Subtle Action</Text>
</Button>
// Link button
<Button variant="link" className="w-full">
<Text className="text-primary underline">Learn More</Text>
</Button>
```
### Button States
Buttons handle the following states automatically through the theme system:
- Default
- Hover/active (handled differently on mobile and web)
- Disabled
- Loading
```jsx
// Disabled button
<Button variant="default" disabled className="w-full">
<Text className="text-primary-foreground">Unavailable</Text>
</Button>
// Loading button
<Button variant="default" isLoading className="w-full">
<Text className="text-primary-foreground">Loading...</Text>
</Button>
```
### Platform-Specific Button Considerations
- **Android**:
- Android buttons may need additional padding to match iOS visual weight
- Use `android:elevation` or equivalent shadow values for proper elevation on Android
- Ripple effects require additional configuration to work properly
- Consider using `TouchableNativeFeedback` for Android-specific feedback on buttons
- **iOS**:
- iOS buttons typically have more subtle feedback effects
- Shadow properties work more predictably on iOS
## Header Component
Use the `Header` component consistently across all screens for navigation and context.
### Header Configuration
```jsx
import { Header } from '@/components/Header';
// Standard header with title
<Header title="Screen Title" showNotifications={true} />
// Header with logo
<Header useLogo={true} showNotifications={true} />
// Header with custom right element
<Header
title="Screen Title"
rightElement={<YourCustomElement />}
/>
// Header with back button
<Header
title="Details"
showBackButton={true}
onBack={() => navigation.goBack()}
/>
```
### Platform-Specific Header Considerations
- **Android**:
- Android status bar customization requires `StatusBar` component with platform checks
- Text in headers may render differently, requiring platform-specific adjustments
- Back button styling differs between platforms - use the Header component's built-in options
- Shadow effects need to be handled differently on Android (elevation vs shadowProps)
- **iOS**:
- iOS has native support for large titles and collapsible headers
- Safe area insets are critical for proper header positioning on iOS
- Status bar content color changes (dark/light) may need to be explicitly specified
## Text Styling
Use the `Text` component with appropriate Tailwind classes for typography. This ensures the correct font styles across platforms.
### Text Hierarchy
```jsx
import { Text } from '@/components/ui/text';
// Page title
<Text className="text-2xl font-bold text-foreground">
Page Title
</Text>
// Section heading
<Text className="text-xl font-semibold text-foreground mb-2">
Section Heading
</Text>
// Subsection heading
<Text className="text-lg font-medium text-foreground mb-1">
Subsection Heading
</Text>
// Body text
<Text className="text-base text-foreground">
Regular body text for primary content.
</Text>
// Secondary text
<Text className="text-sm text-muted-foreground">
Secondary information or supporting text.
</Text>
// Small text / captions
<Text className="text-xs text-muted-foreground">
Caption text, timestamps, etc.
</Text>
```
### Platform-Specific Text Considerations
- **Android**:
- Font rendering is different on Android - text may appear smaller or thinner
- Android requires explicit `fontFamily` specification for custom fonts
- Line height calculations differ between platforms - may need adjustments
- Some text styling properties like `letterSpacing` work differently on Android
- Use `includeFontPadding: false` on Android to fix inconsistent text height
- **iOS**:
- Dynamic Type (iOS accessibility feature) should be supported
- Certain text styles like small caps require different implementations
- Font weights map differently between platforms (400 on iOS may not look the same as 400 on Android)
## Card Components
Use the Card component family for content blocks throughout the app.
### Basic Card
```jsx
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
<Card className="mx-4 mb-4">
<CardHeader>
<CardTitle>
<Text className="text-lg font-semibold">Card Title</Text>
</CardTitle>
</CardHeader>
<CardContent className="p-4">
<Text className="text-foreground">
Card content goes here.
</Text>
</CardContent>
<CardFooter className="flex-row justify-between px-4 py-2">
<Button variant="ghost" size="sm">
<Text>Cancel</Text>
</Button>
<Button variant="default" size="sm">
<Text className="text-primary-foreground">Confirm</Text>
</Button>
</CardFooter>
</Card>
```
### Interactive Card
For cards that function as buttons:
```jsx
<Pressable onPress={handlePress}>
<Card className="mx-4 mb-4 border-l-4 border-l-primary">
<CardContent className="p-4">
<Text className="text-foreground font-medium">
Interactive Card
</Text>
<Text className="text-sm text-muted-foreground mt-1">
Tap to interact
</Text>
</CardContent>
</Card>
</Pressable>
```
### Platform-Specific Card Considerations
- **Android**:
- Use `elevation` for shadows on Android instead of `shadow-*` classes
- Border radius may render differently on older Android versions
- Ripple effects for interactive cards need platform-specific configuration
- Border styles may appear differently on Android
- **iOS**:
- Shadow properties work more predictably on iOS
- Cards with dynamic height may need additional configuration for iOS
## Dialog/Alert Styling
Center buttons in dialogs for better usability and maintain consistent styling for these components.
### Alert Dialog Example
```jsx
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
<AlertDialog>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Text className="text-lg font-semibold text-foreground">
Confirm Action
</Text>
</AlertDialogTitle>
<AlertDialogDescription>
<Text className="text-muted-foreground">
Are you sure you want to continue? This action cannot be undone.
</Text>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-row justify-center gap-3 mt-4">
<AlertDialogCancel>
<Text>Cancel</Text>
</AlertDialogCancel>
<AlertDialogAction className="bg-destructive">
<Text className="text-destructive-foreground">Confirm</Text>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
```
### Platform-Specific Dialog Considerations
- **Android**:
- Android dialogs traditionally have buttons aligned to the right
- Back button behavior needs special handling on Android dialogs
- Touch outside to dismiss works differently on Android
- Dialog animations differ between platforms
- Material Design guidelines suggest different spacing and typography than iOS
- **iOS**:
- iOS dialogs typically have vertically stacked buttons
- Safe area insets must be respected on full-screen iOS sheets
- iOS has specific swipe gestures for sheet dismissal
## Form Elements
Style form elements consistently for a coherent user experience.
### Form Field Examples
```jsx
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
// Text input with label
<View className="mb-4">
<Label htmlFor="name" className="mb-1.5">
<Text className="text-sm font-medium">Name</Text>
</Label>
<Input
id="name"
placeholder="Enter your name"
value={name}
onChangeText={setName}
className="bg-background"
/>
{error && (
<Text className="text-xs text-destructive mt-1">
{error}
</Text>
)}
</View>
// Select input with label
<View className="mb-4">
<Label htmlFor="category" className="mb-1.5">
<Text className="text-sm font-medium">Category</Text>
</Label>
<Select
id="category"
value={category}
onValueChange={setCategory}
className="bg-background"
>
{categories.map(cat => (
<SelectItem key={cat.id} label={cat.name} value={cat.id} />
))}
</Select>
</View>
```
### Platform-Specific Form Element Considerations
- **Android**:
- Input fields may need additional padding or height adjustments
- Text field focus appearance differs significantly (Material Design vs. iOS)
- Android requires explicit configuration for soft keyboard behavior
- Date/time pickers have completely different UIs between platforms
- Dropdown selects appear and behave differently on Android
- **iOS**:
- Form elements typically have a lighter visual style
- iOS has specific picker components that are different from Android
- Keyboard accessories are common on iOS but less so on Android
- Text selection handles and behavior differ between platforms
## Common Cross-Platform Issues and Solutions
### Shadow Implementation
**Issue**: Shadow styling works differently between iOS and Android.
**Solution**:
```jsx
// Cross-platform shadow solution
<View
className="bg-card rounded-lg p-4"
style={Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
})}
>
<Text>Content with consistent shadow across platforms</Text>
</View>
```
### Icon Rendering
**Issue**: Icons appear properly on iOS but are barely visible on Android.
**Solution**:
```jsx
// Always use the icon utility
import { useIconColor } from '@/lib/theme/iconUtils';
function MyComponent() {
const { getIconProps } = useIconColor();
return (
<Icon
{...getIconProps('primary')}
size={24} // Slightly larger for Android
strokeWidth={Platform.OS === 'android' ? 2 : 1.5} // Explicit adjustment
/>
);
}
```
### Text Alignment
**Issue**: Text alignment and truncation behaves differently across platforms.
**Solution**:
```jsx
// Text alignment helper component
function AlignedText({ children, ...props }) {
return (
<Text
{...props}
style={[
props.style,
Platform.OS === 'android' ? { includeFontPadding: false } : null,
Platform.OS === 'android' ? { lineHeight: 24 } : null,
]}
>
{children}
</Text>
);
}
```
### Touchable Feedback
**Issue**: Touch feedback effects differ between platforms.
**Solution**:
```jsx
// Platform-specific touchable
function AppTouchable({ children, onPress, ...props }) {
if (Platform.OS === 'android') {
return (
<TouchableNativeFeedback
onPress={onPress}
background={TouchableNativeFeedback.Ripple('#rgba(0,0,0,0.1)', false)}
{...props}
>
<View>{children}</View>
</TouchableNativeFeedback>
);
}
return (
<TouchableOpacity onPress={onPress} activeOpacity={0.7} {...props}>
{children}
</TouchableOpacity>
);
}
```
### Keyboard Handling
**Issue**: Keyboard behavior and avoidance differs between platforms.
**Solution**:
```jsx
// Keyboard handling helper
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 0}
style={{ flex: 1 }}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={{ flex: 1 }}>
{/* Form content */}
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
```
## Platform-Specific Component Extensions
For cases where significant platform differences exist, create platform-specific component extensions:
### Example: Platform-Specific DatePicker
```jsx
// DatePickerWrapper.jsx
import { DatePicker } from './DatePicker.ios';
import { DatePicker } from './DatePicker.android';
export const DatePickerWrapper = (props) => {
const Component = Platform.select({
ios: DatePickerIOS,
android: DatePickerAndroid,
});
return <Component {...props} />;
};
```
## Best Practices for Cross-Platform Development
1. **Always test on both platforms** before considering a feature complete
2. **Use platform detection judiciously** - prefer cross-platform solutions where possible
3. **Create abstraction layers** for significantly different platform components
4. **Leverage UI component libraries** that handle cross-platform differences (like UI Kitten, React Native Paper)
5. **Document platform-specific quirks** that you encounter for future reference
6. **Create utility functions** for common platform-specific adjustments
7. **Use feature detection** instead of platform detection when possible
8. **Consider native device capabilities** like haptic feedback that may not exist on all devices
## Best Practices for POWR App Styling
1. **Never use hardcoded colors** - Always use theme variables through Tailwind classes
2. **Always use `getIconProps` for icons** - Ensures visibility on both iOS and Android
3. **Use semantic variants** - Choose button and icon variants based on their purpose
4. **Maintain consistent spacing** - Use Tailwind spacing classes (p-4, m-2, etc.)
5. **Test both platforms** - Verify UI rendering on both iOS and Android
6. **Use platform-specific overrides** when necessary
7. **Document platform-specific behavior** in component comments
## Troubleshooting Common Issues
### Icons Not Visible on Android
Problem: Icons don't appear or are difficult to see on Android devices.
Solution:
- Ensure you're using `getIconProps()` instead of direct styling
- Add `strokeWidth={2}` to increase visibility
- Verify that icon size is appropriate (min 24px recommended for Android)
- Check that the icon color has sufficient contrast with the background
### Inconsistent Colors
Problem: Colors appear inconsistent between components or platforms.
Solution:
- Verify you're using Tailwind classes (text-primary vs #8B5CF6)
- Check that the correct variant is being used for the component
- Ensure components are properly wrapped with theme provider
- Examine component hierarchy for style inheritance issues
### Text Truncation Issues
Problem: Text doesn't truncate properly or layout breaks with long content.
Solution:
- Add `numberOfLines={1}` for single-line truncation
- Use `ellipsizeMode="tail"` for text truncation
- Wrap Text components with a fixed-width container
- Consider using a more robust solution for responsive text
- Apply platform-specific text style adjustments
### Shadow and Elevation
Problem: Shadows appear on iOS but not on Android, or look inconsistent.
Solution:
- Use platform-specific shadow implementation (see example above)
- For Android, use `elevation` property
- For iOS, use `shadowColor`, `shadowOffset`, `shadowOpacity`, and `shadowRadius`
- Test shadow values on different Android versions
### Keyboard Issues
Problem: Keyboard covers input fields or doesn't dismiss properly.
Solution:
- Use KeyboardAvoidingView with platform-specific behavior
- Implement Keyboard.dismiss on background taps
- Add ScrollView for forms to ensure all fields are accessible
- Consider using a keyboard manager library for complex forms
## Related Documentation
- [Coding Style Guide](../../guides/coding_style.md) - General coding patterns and practices
- [Component Architecture](../../architecture/index.md) - How components are organized
- [Accessibility Guidelines](../../guides/accessibility.md) - Making the app accessible to all users

29
utils/avatar.ts Normal file
View File

@ -0,0 +1,29 @@
/**
* Utility functions for avatar handling
*/
/**
* Generates a consistent Robohash URL for a user
* @param pubkey User's public key or npub
* @param fallback Fallback string to use if no pubkey is provided
* @param size Size of the image to request (default: 150x150)
* @returns URL string for the Robohash avatar
*/
export const getRobohashUrl = (pubkey?: string, fallback: string = 'anonymous', size: string = '150x150'): string => {
// Use pubkey if available, otherwise use fallback string
const seed = pubkey || fallback;
// Always use set3 for consistent robot style across the app
return `https://robohash.org/${seed}?set=set3&size=${size}`;
};
/**
* Gets a consistent seed for user avatar generation
* Useful when we need the seed but not the full URL
* @param pubkey User's public key or npub
* @param fallback Fallback string to use if no pubkey is provided
* @returns A string to use as a seed for avatar generation
*/
export const getAvatarSeed = (pubkey?: string, fallback: string = 'anonymous'): string => {
return pubkey || fallback;
};