mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-19 10:51:19 +00:00

UI enhancements and production optimizations: - Added production flag in theme constants - Hid development-only Programs tab in production builds - Removed debug UI elements and debug logs from social feed - Fixed workout completion flow UI issues (input styling, borders, spacing) - Made improvements to exercise name resolution in feeds - Standardized form element spacing and styling - Enhanced multiline inputs with consistent design system Note: Exercise name resolution in social feed still needs additional work
295 lines
10 KiB
TypeScript
295 lines
10 KiB
TypeScript
/**
|
|
* useExerciseNames - React Query hook to resolve exercise names from various formats
|
|
*
|
|
* This hook provides a standardized way to resolve exercise names from:
|
|
* 1. Local database IDs (format: "local:id-hash")
|
|
* 2. Global identifiers (format: "kind:pubkey:id")
|
|
* 3. UUIDs and other unique identifiers
|
|
*
|
|
* It implements a multi-step resolution process:
|
|
* 1. First tries using any existing name from the exercise object
|
|
* 2. Then attempts to parse the ID using pattern recognition
|
|
* 3. Finally falls back to database lookup if possible
|
|
*/
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { QUERY_KEYS } from '../queryKeys';
|
|
import { NDKEvent } from '@nostr-dev-kit/ndk-mobile';
|
|
import { createLogger } from '@/lib/utils/logger';
|
|
import { useNDK } from '@/lib/hooks/useNDK';
|
|
import {
|
|
ParsedWorkoutRecord,
|
|
ParsedTemplateExercise,
|
|
parseWorkoutRecord,
|
|
extractExerciseName,
|
|
lookupExerciseTitle
|
|
} from '@/types/nostr-workout';
|
|
|
|
// Create a module-specific logger that follows the app's logging system
|
|
const logger = createLogger('ExerciseNames');
|
|
|
|
// Type alias for the exercise resolution format
|
|
export type ExerciseNamesMap = Record<string, string>;
|
|
|
|
/**
|
|
* Hook for resolving exercise names from a Nostr workout event
|
|
*/
|
|
export function useExerciseNamesFromEvent(event: NDKEvent | null | undefined) {
|
|
const { ndk } = useNDK();
|
|
|
|
return useQuery({
|
|
queryKey: QUERY_KEYS.exercises.namesByEvent(event?.id || ''),
|
|
queryFn: async (): Promise<ExerciseNamesMap> => {
|
|
if (!event) return {};
|
|
|
|
try {
|
|
// Parse the workout record
|
|
const workout = parseWorkoutRecord(event);
|
|
return resolveExerciseNames(workout.exercises, ndk);
|
|
} catch (error) {
|
|
logger.error('Error resolving exercise names from event:', error);
|
|
return {};
|
|
}
|
|
},
|
|
// Cache for 15 minutes - exercise names don't change often
|
|
staleTime: 15 * 60 * 1000,
|
|
// Don't refetch on window focus - this data is stable
|
|
refetchOnWindowFocus: false,
|
|
// Enable only if we have an event
|
|
enabled: !!event,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook for resolving exercise names from an already parsed workout record
|
|
*/
|
|
export function useExerciseNames(workout: ParsedWorkoutRecord | null | undefined) {
|
|
const { ndk } = useNDK();
|
|
|
|
return useQuery({
|
|
queryKey: QUERY_KEYS.exercises.namesByWorkout(workout?.id || ''),
|
|
queryFn: async (): Promise<ExerciseNamesMap> => {
|
|
if (!workout || !workout.exercises) return {};
|
|
|
|
try {
|
|
return resolveExerciseNames(workout.exercises, ndk);
|
|
} catch (error) {
|
|
logger.error('Error resolving exercise names from workout:', error);
|
|
return {};
|
|
}
|
|
},
|
|
// Cache for 15 minutes - exercise names don't change often
|
|
staleTime: 15 * 60 * 1000,
|
|
// Don't refetch on window focus - this data is stable
|
|
refetchOnWindowFocus: false,
|
|
// Enable only if we have a workout with exercises
|
|
enabled: !!(workout?.exercises && workout.exercises.length > 0),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook for resolving exercise names from template exercises
|
|
*
|
|
* Enhanced to use the same comprehensive resolution strategy
|
|
* as regular exercise name resolution
|
|
*/
|
|
export function useTemplateExerciseNames(
|
|
templateId: string,
|
|
exercises: ParsedTemplateExercise[] | null | undefined
|
|
) {
|
|
const { ndk } = useNDK();
|
|
|
|
return useQuery({
|
|
queryKey: [...QUERY_KEYS.exercises.all, 'templateExercises', templateId],
|
|
queryFn: async (): Promise<ExerciseNamesMap> => {
|
|
if (!exercises || exercises.length === 0) return {};
|
|
|
|
// Convert template exercises to format compatible with resolveExerciseNames
|
|
const adaptedExercises = exercises.map(exercise => ({
|
|
id: exercise.reference,
|
|
name: exercise.name
|
|
}));
|
|
|
|
// Use the same resolution logic as for workout exercises
|
|
return resolveExerciseNames(adaptedExercises, ndk);
|
|
},
|
|
// Cache for 15 minutes
|
|
staleTime: 15 * 60 * 1000,
|
|
refetchOnWindowFocus: false,
|
|
enabled: !!(exercises && exercises.length > 0),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper function to resolve exercise names from an array of exercises
|
|
*/
|
|
/**
|
|
* Enhanced version of resolveExerciseNames that implements multiple resolution strategies
|
|
* to ensure we get readable exercise names in all contexts
|
|
*
|
|
* @param exercises Array of exercise objects to resolve names for
|
|
* @param ndk Optional NDK instance for direct event fetching
|
|
* @returns Map of exercise IDs to display names
|
|
*/
|
|
async function resolveExerciseNames(exercises: any[], ndk?: any): Promise<ExerciseNamesMap> {
|
|
if (!exercises || exercises.length === 0) return {};
|
|
|
|
logger.debug(`Resolving names for ${exercises.length} exercises`);
|
|
if (exercises[0]) {
|
|
logger.debug(`Exercise data sample:`, exercises[0]);
|
|
}
|
|
|
|
const names: ExerciseNamesMap = {};
|
|
|
|
// RESOLUTION STRATEGY 0: Use NDK to fetch exercise events directly
|
|
if (ndk) {
|
|
// Get all exercise IDs that might be Nostr references (kind 33401)
|
|
const nostrRefs = exercises
|
|
.map(ex => {
|
|
const id = typeof ex === 'string' ? ex :
|
|
'id' in ex ? ex.id :
|
|
'reference' in ex ? ex.reference :
|
|
'exerciseId' in ex ? ex.exerciseId : null;
|
|
|
|
if (!id) return null;
|
|
|
|
// Check for explicit Nostr kind:pubkey:id format
|
|
if (id.includes(':')) {
|
|
const parts = id.split(':');
|
|
if (parts[0] === '33401') {
|
|
return {
|
|
exerciseId: id,
|
|
eventId: parts.length > 2 ? parts[2] : parts[parts.length - 1]
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
})
|
|
.filter(Boolean);
|
|
|
|
// If we have Nostr references, fetch them all at once
|
|
if (nostrRefs.length > 0) {
|
|
logger.debug(`Fetching ${nostrRefs.length} exercise events via NDK`);
|
|
try {
|
|
const eventIds = nostrRefs.map(ref => ref!.eventId);
|
|
const events = await ndk.fetchEvents({ ids: eventIds });
|
|
|
|
// Process each event to extract title
|
|
events.forEach((event: any) => {
|
|
// Find the title tag in the event
|
|
const titleTag = event.tags.find((tag: any[]) => tag[0] === 'title');
|
|
if (titleTag && titleTag.length > 1) {
|
|
// Find which exercise this corresponds to
|
|
const matchingRef = nostrRefs.find(ref => ref!.eventId === event.id);
|
|
if (matchingRef) {
|
|
names[matchingRef.exerciseId] = titleTag[1];
|
|
logger.debug(`Found title for ${matchingRef.exerciseId}: ${titleTag[1]}`);
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error fetching exercise events:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process each exercise in parallel for better performance
|
|
await Promise.all(exercises.map(async (exercise, index) => {
|
|
// Enhanced ID extraction with more formats supported
|
|
const exerciseId = typeof exercise === 'string' ? exercise :
|
|
'id' in exercise ? exercise.id :
|
|
'reference' in exercise ? exercise.reference :
|
|
'exerciseId' in exercise ? exercise.exerciseId :
|
|
`unknown-${index}`;
|
|
|
|
// For debugging the exact format of IDs we're receiving
|
|
logger.debug(`Processing exercise with ID: "${exerciseId}" (${typeof exerciseId})`);
|
|
|
|
// RESOLUTION STRATEGY 1: Check for meaningful name in the exercise object
|
|
if (exercise.name && exercise.name !== 'Exercise' && exercise.name !== 'Unknown Exercise') {
|
|
logger.debug(`Found exercise name in object: ${exercise.name}`);
|
|
names[exerciseId] = exercise.name;
|
|
return;
|
|
}
|
|
|
|
// RESOLUTION STRATEGY 2: Handle POWR's specific ID format, with or without local: prefix
|
|
let idToProcess = exerciseId;
|
|
let localPrefix = false;
|
|
|
|
// Check for and handle local: prefix
|
|
if (exerciseId.startsWith('local:')) {
|
|
idToProcess = exerciseId.substring(6);
|
|
localPrefix = true;
|
|
logger.debug(`Stripped local: prefix, processing ID: ${idToProcess}`);
|
|
}
|
|
|
|
// Check for POWR specific format
|
|
const powrFormatMatch = idToProcess.match(/^(m[a-z0-9]{7}-[a-z0-9]{10})$/i);
|
|
if (powrFormatMatch) {
|
|
const idWithoutPrefix = powrFormatMatch[1];
|
|
const betterName = `Exercise ${idWithoutPrefix.substring(1, 5).toUpperCase()}`;
|
|
logger.debug(`Created better name for POWR format ID: ${betterName}`);
|
|
names[exerciseId] = betterName;
|
|
|
|
// Still try database lookup for a proper name
|
|
try {
|
|
// Try different variations of the ID for lookup
|
|
const idVariations = [
|
|
idToProcess,
|
|
// If we already removed the local: prefix, don't need to try again
|
|
localPrefix ? [] : (exerciseId.startsWith('local:') ? [exerciseId.substring(6)] : []),
|
|
].flat();
|
|
|
|
for (const id of idVariations) {
|
|
const dbTitle = await lookupExerciseTitle(id);
|
|
if (dbTitle) {
|
|
logger.debug(`Found in database: ${id} → ${dbTitle}`);
|
|
names[exerciseId] = dbTitle;
|
|
break;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Database lookup failed for ${exerciseId}:`, error);
|
|
// Keep the better name we created earlier
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// RESOLUTION STRATEGY 3: Try extracting a name from the ID pattern using the helper
|
|
const extractedName = extractExerciseName(idToProcess);
|
|
logger.debug(`Extracted name for ${idToProcess}: ${extractedName}`);
|
|
|
|
if (extractedName !== 'Exercise') {
|
|
names[exerciseId] = extractedName;
|
|
return;
|
|
}
|
|
|
|
// RESOLUTION STRATEGY 4: Database lookup as last resort
|
|
try {
|
|
// Try with and without "local:" prefix if we haven't already handled it
|
|
const lookupIds = localPrefix ? [idToProcess] : [exerciseId, idToProcess];
|
|
|
|
for (const id of lookupIds) {
|
|
const dbTitle = await lookupExerciseTitle(id);
|
|
if (dbTitle) {
|
|
logger.debug(`Database lookup successful for ${id}: ${dbTitle}`);
|
|
names[exerciseId] = dbTitle;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If we get here, nothing worked, use a generic name
|
|
logger.debug(`No resolution found for ${exerciseId}, using generic name`);
|
|
names[exerciseId] = `Exercise ${index + 1}`;
|
|
} catch (error) {
|
|
logger.error(`Error in name resolution process for ${exerciseId}:`, error);
|
|
names[exerciseId] = `Exercise ${index + 1}`;
|
|
}
|
|
}));
|
|
|
|
logger.debug('Final resolved names:', names);
|
|
return names;
|
|
}
|