diff --git a/app/(tabs)/CLAUDE.md b/app/(tabs)/CLAUDE.md deleted file mode 100644 index 28c610c..0000000 --- a/app/(tabs)/CLAUDE.md +++ /dev/null @@ -1,43 +0,0 @@ - -# Recent Activity - - - -### Feb 20, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #5056 | 8:24 AM | ✅ | Completed Host wrapper restoration in home screen | ~258 | -| #5055 | " | ✅ | Re-added Host wrapper to home screen JSX | ~187 | -| #5054 | " | ✅ | Re-added Host import to home screen | ~184 | -| #5043 | 8:22 AM | ✅ | Removed closing Host tag from profile screen | ~210 | -| #5042 | " | ✅ | Removed opening Host tag from profile screen | ~164 | -| #5041 | " | ✅ | Removed closing Host tag from browse screen | ~187 | -| #5040 | " | ✅ | Removed opening Host tag from browse screen | ~159 | -| #5039 | 8:21 AM | ✅ | Removed closing Host tag from activity screen | ~193 | -| #5038 | " | ✅ | Removed opening Host tag from activity screen | ~154 | - -### Apr 11, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #6124 | 7:41 PM | 🔵 | Home screen uses theme-based colors properly | ~229 | - -### Apr 13, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #6166 | 10:03 PM | ✅ | Updated Tab Layout Documentation | ~137 | -| #6154 | 10:01 PM | 🔵 | Explored Explore Tab Structure | ~174 | - -### Apr 17, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #6349 | 9:48 AM | 🔄 | Removed usePurchases import from home screen | ~271 | -| #6348 | " | 🔄 | Removed usePurchases hook from home screen | ~277 | -| #6346 | 9:47 AM | 🔄 | Cleaned up unused imports in home screen after removing direct program navigation | ~321 | -| #6343 | 9:46 AM | 🔄 | Refactored home screen body zone sections to clickable cards | ~400 | -| #6342 | 9:44 AM | 🔄 | Removed direct program navigation handler from home screen | ~305 | -| #6336 | 9:39 AM | 🔵 | Reviewed complete home screen implementation for body-zone workout programs | ~386 | - \ No newline at end of file diff --git a/app/admin/_layout.tsx b/app/admin/_layout.tsx deleted file mode 100644 index c3da7d7..0000000 --- a/app/admin/_layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Stack } from 'expo-router' -import { AdminAuthProvider, useAdminAuth } from '../../src/admin/components/AdminAuthProvider' -import { View, ActivityIndicator } from 'react-native' -import { Redirect } from 'expo-router' - -function AdminLayoutContent({ children }: { children: React.ReactNode }) { - const { isAuthenticated, isAdmin, isLoading } = useAdminAuth() - - if (isLoading) { - return ( - - - - ) - } - - if (!isAuthenticated || !isAdmin) { - return - } - - return ( - <> - - {children} - - ) -} - -export default function AdminLayout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ) -} diff --git a/app/admin/collections.tsx b/app/admin/collections.tsx deleted file mode 100644 index aeeeca6..0000000 --- a/app/admin/collections.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useState } from 'react' -import { - View, - Text, - ScrollView, - TouchableOpacity, - StyleSheet, - ActivityIndicator, - Alert, -} from 'react-native' -import { useRouter } from 'expo-router' -import { useCollections } from '../../src/shared/hooks/useSupabaseData' -import { adminService } from '../../src/admin/services/adminService' -import type { Collection } from '../../src/shared/types' - -export default function AdminCollectionsScreen() { - const router = useRouter() - const { data: collections = [], isLoading: loading, refetch } = useCollections() - const [updatingId, setUpdatingId] = useState(null) - - const handleDelete = (collection: Collection) => { - Alert.alert( - 'Delete Collection', - `Are you sure you want to delete "${collection.title}"?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - Alert.alert('Info', 'Collection deletion not yet implemented') - } - }, - ] - ) - } - - if (loading) { - return ( - - - - ) - } - - return ( - - - router.back()} style={styles.backButton}> - ← Back - - Collections - - + Add - - - - - {collections.map((collection: Collection) => ( - - - {collection.icon} - - - - {collection.title} - {collection.description} - - {collection.workoutIds.length} workouts - - - - - - Edit - - handleDelete(collection)} - disabled={updatingId === collection.id} - > - - {updatingId === collection.id ? '...' : 'Delete'} - - - - - ))} - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - }, - centered: { - justifyContent: 'center', - alignItems: 'center', - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 20, - paddingTop: 60, - borderBottomWidth: 1, - borderBottomColor: '#1C1C1E', - }, - backButton: { - padding: 8, - }, - backText: { - color: '#FF6B35', - fontSize: 16, - }, - title: { - fontSize: 20, - fontWeight: 'bold', - color: '#fff', - }, - addButton: { - backgroundColor: '#FF6B35', - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 8, - }, - addButtonText: { - color: '#000', - fontWeight: 'bold', - }, - content: { - flex: 1, - padding: 16, - }, - collectionCard: { - backgroundColor: '#1C1C1E', - borderRadius: 12, - padding: 16, - marginBottom: 12, - flexDirection: 'row', - alignItems: 'center', - }, - iconContainer: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: '#2C2C2E', - justifyContent: 'center', - alignItems: 'center', - marginRight: 12, - }, - icon: { - fontSize: 24, - }, - collectionInfo: { - flex: 1, - }, - collectionTitle: { - fontSize: 16, - fontWeight: 'bold', - color: '#fff', - marginBottom: 4, - }, - collectionDescription: { - fontSize: 14, - color: '#999', - marginBottom: 4, - }, - collectionMeta: { - fontSize: 12, - color: '#666', - }, - actions: { - flexDirection: 'row', - gap: 8, - }, - editButton: { - backgroundColor: '#2C2C2E', - paddingHorizontal: 12, - paddingVertical: 8, - borderRadius: 6, - }, - editText: { - color: '#5AC8FA', - }, - deleteButton: { - backgroundColor: '#2C2C2E', - paddingHorizontal: 12, - paddingVertical: 8, - borderRadius: 6, - }, - disabledButton: { - opacity: 0.5, - }, - deleteText: { - color: '#FF3B30', - }, -}) diff --git a/app/admin/index.tsx b/app/admin/index.tsx deleted file mode 100644 index e84c3a8..0000000 --- a/app/admin/index.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { useState, useCallback } from 'react' -import { - View, - Text, - ScrollView, - TouchableOpacity, - StyleSheet, - RefreshControl, -} from 'react-native' -import { useRouter } from 'expo-router' -import { useAdminAuth } from '../../src/admin/components/AdminAuthProvider' -import { useWorkouts, useTrainers, useCollections } from '../../src/shared/hooks/useSupabaseData' - -export default function AdminDashboardScreen() { - const router = useRouter() - const { signOut } = useAdminAuth() - const [refreshing, setRefreshing] = useState(false) - - const { - data: workouts = [], - isLoading: workoutsLoading, - refetch: refetchWorkouts - } = useWorkouts() - - const { - data: trainers = [], - isLoading: trainersLoading, - refetch: refetchTrainers - } = useTrainers() - - const { - data: collections = [], - isLoading: collectionsLoading, - refetch: refetchCollections - } = useCollections() - - const onRefresh = useCallback(async () => { - setRefreshing(true) - await Promise.all([ - refetchWorkouts(), - refetchTrainers(), - refetchCollections(), - ]) - setRefreshing(false) - }, [refetchWorkouts, refetchTrainers, refetchCollections]) - - const handleLogout = async () => { - await signOut() - router.replace('/admin/login') - } - - const isLoading = workoutsLoading || trainersLoading || collectionsLoading - - return ( - - - Admin Dashboard - - Logout - - - - - } - > - - - {workouts.length} - Workouts - - - {trainers.length} - Trainers - - - {collections.length} - Collections - - - - Quick Actions - - - router.push('/admin/workouts')} - > - 💪 - Manage Workouts - Add, edit, or delete workouts - - - router.push('/admin/trainers')} - > - 👥 - Manage Trainers - Update trainer profiles - - - router.push('/admin/collections')} - > - 📁 - Manage Collections - Organize workout collections - - - router.push('/admin/media')} - > - 🎬 - Media Library - Upload videos and images - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 20, - paddingTop: 60, - borderBottomWidth: 1, - borderBottomColor: '#1C1C1E', - }, - title: { - fontSize: 28, - fontWeight: 'bold', - color: '#fff', - }, - logoutButton: { - padding: 8, - }, - logoutText: { - color: '#FF6B35', - fontSize: 16, - }, - content: { - flex: 1, - padding: 20, - }, - statsGrid: { - flexDirection: 'row', - gap: 12, - marginBottom: 32, - }, - statCard: { - flex: 1, - backgroundColor: '#1C1C1E', - borderRadius: 12, - padding: 16, - alignItems: 'center', - }, - statNumber: { - fontSize: 32, - fontWeight: 'bold', - color: '#FF6B35', - }, - statLabel: { - fontSize: 14, - color: '#999', - marginTop: 4, - }, - sectionTitle: { - fontSize: 20, - fontWeight: 'bold', - color: '#fff', - marginBottom: 16, - }, - actionsGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 12, - }, - actionCard: { - width: '47%', - backgroundColor: '#1C1C1E', - borderRadius: 12, - padding: 20, - marginBottom: 12, - }, - actionIcon: { - fontSize: 32, - marginBottom: 12, - }, - actionTitle: { - fontSize: 16, - fontWeight: 'bold', - color: '#fff', - marginBottom: 4, - }, - actionDescription: { - fontSize: 14, - color: '#999', - }, -}) diff --git a/app/admin/login.tsx b/app/admin/login.tsx deleted file mode 100644 index b9ed77a..0000000 --- a/app/admin/login.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useState } from 'react' -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - ActivityIndicator, -} from 'react-native' -import { useAdminAuth } from '../../src/admin/components/AdminAuthProvider' - -export default function AdminLoginScreen() { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const { signIn, isLoading } = useAdminAuth() - - const handleLogin = async () => { - if (!email || !password) { - setError('Please enter both email and password') - return - } - - setError('') - try { - await signIn(email, password) - } catch (err) { - setError(err instanceof Error ? err.message : 'Login failed') - } - } - - return ( - - - TabataFit Admin - Sign in to manage content - - {error ? {error} : null} - - - - - - - {isLoading ? ( - - ) : ( - Sign In - )} - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - justifyContent: 'center', - alignItems: 'center', - padding: 20, - }, - card: { - backgroundColor: '#1C1C1E', - borderRadius: 16, - padding: 32, - width: '100%', - maxWidth: 400, - }, - title: { - fontSize: 28, - fontWeight: 'bold', - color: '#fff', - marginBottom: 8, - }, - subtitle: { - fontSize: 16, - color: '#999', - marginBottom: 24, - }, - errorText: { - color: '#FF3B30', - marginBottom: 16, - }, - input: { - backgroundColor: '#2C2C2E', - borderRadius: 8, - padding: 16, - marginBottom: 16, - color: '#fff', - fontSize: 16, - }, - button: { - backgroundColor: '#FF6B35', - borderRadius: 8, - padding: 16, - alignItems: 'center', - }, - buttonText: { - color: '#000', - fontSize: 16, - fontWeight: 'bold', - }, -}) diff --git a/app/admin/media.tsx b/app/admin/media.tsx deleted file mode 100644 index dd61643..0000000 --- a/app/admin/media.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useState } from 'react' -import { - View, - Text, - ScrollView, - TouchableOpacity, - StyleSheet, - ActivityIndicator, - Alert, -} from 'react-native' -import { useRouter } from 'expo-router' -import { supabase } from '../../src/shared/supabase' - -export default function AdminMediaScreen() { - const router = useRouter() - const [uploading, setUploading] = useState(false) - const [activeTab, setActiveTab] = useState<'videos' | 'thumbnails' | 'avatars'>('videos') - - const handleUpload = async () => { - Alert.alert('Info', 'File upload requires file picker integration. This is a placeholder.') - } - - const handleDelete = async (path: string) => { - Alert.alert( - 'Delete File', - `Are you sure you want to delete "${path}"?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - try { - const { error } = await supabase.storage - .from(activeTab) - .remove([path]) - - if (error) throw error - Alert.alert('Success', 'File deleted') - } catch (err) { - Alert.alert('Error', err instanceof Error ? err.message : 'Failed to delete') - } - } - }, - ] - ) - } - - return ( - - - router.back()} style={styles.backButton}> - ← Back - - Media Library - - Upload - - - - - {(['videos', 'thumbnails', 'avatars'] as const).map((tab) => ( - setActiveTab(tab)} - > - - {tab.charAt(0).toUpperCase() + tab.slice(1)} - - - ))} - - - - - Storage Buckets - - • videos - Workout videos (MP4, MOV){'\n'} - • thumbnails - Workout thumbnails (JPG, PNG){'\n'} - • avatars - Trainer avatars (JPG, PNG) - - - - - 🎬 - Media Management - - Upload and manage media files for workouts and trainers.{'\n\n'} - This feature requires file picker integration.{'\n'} - Files will be stored in Supabase Storage. - - - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 20, - paddingTop: 60, - borderBottomWidth: 1, - borderBottomColor: '#1C1C1E', - }, - backButton: { - padding: 8, - }, - backText: { - color: '#FF6B35', - fontSize: 16, - }, - title: { - fontSize: 20, - fontWeight: 'bold', - color: '#fff', - }, - uploadButton: { - backgroundColor: '#FF6B35', - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 8, - }, - uploadButtonText: { - color: '#000', - fontWeight: 'bold', - }, - tabs: { - flexDirection: 'row', - padding: 16, - gap: 8, - }, - tab: { - flex: 1, - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 8, - backgroundColor: '#1C1C1E', - alignItems: 'center', - }, - activeTab: { - backgroundColor: '#FF6B35', - }, - tabText: { - color: '#999', - fontWeight: '600', - }, - activeTabText: { - color: '#000', - }, - content: { - flex: 1, - padding: 16, - }, - infoCard: { - backgroundColor: '#1C1C1E', - borderRadius: 12, - padding: 16, - marginBottom: 16, - }, - infoTitle: { - fontSize: 16, - fontWeight: 'bold', - color: '#fff', - marginBottom: 8, - }, - infoText: { - fontSize: 14, - color: '#999', - lineHeight: 20, - }, - placeholderCard: { - backgroundColor: '#1C1C1E', - borderRadius: 12, - padding: 32, - alignItems: 'center', - }, - placeholderIcon: { - fontSize: 48, - marginBottom: 16, - }, - placeholderTitle: { - fontSize: 18, - fontWeight: 'bold', - color: '#fff', - marginBottom: 8, - }, - placeholderText: { - fontSize: 14, - color: '#999', - textAlign: 'center', - lineHeight: 20, - }, -}) diff --git a/app/admin/trainers.tsx b/app/admin/trainers.tsx deleted file mode 100644 index 59a9ae6..0000000 --- a/app/admin/trainers.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { useState } from 'react' -import { - View, - Text, - ScrollView, - TouchableOpacity, - StyleSheet, - ActivityIndicator, - Alert, -} from 'react-native' -import { useRouter } from 'expo-router' -import { useTrainers } from '../../src/shared/hooks/useSupabaseData' -import { adminService } from '../../src/admin/services/adminService' -import type { Trainer } from '../../src/shared/types' - -export default function AdminTrainersScreen() { - const router = useRouter() - const { data: trainers = [], isLoading: loading, refetch } = useTrainers() - const [deletingId, setDeletingId] = useState(null) - - const handleDelete = (trainer: Trainer) => { - Alert.alert( - 'Delete Trainer', - `Are you sure you want to delete "${trainer.name}"?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - setDeletingId(trainer.id) - try { - await adminService.deleteTrainer(trainer.id) - await refetch() - } catch (err) { - Alert.alert('Error', err instanceof Error ? err.message : 'Failed to delete') - } finally { - setDeletingId(null) - } - } - }, - ] - ) - } - - if (loading) { - return ( - - - - ) - } - - return ( - - - router.back()} style={styles.backButton}> - ← Back - - Trainers - - + Add - - - - - {trainers.map((trainer) => ( - - - - {trainer.name} - - {trainer.specialty} • {trainer.workoutCount} workouts - - - - - - Edit - - handleDelete(trainer)} - disabled={deletingId === trainer.id} - > - - {deletingId === trainer.id ? '...' : 'Delete'} - - - - - ))} - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - }, - centered: { - justifyContent: 'center', - alignItems: 'center', - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 20, - paddingTop: 60, - borderBottomWidth: 1, - borderBottomColor: '#1C1C1E', - }, - backButton: { - padding: 8, - }, - backText: { - color: '#FF6B35', - fontSize: 16, - }, - title: { - fontSize: 20, - fontWeight: 'bold', - color: '#fff', - }, - addButton: { - backgroundColor: '#FF6B35', - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 8, - }, - addButtonText: { - color: '#000', - fontWeight: 'bold', - }, - content: { - flex: 1, - padding: 16, - }, - trainerCard: { - backgroundColor: '#1C1C1E', - borderRadius: 12, - padding: 16, - marginBottom: 12, - flexDirection: 'row', - alignItems: 'center', - }, - colorIndicator: { - width: 12, - height: 12, - borderRadius: 6, - marginRight: 12, - }, - trainerInfo: { - flex: 1, - }, - trainerName: { - fontSize: 16, - fontWeight: 'bold', - color: '#fff', - marginBottom: 4, - }, - trainerMeta: { - fontSize: 14, - color: '#999', - }, - actions: { - flexDirection: 'row', - gap: 8, - }, - editButton: { - backgroundColor: '#2C2C2E', - paddingHorizontal: 12, - paddingVertical: 8, - borderRadius: 6, - }, - editText: { - color: '#5AC8FA', - }, - deleteButton: { - backgroundColor: '#2C2C2E', - paddingHorizontal: 12, - paddingVertical: 8, - borderRadius: 6, - }, - disabledButton: { - opacity: 0.5, - }, - deleteText: { - color: '#FF3B30', - }, -}) diff --git a/app/admin/workouts.tsx b/app/admin/workouts.tsx deleted file mode 100644 index 6dcfff8..0000000 --- a/app/admin/workouts.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useState } from 'react' -import { - View, - Text, - ScrollView, - TouchableOpacity, - StyleSheet, - ActivityIndicator, - Alert, -} from 'react-native' -import { useRouter } from 'expo-router' -import { useWorkouts } from '../../src/shared/hooks/useSupabaseData' -import { adminService } from '../../src/admin/services/adminService' -import type { Workout } from '../../src/shared/types' - -export default function AdminWorkoutsScreen() { - const router = useRouter() - const { data: workouts = [], isLoading: loading, error, refetch } = useWorkouts() - const [deletingId, setDeletingId] = useState(null) - - const handleDelete = (workout: Workout) => { - Alert.alert( - 'Delete Workout', - `Are you sure you want to delete "${workout.title}"?`, - [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Delete', - style: 'destructive', - onPress: async () => { - setDeletingId(workout.id) - try { - await adminService.deleteWorkout(workout.id) - await refetch() - } catch (err) { - Alert.alert('Error', err instanceof Error ? err.message : 'Failed to delete') - } finally { - setDeletingId(null) - } - } - }, - ] - ) - } - - if (loading) { - return ( - - - - ) - } - - return ( - - - router.back()} style={styles.backButton}> - ← Back - - Workouts - - + Add - - - - - {workouts.map((workout) => ( - - - {workout.title} - - {workout.category} • {workout.level} • {workout.duration}min - - - {workout.rounds} rounds • {workout.calories} cal - - - - - Edit - - handleDelete(workout)} - disabled={deletingId === workout.id} - > - - {deletingId === workout.id ? '...' : 'Delete'} - - - - - ))} - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#000', - }, - centered: { - justifyContent: 'center', - alignItems: 'center', - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 20, - paddingTop: 60, - borderBottomWidth: 1, - borderBottomColor: '#1C1C1E', - }, - backButton: { - padding: 8, - }, - backText: { - color: '#FF6B35', - fontSize: 16, - }, - title: { - fontSize: 20, - fontWeight: 'bold', - color: '#fff', - }, - addButton: { - backgroundColor: '#FF6B35', - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 8, - }, - addButtonText: { - color: '#000', - fontWeight: 'bold', - }, - content: { - flex: 1, - padding: 16, - }, - workoutCard: { - backgroundColor: '#1C1C1E', - borderRadius: 12, - padding: 16, - marginBottom: 12, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - workoutInfo: { - flex: 1, - }, - workoutTitle: { - fontSize: 16, - fontWeight: 'bold', - color: '#fff', - marginBottom: 4, - }, - workoutMeta: { - fontSize: 14, - color: '#999', - }, - actions: { - flexDirection: 'row', - gap: 8, - }, - editButton: { - backgroundColor: '#2C2C2E', - paddingHorizontal: 12, - paddingVertical: 8, - borderRadius: 6, - }, - editText: { - color: '#5AC8FA', - }, - deleteButton: { - backgroundColor: '#2C2C2E', - paddingHorizontal: 12, - paddingVertical: 8, - borderRadius: 6, - }, - disabledButton: { - opacity: 0.5, - }, - deleteText: { - color: '#FF3B30', - }, -}) diff --git a/app/assessment.tsx b/app/assessment.tsx deleted file mode 100644 index 3d4089a..0000000 --- a/app/assessment.tsx +++ /dev/null @@ -1,445 +0,0 @@ -/** - * TabataFit Assessment Screen - * Initial movement assessment to personalize experience - */ - -import { View, StyleSheet, ScrollView, Pressable } from 'react-native' -import { useRouter } from 'expo-router' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { LinearGradient } from 'expo-linear-gradient' -import { Icon } from '@/src/shared/components/Icon' -import { NativeButton } from '@/src/shared/components/native' - -import { useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useHaptics } from '@/src/shared/hooks' -import { useProgramStore } from '@/src/shared/stores' -import { ASSESSMENT_WORKOUT } from '@/src/shared/data/programs' -import { StyledText } from '@/src/shared/components/StyledText' - -import { useThemeColors, BRAND } from '@/src/shared/theme' -import { withOpacity } from '@/src/shared/utils/color' -import type { ThemeColors } from '@/src/shared/theme/types' -import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' -import { RADIUS } from '@/src/shared/constants/borderRadius' -import { TEXT, GRADIENTS } from '@/src/shared/constants/colors' - -const FONTS = { - LARGE_TITLE: 28, - TITLE: 24, - HEADLINE: 17, - BODY: 16, - CAPTION: 13, -} - -export default function AssessmentScreen() { - const { t } = useTranslation('screens') - const insets = useSafeAreaInsets() - const router = useRouter() - const haptics = useHaptics() - const colors = useThemeColors() - const styles = useMemo(() => createStyles(colors), [colors]) - - const [showIntro, setShowIntro] = useState(true) - const skipAssessment = useProgramStore((s) => s.skipAssessment) - const completeAssessment = useProgramStore((s) => s.completeAssessment) - - const handleSkip = () => { - haptics.buttonTap() - skipAssessment() - router.back() - } - - const handleStart = () => { - haptics.buttonTap() - setShowIntro(false) - } - - const handleComplete = () => { - haptics.workoutComplete() - completeAssessment({ - completedAt: new Date().toISOString(), - exercisesCompleted: ASSESSMENT_WORKOUT.exercises.map(e => e.name), - }) - router.back() - } - - if (!showIntro) { - // Here we'd show the actual assessment workout player - // For now, just show a completion screen - return ( - - - setShowIntro(true)}> - - - - {t('assessment.title')} - - - - - - - - {ASSESSMENT_WORKOUT.exercises.map((exercise, index) => ( - - - - {index + 1} - - - - - {exercise.name} - - - {exercise.duration}s • {exercise.purpose} - - - - ))} - - - - - {t('assessment.tips')} - - {[1, 2, 3, 4].map((index) => ( - - - - {t(`assessment.tip${index}`)} - - - ))} - - - - - - - - - ) - } - - return ( - - {/* Header */} - - - - - - - - - {/* Hero */} - - - - - - - {t('assessment.welcomeTitle')} - - - - {t('assessment.welcomeDescription')} - - - - {/* Features */} - - - - - - - - {ASSESSMENT_WORKOUT.duration} {t('assessment.minutes')} - - - {t('assessment.quickCheck')} - - - - - - - - - - - {ASSESSMENT_WORKOUT.exercises.length} {t('assessment.movements')} - - - {t('assessment.testMovements')} - - - - - - - - - - - {t('assessment.noEquipment')} - - - {t('assessment.justYourBody')} - - - - - - {/* Benefits */} - - - {t('assessment.whatWeCheck')} - - - - - - {t('assessment.mobility')} - - - - - {t('assessment.strength')} - - - - - {t('assessment.stability')} - - - - - {t('assessment.balance')} - - - - - - - {/* Bottom Actions */} - - - - - - - ) -} - -function createStyles(colors: ThemeColors) { - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.bg.base, - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - }, - - // Header - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingVertical: SPACING[3], - }, - backButton: { - width: 40, - height: 40, - alignItems: 'center', - justifyContent: 'center', - }, - placeholder: { - width: 40, - }, - - // Hero - heroSection: { - alignItems: 'center', - marginTop: SPACING[4], - marginBottom: SPACING[8], - }, - iconContainer: { - width: 100, - height: 100, - borderRadius: RADIUS.FULL, - backgroundColor: withOpacity(BRAND.PRIMARY, 0.08), - alignItems: 'center', - justifyContent: 'center', - marginBottom: SPACING[5], - }, - heroTitle: { - textAlign: 'center', - marginBottom: SPACING[3], - }, - heroDescription: { - textAlign: 'center', - lineHeight: 24, - paddingHorizontal: SPACING[4], - }, - - // Features - featuresSection: { - marginBottom: SPACING[8], - }, - featureItem: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: SPACING[4], - backgroundColor: colors.bg.surface, - padding: SPACING[4], - borderRadius: RADIUS.LG, - }, - featureIcon: { - width: 48, - height: 48, - borderRadius: RADIUS.FULL, - backgroundColor: withOpacity(BRAND.PRIMARY, 0.08), - alignItems: 'center', - justifyContent: 'center', - marginRight: SPACING[3], - }, - featureText: { - flex: 1, - }, - - // Benefits - benefitsSection: { - marginBottom: SPACING[6], - }, - benefitsTitle: { - marginBottom: SPACING[3], - }, - benefitsList: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: SPACING[2], - }, - benefitTag: { - backgroundColor: colors.bg.surface, - paddingHorizontal: SPACING[4], - paddingVertical: SPACING[2], - borderRadius: RADIUS.FULL, - borderWidth: 1, - borderColor: colors.border.dim, - }, - - // Assessment Container - assessmentContainer: { - marginTop: SPACING[2], - }, - exerciseList: { - marginBottom: SPACING[6], - }, - exerciseItem: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: colors.bg.surface, - padding: SPACING[4], - borderRadius: RADIUS.LG, - marginBottom: SPACING[2], - }, - exerciseNumber: { - width: 36, - height: 36, - borderRadius: RADIUS.FULL, - backgroundColor: withOpacity(BRAND.PRIMARY, 0.08), - alignItems: 'center', - justifyContent: 'center', - marginRight: SPACING[3], - }, - exerciseInfo: { - flex: 1, - }, - - // Tips - tipsSection: { - backgroundColor: colors.bg.surface, - borderRadius: RADIUS.LG, - padding: SPACING[5], - }, - tipsTitle: { - marginBottom: SPACING[4], - }, - tipItem: { - flexDirection: 'row', - alignItems: 'flex-start', - marginBottom: SPACING[3], - }, - tipText: { - marginLeft: SPACING[2], - flex: 1, - lineHeight: 20, - }, - - // Bottom Bar - bottomBar: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - backgroundColor: colors.bg.base, - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingTop: SPACING[3], - borderTopWidth: 1, - borderTopColor: colors.border.dim, - }, - ctaButton: { - borderRadius: RADIUS.LG, - overflow: 'hidden', - marginBottom: SPACING[3], - }, - ctaGradient: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING[4], - }, - ctaIcon: { - marginLeft: SPACING[2], - }, - skipButton: { - alignItems: 'center', - paddingVertical: SPACING[2], - }, - }) -} diff --git a/app/collection/CLAUDE.md b/app/collection/CLAUDE.md deleted file mode 100644 index 31017cc..0000000 --- a/app/collection/CLAUDE.md +++ /dev/null @@ -1,11 +0,0 @@ - -# Recent Activity - - - -### Apr 13, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #6159 | 10:02 PM | 🔵 | Examined Collection Screen Explore Reference | ~150 | - \ No newline at end of file diff --git a/app/collection/[id].tsx b/app/collection/[id].tsx deleted file mode 100644 index 0d2b83e..0000000 --- a/app/collection/[id].tsx +++ /dev/null @@ -1,245 +0,0 @@ -/** - * TabataFit Collection Detail Screen - * Shows collection info + list of workouts in that collection - */ - -import { useMemo } from 'react' -import { View, StyleSheet, ScrollView, Pressable } from 'react-native' -import { useRouter, useLocalSearchParams } from 'expo-router' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { Icon } from '@/src/shared/components/Icon' -import { useTranslation } from 'react-i18next' - -import { useHaptics } from '@/src/shared/hooks' -import { useCollection } from '@/src/shared/hooks/useSupabaseData' -import { getWorkoutById } from '@/src/shared/data' -import { useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData' -import { StyledText } from '@/src/shared/components/StyledText' -import { track } from '@/src/shared/services/analytics' - -import { useThemeColors, BRAND } from '@/src/shared/theme' -import type { ThemeColors } from '@/src/shared/theme/types' -import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' -import { RADIUS } from '@/src/shared/constants/borderRadius' -import { TEXT, NAVY } from '@/src/shared/constants/colors' - -export default function CollectionDetailScreen() { - const insets = useSafeAreaInsets() - const router = useRouter() - const haptics = useHaptics() - const { t } = useTranslation() - const { id } = useLocalSearchParams<{ id: string }>() - - const colors = useThemeColors() - const styles = useMemo(() => createStyles(colors), [colors]) - - const { data: collection, isLoading } = useCollection(id) - - // Resolve workouts from collection's workoutIds - const rawWorkouts = useMemo(() => { - if (!collection) return [] - return collection.workoutIds - .map((wId) => getWorkoutById(wId)) - .filter(Boolean) as NonNullable>[] - }, [collection]) - - const workouts = useTranslatedWorkouts(rawWorkouts) - - const handleBack = () => { - haptics.selection() - router.back() - } - - const handleWorkoutPress = (workoutId: string) => { - haptics.buttonTap() - track('collection_workout_tapped', { - collection_id: id, - workout_id: workoutId, - }) - router.push(`/workout/${workoutId}`) - } - - if (isLoading) { - return ( - - Loading... - - ) - } - - if (!collection) { - return ( - - - - Collection not found - - - ) - } - - return ( - - {/* Header */} - - - - - - {collection.title} - - - - - - {/* Hero Card */} - - - - - {collection.icon} - - - {collection.title} - - - {collection.description} - - - {t('plurals.workout', { count: workouts.length })} - - - - - {/* Workout List */} - - {t('screens:explore.workouts')} - - - {workouts.map((workout) => ( - handleWorkoutPress(workout.id)} - > - - - - - - {workout.title} - - - {t('durationLevel', { - duration: workout.duration, - level: t(`levels.${(workout.level ?? 'Beginner').toLowerCase()}`), - })} - - - - - {t('units.calUnit', { count: workout.calories })} - - - - - ))} - - {workouts.length === 0 && ( - - - - No workouts in this collection - - - )} - - - ) -} - -function createStyles(colors: ThemeColors) { - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.bg.base, - }, - centered: { - alignItems: 'center', - justifyContent: 'center', - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingVertical: SPACING[3], - }, - backButton: { - width: 44, - height: 44, - alignItems: 'center', - justifyContent: 'center', - }, - heroCard: { - height: 200, - borderRadius: RADIUS.XL, - overflow: 'hidden', - backgroundColor: NAVY[700], - }, - heroContent: { - flex: 1, - padding: SPACING[5], - justifyContent: 'flex-end', - }, - heroIcon: { - marginBottom: SPACING[2], - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - }, - workoutCard: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: SPACING[3], - paddingHorizontal: SPACING[4], - backgroundColor: colors.bg.surface, - borderRadius: RADIUS.LG, - marginBottom: SPACING[2], - gap: SPACING[3], - }, - workoutAvatar: { - width: 44, - height: 44, - borderRadius: RADIUS.FULL, - alignItems: 'center', - justifyContent: 'center', - }, - workoutInfo: { - flex: 1, - gap: 2, - }, - workoutMeta: { - alignItems: 'flex-end', - gap: 4, - }, - emptyState: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING[12], - }, - }) -} diff --git a/app/workout/CLAUDE.md b/app/workout/CLAUDE.md deleted file mode 100644 index 42d3fc8..0000000 --- a/app/workout/CLAUDE.md +++ /dev/null @@ -1,20 +0,0 @@ - -# Recent Activity - - - -### Feb 20, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #5045 | 8:22 AM | ✅ | Removed closing Host tag from workout detail screen | ~188 | -| #5044 | " | ✅ | Removed opening Host tag from workout detail screen | ~158 | -| #5032 | 8:19 AM | ✅ | Removed Host import from workout detail screen | ~194 | -| #5025 | 8:18 AM | 🔵 | Workout detail screen properly wraps content with Host component | ~244 | - -### Apr 17, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #6366 | 10:21 AM | 🔵 | Verified workout program player integration in workout/[id].tsx | ~348 | - \ No newline at end of file diff --git a/app/workout/[id].tsx b/app/workout/[id].tsx deleted file mode 100644 index 0049502..0000000 --- a/app/workout/[id].tsx +++ /dev/null @@ -1,777 +0,0 @@ -/** - * TabataFit Pre-Workout Detail Screen - * Clean scrollable layout — native header, no hero - */ - -import React, { useEffect, useRef, useState } from 'react' -import { - View, - Text as RNText, - StyleSheet, - ScrollView, - Pressable, - Animated, -} from 'react-native' -import { Stack } from 'expo-router' -import { useRouter, useLocalSearchParams } from 'expo-router' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { Icon } from '@/src/shared/components/Icon' -import { VideoPlayer } from '@/src/shared/components/VideoPlayer' -import { Image } from 'expo-image' -import { useTranslation } from 'react-i18next' - -import { useHaptics } from '@/src/shared/hooks' -import { usePurchases } from '@/src/shared/hooks/usePurchases' -import { useUserStore } from '@/src/shared/stores' -import { track } from '@/src/shared/services/analytics' -import { canAccessWorkout, canAccessSession } from '@/src/shared/services/access' -import { getTabataSessionById, isTabataSession } from '@/src/shared/data/tabata' -import { isWorkoutProgramId, parseWorkoutProgramId, fetchProgramById, workoutProgramToTabataSession } from '@/src/shared/data/workoutPrograms' -import { BODY_ZONE_META } from '@/src/shared/types/workoutProgram' -import type { TabataSession } from '@/src/shared/types/program' -import { getWorkoutById, getTrainerById, getWorkoutAccentColor } from '@/src/shared/data' -import { useTranslatedWorkout, useMusicVibeLabel } from '@/src/shared/data/useTranslatedData' - -import { useThemeColors } from '@/src/shared/theme' -import type { ThemeColors } from '@/src/shared/theme/types' -import { TYPOGRAPHY } from '@/src/shared/constants/typography' -import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' -import { RADIUS } from '@/src/shared/constants/borderRadius' -import { SPRING } from '@/src/shared/constants/animations' -import { TEXT, NAVY, BRAND, GREEN, AMBER, RED, DARK, BORDER_COLORS } from '@/src/shared/constants/colors' -import { NativeButton } from '@/src/shared/components/native' - -// ─── Save Button (headerRight) ─────────────────────────────────────────────── - -function SaveButton({ - isSaved, - onPress, - colors, -}: { - isSaved: boolean - onPress: () => void - colors: ThemeColors -}) { - return ( - [ - { width: 32, height: 32, alignItems: 'center', justifyContent: 'center' }, - pressed && { opacity: 0.6 }, - ]} - > - - - ) -} - -// ─── Main Screen ───────────────────────────────────────────────────────────── - -export default function WorkoutDetailScreen() { - const insets = useSafeAreaInsets() - const router = useRouter() - const haptics = useHaptics() - const { t } = useTranslation() - const { id } = useLocalSearchParams<{ id: string }>() - - // ─── Dispatch: Workout Program → Tabata session → Legacy workout ─ - if (isWorkoutProgramId(id ?? '')) { - return - } - - if (isTabataSession(id ?? '')) { - return - } - - return -} - -/** - * Workout Program Detail — loads a program tabata and delegates to TabataSessionDetailScreen - */ -function WorkoutProgramDetailScreen({ compositeId }: { compositeId: string }) { - const [session, setSession] = React.useState(undefined) - const [accent, setAccent] = React.useState(GREEN[500]) - const [isFree, setIsFree] = React.useState(false) - - React.useEffect(() => { - let cancelled = false - async function load() { - const parsed = parseWorkoutProgramId(compositeId) - if (!parsed) { if (!cancelled) setSession(null); return } - const program = await fetchProgramById(parsed.programId) - if (cancelled) return - if (!program) { setSession(null); return } - const tabataSession = workoutProgramToTabataSession(program) - setSession(tabataSession) - setIsFree(program.isFree === true) - const zoneMeta = BODY_ZONE_META[program.bodyZone] - setAccent(program.accentColor ?? zoneMeta.color) - } - load() - return () => { cancelled = true } - }, [compositeId]) - - if (session === undefined) { - return ( - - - Chargement... - - ) - } - - if (session === null) { - return ( - - - Programme non trouvé - - ) - } - - return -} - -/** - * Tabata Session Detail — shows warmup, blocks, cooldown, tabata tips - */ -function TabataSessionDetailScreen({ - sessionId, - sessionOverride, - accentOverride, - isFreeOverride, -}: { - sessionId: string - sessionOverride?: TabataSession - accentOverride?: string - isFreeOverride?: boolean -}) { - const router = useRouter() - const insets = useSafeAreaInsets() - const haptics = useHaptics() - const session = sessionOverride ?? getTabataSessionById(sessionId) - const { isPremium } = usePurchases() - const canAccess = isFreeOverride !== undefined - ? (isPremium || isFreeOverride) - : canAccessSession(sessionId, isPremium) - - if (!session) { - return ( - - - Séance non trouvée - - ) - } - - const programId = sessionId.startsWith('deb-') ? 'debutant' : sessionId.startsWith('int-') ? 'intermediaire' : sessionId.startsWith('avc-') ? 'avance' : 'bureau' - const accentMap: Record = { debutant: GREEN[500], intermediaire: BRAND.INFO, avance: RED[500], bureau: AMBER[500] } - const accent = accentOverride ?? accentMap[programId] ?? GREEN[500] - - const handleStart = () => { - haptics.buttonTap() - track('tabata_session_start_pressed', { session_id: sessionId }) - router.push(`/player/${sessionId}`) - } - - return ( - - - - {/* Session info */} - - {session.title} - {session.description} - - {session.blocks.length} bloc{session.blocks.length > 1 ? 's' : ''} - · - {session.totalDuration} min - · - {session.calories} cal - - {/* Focus tags */} - - {session.focus.map((f, i) => ( - - {f} - - ))} - - - - {/* Warmup */} - {session.warmup.movements.length > 0 && ( - - Échauffement · {Math.floor(session.warmup.totalDuration / 60)} min - {session.warmup.movements.map((m, i) => ( - - - {m.name} - {m.duration}s - - ))} - - )} - - {/* Blocks */} - {session.blocks.map((block, bi) => ( - - Bloc {bi + 1} · {block.rounds} rounds · {block.workTime}/{block.restTime}s - - - Rounds impairs - {block.oddExercise.name} - {block.oddExercise.conseil ? 📋 {block.oddExercise.conseil} : null} - - - Rounds pairs - {block.evenExercise.name} - {block.evenExercise.conseil ? 📋 {block.evenExercise.conseil} : null} - - - - ))} - - {/* Cooldown */} - {session.cooldown.movements.length > 0 && ( - - Retour au calme · {Math.floor(session.cooldown.totalDuration / 60)} min - {session.cooldown.movements.map((m, i) => ( - - - {m.name} - {m.duration}s - - ))} - - )} - - {/* Equipment */} - {session.equipment.length > 0 && ( - - Matériel - {session.equipment.map((eq, i) => ( - • {eq} - ))} - - )} - - - {/* CTA */} - - {canAccess ? ( - - Commencer la séance - - ) : ( - router.push('/paywall')}> - Débloquer avec Premium - - )} - - - ) -} - -/** - * Legacy workout detail — original format - */ -function LegacyWorkoutDetailScreen({ id }: { id: string }) { - const insets = useSafeAreaInsets() - const router = useRouter() - const haptics = useHaptics() - const { t } = useTranslation() - const savedWorkouts = useUserStore((s) => s.savedWorkouts) - const toggleSavedWorkout = useUserStore((s) => s.toggleSavedWorkout) - const { isPremium } = usePurchases() - - const colors = useThemeColors() - const isDark = colors.colorScheme === 'dark' - - const rawWorkout = getWorkoutById(id ?? '1') - const workout = useTranslatedWorkout(rawWorkout) - const musicVibeLabel = useMusicVibeLabel(rawWorkout?.musicVibe ?? '') - const trainer = rawWorkout ? getTrainerById(rawWorkout.trainerId) : undefined - const accentColor = GREEN[500] - - // CTA entrance - const ctaAnim = useRef(new Animated.Value(0)).current - useEffect(() => { - Animated.sequence([ - Animated.delay(300), - Animated.spring(ctaAnim, { - toValue: 1, - ...SPRING.GENTLE, - useNativeDriver: true, - }), - ]).start() - }, []) - - useEffect(() => { - if (workout) { - track('workout_detail_viewed', { - workout_id: workout.id, - workout_title: workout.title, - level: workout.level, - duration: workout.duration, - }) - } - }, [workout?.id]) - - const isSaved = savedWorkouts.includes(workout?.id?.toString() ?? '') - const toggleSave = () => { - if (!workout) return - haptics.selection() - toggleSavedWorkout(workout.id.toString()) - } - - if (!workout) { - return ( - <> - - - - {t('screens:workout.notFound')} - - - - ) - } - - const isLocked = !canAccessWorkout(workout.id, isPremium) - const exerciseCount = workout.exercises?.length || 1 - const repeatCount = Math.max(1, Math.floor((workout.rounds || exerciseCount) / exerciseCount)) - - const handleStartWorkout = () => { - if (isLocked) { - haptics.buttonTap() - track('paywall_triggered', { source: 'workout_detail', workout_id: workout.id }) - router.push('/paywall') - return - } - haptics.phaseChange() - router.push(`/player/${workout.id}`) - } - - const ctaBg = isDark ? TEXT.PRIMARY : NAVY[900] - const ctaText = isDark ? NAVY[900] : TEXT.PRIMARY - const ctaLockedBg = isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)' - const ctaLockedText = colors.text.primary - - const equipmentText = workout.equipment.length > 0 - ? workout.equipment.join(' · ') - : t('screens:workout.noEquipment', { defaultValue: 'No equipment needed' }) - - return ( - <> - ( - - ), - }} - /> - - - - {/* Thumbnail / Video Preview */} - {rawWorkout?.thumbnailUrl ? ( - - - - ) : rawWorkout?.videoUrl ? ( - - - - ) : null} - - {/* Title */} - - {workout.title} - - - {/* Trainer */} - {trainer && ( - - with {trainer.name} - - )} - - {/* Inline metadata */} - - - - - {t('units.minUnit', { count: workout.duration })} - - - · - - - - {t('units.calUnit', { count: workout.calories })} - - - · - - {t(`levels.${(workout.level ?? 'Beginner').toLowerCase()}`)} - - - - {/* Equipment */} - - {equipmentText} - - - {/* Separator */} - - - {/* Timing Card */} - - - - - {workout.prepTime}s - - - {t('screens:workout.prep', { defaultValue: 'Prep' })} - - - - - - {workout.workTime}s - - - {t('screens:workout.work', { defaultValue: 'Work' })} - - - - - - {workout.restTime}s - - - {t('screens:workout.rest', { defaultValue: 'Rest' })} - - - - - - {workout.rounds} - - - {t('screens:workout.rounds', { defaultValue: 'Rounds' })} - - - - - - {/* Exercises Card */} - - {t('screens:workout.exercises', { count: workout.rounds })} - - - - {workout.exercises.map((exercise, index) => ( - - - - {index + 1} - - - {exercise.name} - - - {exercise.duration}s - - - {index < workout.exercises.length - 1 && ( - - )} - - ))} - - - {repeatCount > 1 && ( - - - - {t('screens:workout.repeatRounds', { count: repeatCount })} - - - )} - - {/* Music */} - - - - {t('screens:workout.musicMix', { vibe: musicVibeLabel })} - - - - - {/* CTA */} - - - - - - ) -} - -// ─── Styles ────────────────────────────────────────────────────────────────── - -const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: NAVY[900] }, - heroSection: { padding: SPACING[5], alignItems: 'center' }, - sessionTitle: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, textAlign: 'center' }, - sessionDesc: { ...TYPOGRAPHY.BODY, color: TEXT.SECONDARY, textAlign: 'center', marginTop: SPACING[2], lineHeight: 22 }, - metaRow: { flexDirection: 'row', marginTop: SPACING[4], gap: SPACING[2], justifyContent: 'center' }, - metaText: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY }, - focusRow: { flexDirection: 'row', flexWrap: 'wrap', gap: SPACING[2], marginTop: SPACING[3], justifyContent: 'center' }, - focusTag: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 12, borderWidth: 1 }, - focusTagText: { fontSize: 12, fontWeight: '600' }, - section: { paddingHorizontal: SPACING[5], marginTop: SPACING[6] }, - sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, marginBottom: SPACING[3] }, - movementRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING[2], marginBottom: SPACING[2] }, - movementDot: { fontSize: 8, color: TEXT.TERTIARY }, - movementName: { ...TYPOGRAPHY.BODY, color: TEXT.PRIMARY, flex: 1 }, - movementDuration: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY }, - exercisePair: { gap: SPACING[3] }, - exerciseCard: { backgroundColor: NAVY[800], borderRadius: RADIUS.MD, padding: SPACING[3], borderLeftWidth: 3 }, - exerciseLabel: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 4 }, - exerciseName: { ...TYPOGRAPHY.TITLE_3, color: TEXT.PRIMARY }, - exerciseTip: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.SECONDARY, marginTop: SPACING[1], lineHeight: 18 }, - equipText: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY, marginBottom: SPACING[1] }, - ctaContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: SPACING[5], paddingTop: SPACING[3], backgroundColor: DARK.SCRIM, borderTopWidth: 1, borderTopColor: BORDER_COLORS.DIM }, - ctaButton: { height: 52, borderRadius: RADIUS.MD, borderCurve: 'continuous', alignItems: 'center', justifyContent: 'center' }, - ctaText: { ...TYPOGRAPHY.BUTTON_MEDIUM, color: NAVY[900], letterSpacing: 0.5 }, -}) - -const s = StyleSheet.create({ - container: { - flex: 1, - }, - centered: { - alignItems: 'center', - justifyContent: 'center', - }, - scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingTop: SPACING[2], - }, - - // Media - mediaContainer: { - height: 200, - borderRadius: RADIUS.LG, - borderCurve: 'continuous', - overflow: 'hidden', - marginBottom: SPACING[4], - }, - thumbnail: { - width: '100%', - height: '100%', - }, - - // Title - title: { - ...TYPOGRAPHY.TITLE_1, - marginBottom: SPACING[2], - }, - - // Trainer - trainerName: { - ...TYPOGRAPHY.SUBHEADLINE, - marginBottom: SPACING[3], - }, - - // Metadata - metaRow: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - gap: SPACING[2], - marginBottom: SPACING[2], - }, - metaItem: { - flexDirection: 'row', - alignItems: 'center', - gap: 4, - }, - metaText: { - ...TYPOGRAPHY.SUBHEADLINE, - }, - metaDot: { - ...TYPOGRAPHY.SUBHEADLINE, - }, - - // Equipment - equipmentText: { - ...TYPOGRAPHY.FOOTNOTE, - marginBottom: SPACING[4], - }, - - // Separator - separator: { - height: StyleSheet.hairlineWidth, - marginBottom: SPACING[4], - }, - - // Card - card: { - borderRadius: RADIUS.LG, - borderCurve: 'continuous', - overflow: 'hidden', - marginBottom: SPACING[4], - }, - - // Timing - timingRow: { - flexDirection: 'row', - paddingVertical: SPACING[4], - }, - timingItem: { - flex: 1, - alignItems: 'center', - gap: 2, - }, - timingDivider: { - width: StyleSheet.hairlineWidth, - alignSelf: 'stretch', - }, - timingValue: { - ...TYPOGRAPHY.HEADLINE, - fontVariant: ['tabular-nums'], - }, - timingLabel: { - ...TYPOGRAPHY.CAPTION_2, - textTransform: 'uppercase' as const, - letterSpacing: 0.5, - }, - - // Section - sectionTitle: { - ...TYPOGRAPHY.HEADLINE, - marginBottom: SPACING[3], - }, - - // Exercise - exerciseRow: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: SPACING[3], - paddingHorizontal: SPACING[4], - }, - exerciseIndex: { - ...TYPOGRAPHY.FOOTNOTE, - fontVariant: ['tabular-nums'], - width: 24, - }, - exerciseName: { - ...TYPOGRAPHY.BODY, - flex: 1, - }, - exerciseDuration: { - ...TYPOGRAPHY.SUBHEADLINE, - fontVariant: ['tabular-nums'], - marginLeft: SPACING[3], - }, - exerciseSep: { - height: StyleSheet.hairlineWidth, - marginLeft: SPACING[4] + 24, - marginRight: SPACING[4], - }, - repeatRow: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[2], - marginTop: SPACING[2], - paddingLeft: 24, - }, - repeatText: { - ...TYPOGRAPHY.FOOTNOTE, - }, - - // Music - musicRow: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[2], - marginTop: SPACING[5], - }, - musicText: { - ...TYPOGRAPHY.FOOTNOTE, - }, - - // Bottom bar - bottomBar: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingTop: SPACING[3], - }, - ctaButton: { - height: 54, - borderRadius: RADIUS.MD, - borderCurve: 'continuous', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'row', - }, - ctaText: { - ...TYPOGRAPHY.BUTTON_LARGE, - }, -}) diff --git a/app/workout/body-zone/CLAUDE.md b/app/workout/body-zone/CLAUDE.md deleted file mode 100644 index b82cf83..0000000 --- a/app/workout/body-zone/CLAUDE.md +++ /dev/null @@ -1,16 +0,0 @@ - -# Recent Activity - - - -### Apr 17, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #6377 | 10:28 AM | 🔴 | Fixed duplicate ScrollView opening tag in body zone detail screen | ~223 | -| #6374 | 10:26 AM | 🔄 | Removed header section from body zone detail screen | ~260 | -| #6363 | 10:20 AM | 🔄 | Changed program navigation to exclude explicit tabata position | ~319 | -| #6353 | 10:02 AM | 🔄 | Simplified difficulty pill styling in body-zone detail screen | ~281 | -| #6352 | 10:01 AM | 🔄 | Removed program count badges from difficulty filter pills | ~319 | -| #6351 | " | 🔵 | Discovered body zone detail page with difficulty level filtering | ~364 | - \ No newline at end of file diff --git a/app/workout/body-zone/[id].tsx b/app/workout/body-zone/[id].tsx deleted file mode 100644 index ea5e50b..0000000 --- a/app/workout/body-zone/[id].tsx +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Body Zone Detail Screen - * Shows workout programs filtered by body zone with difficulty pills - */ - -import { useState, useMemo, useEffect, useCallback } from 'react' -import { View, StyleSheet, ScrollView, Pressable } from 'react-native' -import { useRouter, useLocalSearchParams } from 'expo-router' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { Icon } from '@/src/shared/components/Icon' - -import { useTranslation } from 'react-i18next' -import { useHaptics } from '@/src/shared/hooks' -import { usePurchases } from '@/src/shared/hooks/usePurchases' -import { StyledText } from '@/src/shared/components/StyledText' -import { useThemeColors } from '@/src/shared/theme' -import type { ThemeColors } from '@/src/shared/theme/types' -import { BRAND, GREEN, TEXT, NAVY, BORDER_COLORS } from '@/src/shared/constants/colors' -import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' -import { RADIUS } from '@/src/shared/constants/borderRadius' -import { canAccessWorkoutProgram } from '@/src/shared/services/access' -import { useWorkoutProgramStore } from '@/src/shared/stores/workoutProgramStore' -import { fetchProgramsByBodyZone, buildWorkoutProgramId } from '@/src/shared/data/workoutPrograms' -import type { WorkoutProgram, BodyZone, ProgramLevel } from '@/src/shared/types/workoutProgram' -import { BODY_ZONE_META, LEVEL_META } from '@/src/shared/types/workoutProgram' - -const LEVELS: ProgramLevel[] = ['Beginner', 'Intermediate', 'Advanced'] - -export default function BodyZoneDetailScreen() { - const insets = useSafeAreaInsets() - const router = useRouter() - const haptics = useHaptics() - const { t } = useTranslation('screens') - const { id } = useLocalSearchParams<{ id: string }>() - const colors = useThemeColors() - const { isPremium } = usePurchases() - const isProgramCompleted = useWorkoutProgramStore(s => s.isProgramCompleted) - - const bodyZone = (id ?? 'full-body') as BodyZone - const meta = BODY_ZONE_META[bodyZone] - - const [programs, setPrograms] = useState([]) - const [selectedLevel, setSelectedLevel] = useState('Beginner') - - useEffect(() => { - fetchProgramsByBodyZone(bodyZone).then((data) => { - setPrograms(data) - // Default to first level that has programs - const firstAvailable = LEVELS.find(l => data.some(p => p.level === l)) - if (firstAvailable) setSelectedLevel(firstAvailable) - }) - }, [bodyZone]) - - const filteredPrograms = useMemo( - () => programs.filter(p => p.level === selectedLevel), - [programs, selectedLevel], - ) - - const handleProgramPress = (program: WorkoutProgram) => { - haptics.buttonTap() - const isLocked = !canAccessWorkoutProgram(program, isPremium) - if (isLocked) { - router.push('/paywall') - return - } - router.push(`/workout/${buildWorkoutProgramId(program.id)}` as any) - } - - const handleLevelPress = (level: ProgramLevel) => { - haptics.buttonTap() - setSelectedLevel(level) - } - - const accentColor = meta.color - - const styles = useMemo(() => createStyles(colors, accentColor), [colors, accentColor]) - - return ( - - - {/* Difficulty Pills */} - - {LEVELS.map((level) => { - const levelMeta = LEVEL_META[level] - const isActive = selectedLevel === level - - return ( - handleLevelPress(level)} - style={[ - styles.pill, - { - backgroundColor: isActive ? accentColor + '20' : NAVY[800], - borderColor: isActive ? accentColor : BORDER_COLORS.DIM, - }, - ]} - > - - {levelMeta.label} - - - ) - })} - - - {/* Program Count */} - - {filteredPrograms.length} programme{filteredPrograms.length !== 1 ? 's' : ''} {LEVEL_META[selectedLevel].label.toLowerCase()} - - - {/* Program List */} - {filteredPrograms.map((program) => ( - handleProgramPress(program)} - isPremium={isPremium} - isCompleted={isProgramCompleted(program.id)} - /> - ))} - - {filteredPrograms.length === 0 && ( - - - - Aucun programme disponible pour ce niveau - - - )} - - - ) -} - -// ═══════════════════════════════════════════════════════════════════════════ -// PROGRAM CARD (full-width) -// ═══════════════════════════════════════════════════════════════════════════ - -function ProgramCard({ - program, - accentColor, - onPress, - isPremium, - isCompleted, -}: { - program: WorkoutProgram - accentColor: string - onPress: () => void - isPremium: boolean - isCompleted: boolean -}) { - const { t } = useTranslation('screens') - const colors = useThemeColors() - const isLocked = !canAccessWorkoutProgram(program, isPremium) - const levelMeta = LEVEL_META[program.level] - const color = program.accentColor ?? accentColor - - return ( - [ - { - borderRadius: RADIUS.XL, - overflow: 'hidden', - borderWidth: 1, - borderCurve: 'continuous' as const, - borderColor: colors.border.dim, - backgroundColor: colors.surface.default.backgroundColor, - marginBottom: SPACING[3], - opacity: pressed ? 0.85 : 1, - }, - ]} - > - {/* Accent line */} - - - - {/* Title row */} - - - {program.title} - - - {isCompleted ? ( - - - - Complété - - - ) : isLocked ? ( - - - - {t('home.premiumBadge')} - - - ) : ( - - - {t('home.freeBadge')} - - - )} - - - {/* Description */} - {program.description ? ( - - {program.description} - - ) : null} - - {/* Meta row */} - - - - {program.estimatedDuration} min - - - - {program.estimatedCalories} kcal - - - - {program.tabatas.length} tabatas - - - - {/* CTA */} - - - - {isLocked ? t('home.unlockPremium') : t('home.startProgram')} - - - - - ) -} - -// ═══════════════════════════════════════════════════════════════════════════ -// STYLES -// ═══════════════════════════════════════════════════════════════════════════ - -const createStyles = (colors: ThemeColors, accentColor: string) => - StyleSheet.create({ - container: { - flex: 1, - backgroundColor: NAVY[900], - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - }, - - // Difficulty pills - pillsRow: { - flexDirection: 'row', - gap: SPACING[2], - marginBottom: SPACING[5], - }, - pill: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING[3], - borderRadius: RADIUS.PILL, - borderWidth: 1, - borderCurve: 'continuous', - }, - - // Results - resultCount: { - marginBottom: SPACING[4], - }, - - // Empty state - emptyState: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING[10], - }, - }) diff --git a/app/workout/category/CLAUDE.md b/app/workout/category/CLAUDE.md deleted file mode 100644 index 6c41707..0000000 --- a/app/workout/category/CLAUDE.md +++ /dev/null @@ -1,11 +0,0 @@ - -# Recent Activity - - - -### Apr 11, 2026 - -| ID | Time | T | Title | Read | -|----|------|---|-------|------| -| #6114 | 7:39 PM | 🔵 | Category detail screen imports reviewed | ~298 | - \ No newline at end of file diff --git a/app/workout/category/[id].tsx b/app/workout/category/[id].tsx deleted file mode 100644 index 553e05f..0000000 --- a/app/workout/category/[id].tsx +++ /dev/null @@ -1,246 +0,0 @@ -/** - * TabataFit Category Detail Screen - * Filtered workout list for a specific category with level sub-filter - */ - -import { useState, useMemo } from 'react' -import { View, StyleSheet, ScrollView, Pressable, Text as RNText } from 'react-native' -import { useRouter, useLocalSearchParams } from 'expo-router' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { Icon } from '@/src/shared/components/Icon' - -import { useTranslation } from 'react-i18next' - -import { useHaptics } from '@/src/shared/hooks' -import { getWorkoutsByCategory, CATEGORIES } from '@/src/shared/data' -import type { ProgramWorkout } from '@/src/shared/types/program' -import { useTranslatedCategories, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData' -import { StyledText } from '@/src/shared/components/StyledText' -import type { WorkoutCategory, WorkoutLevel } from '@/src/shared/types' - -import { useThemeColors, BRAND } from '@/src/shared/theme' -import type { ThemeColors } from '@/src/shared/theme/types' -import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' -import { RADIUS } from '@/src/shared/constants/borderRadius' -import { TEXT, GREEN } from '@/src/shared/constants/colors' -import { TYPOGRAPHY } from '@/src/shared/constants/typography' - -const LEVEL_IDS: (WorkoutLevel | 'all')[] = ['all', 'Beginner', 'Intermediate', 'Advanced'] - -export default function CategoryDetailScreen() { - const insets = useSafeAreaInsets() - const router = useRouter() - const haptics = useHaptics() - const { t } = useTranslation() - const { id } = useLocalSearchParams<{ id: string }>() - - const colors = useThemeColors() - const styles = useMemo(() => createStyles(colors), [colors]) - - const [selectedLevelIndex, setSelectedLevelIndex] = useState(0) - - const translatedCategories = useTranslatedCategories() - - const levelLabels = [ - t('screens:category.allLevels'), - t('levels.beginner'), - t('levels.intermediate'), - t('levels.advanced'), - ] - - const selectedLevel = LEVEL_IDS[selectedLevelIndex] - const category = translatedCategories.find(c => c.id === id) - const categoryLabel = category?.label ?? id ?? 'Category' - - const allWorkouts = useMemo( - () => (id && id !== 'all') ? getWorkoutsByCategory(id as WorkoutCategory) : [], - [id] - ) - - const filteredWorkouts = useMemo(() => { - if (selectedLevel === 'all') return allWorkouts - return allWorkouts.filter((w: ProgramWorkout) => w.focus?.[0] === selectedLevel) - }, [allWorkouts, selectedLevel]) - - const translatedWorkouts = useTranslatedWorkouts(filteredWorkouts) - - const handleBack = () => { - haptics.selection() - router.back() - } - - const handleWorkoutPress = (workoutId: string) => { - haptics.buttonTap() - router.push(`/workout/${workoutId}`) - } - - return ( - - {/* Header */} - - - - - {categoryLabel} - - - - {/* Level Filter — segmented pills */} - - - {levelLabels.map((label, idx) => ( - { - haptics.selection() - setSelectedLevelIndex(idx) - }} - > - - {label} - - - ))} - - - - - {t('plurals.workout', { count: translatedWorkouts.length })} - - - - {translatedWorkouts.map((workout: ProgramWorkout & { title: string }) => ( - handleWorkoutPress(workout.id)} - > - - - - - {workout.title} - - {workout.duration}min • {workout.focus?.[0] ?? 'Full Body'} - - - - - - - ))} - - {translatedWorkouts.length === 0 && ( - - - - No workouts found - - - )} - - - ) -} - -function createStyles(colors: ThemeColors) { - return StyleSheet.create({ - container: { - flex: 1, - backgroundColor: colors.bg.base, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingVertical: SPACING[3], - }, - backButton: { - width: 44, - height: 44, - alignItems: 'center', - justifyContent: 'center', - }, - filterContainer: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - marginBottom: SPACING[4], - }, - segmentedRow: { - flexDirection: 'row', - gap: SPACING[2], - }, - segment: { - flex: 1, - paddingVertical: SPACING[2], - paddingHorizontal: SPACING[3], - borderRadius: RADIUS.MD, - backgroundColor: colors.bg.surface, - borderWidth: 1, - borderColor: colors.border.dim, - alignItems: 'center', - }, - segmentActive: { - backgroundColor: GREEN.DIM, - borderColor: GREEN.BORDER, - }, - segmentText: { - fontSize: 13, - fontWeight: '500', - color: TEXT.TERTIARY, - }, - segmentTextActive: { - color: BRAND.PRIMARY, - fontWeight: '600', - }, - scrollView: { - flex: 1, - }, - scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - }, - workoutCard: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: SPACING[3], - paddingHorizontal: SPACING[4], - backgroundColor: colors.bg.surface, - borderRadius: RADIUS.LG, - marginBottom: SPACING[2], - gap: SPACING[3], - }, - workoutAvatar: { - width: 44, - height: 44, - borderRadius: RADIUS.FULL, - alignItems: 'center', - justifyContent: 'center', - }, - workoutInitial: { - ...TYPOGRAPHY.HEADING_2, - color: colors.text.primary, - }, - workoutInfo: { - flex: 1, - gap: 2, - }, - workoutMeta: { - alignItems: 'flex-end', - gap: 4, - }, - emptyState: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: SPACING[12], - }, - }) -} diff --git a/blob_https___www.3daistudio.png b/blob_https___www.3daistudio.png deleted file mode 100644 index 32df874..0000000 Binary files a/blob_https___www.3daistudio.png and /dev/null differ diff --git a/src/admin/components/AdminAuthProvider.tsx b/src/admin/components/AdminAuthProvider.tsx deleted file mode 100644 index 1f1f958..0000000 --- a/src/admin/components/AdminAuthProvider.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { createContext, useContext, useState, useEffect, ReactNode } from 'react' -import { adminService } from '../services/adminService' -import { logger } from '@/src/shared/utils/logger' - -interface AdminAuthContextType { - isAuthenticated: boolean - isAdmin: boolean - isLoading: boolean - signIn: (email: string, password: string) => Promise - signOut: () => Promise -} - -const AdminAuthContext = createContext(undefined) - -export function AdminAuthProvider({ children }: { children: ReactNode }) { - const [isAuthenticated, setIsAuthenticated] = useState(false) - const [isAdmin, setIsAdmin] = useState(false) - const [isLoading, setIsLoading] = useState(true) - - useEffect(() => { - checkAuth() - }, []) - - async function checkAuth() { - try { - const user = await adminService.getCurrentUser() - if (user) { - setIsAuthenticated(true) - const adminStatus = await adminService.isAdmin() - setIsAdmin(adminStatus) - } - } catch (error) { - logger.error('Auth check failed:', error) - } finally { - setIsLoading(false) - } - } - - const signIn = async (email: string, password: string) => { - await adminService.signIn(email, password) - setIsAuthenticated(true) - const adminStatus = await adminService.isAdmin() - setIsAdmin(adminStatus) - } - - const signOut = async () => { - await adminService.signOut() - setIsAuthenticated(false) - setIsAdmin(false) - } - - return ( - - {children} - - ) -} - -export function useAdminAuth() { - const context = useContext(AdminAuthContext) - if (context === undefined) { - throw new Error('useAdminAuth must be used within an AdminAuthProvider') - } - return context -} diff --git a/src/admin/services/adminService.ts b/src/admin/services/adminService.ts deleted file mode 100644 index ceab319..0000000 --- a/src/admin/services/adminService.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { supabase, isSupabaseConfigured } from '../../shared/supabase' -import type { Database } from '../../shared/supabase/database.types' - -type WorkoutInsert = Database['public']['Tables']['workouts']['Insert'] -type WorkoutUpdate = Database['public']['Tables']['workouts']['Update'] -type TrainerInsert = Database['public']['Tables']['trainers']['Insert'] -type TrainerUpdate = Database['public']['Tables']['trainers']['Update'] -type CollectionInsert = Database['public']['Tables']['collections']['Insert'] -type CollectionWorkoutInsert = Database['public']['Tables']['collection_workouts']['Insert'] - -export class AdminService { - private checkConfiguration(): boolean { - if (!isSupabaseConfigured()) { - throw new Error('Supabase is not configured. Please set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY') - } - return true - } - - // Workouts - async createWorkout(workout: Omit): Promise { - this.checkConfiguration() - - const { data, error } = await supabase - .from('workouts') - .insert(workout as any) - .select('id') - .single() - - if (error) { - throw new Error(`Failed to create workout: ${error.message}`) - } - - return (data as any).id - } - - async updateWorkout(id: string, workout: WorkoutUpdate): Promise { - this.checkConfiguration() - - const { error } = await (supabase - .from('workouts') as any) - .update({ ...workout, updated_at: new Date().toISOString() }) - .eq('id', id) - - if (error) { - throw new Error(`Failed to update workout: ${error.message}`) - } - } - - async deleteWorkout(id: string): Promise { - this.checkConfiguration() - - const { error } = await supabase - .from('workouts') - .delete() - .eq('id', id) - - if (error) { - throw new Error(`Failed to delete workout: ${error.message}`) - } - } - - // Trainers - async createTrainer(trainer: Omit): Promise { - this.checkConfiguration() - - const { data, error } = await supabase - .from('trainers') - .insert(trainer as any) - .select('id') - .single() - - if (error) { - throw new Error(`Failed to create trainer: ${error.message}`) - } - - return (data as any).id - } - - async updateTrainer(id: string, trainer: TrainerUpdate): Promise { - this.checkConfiguration() - - const { error } = await (supabase - .from('trainers') as any) - .update({ ...trainer, updated_at: new Date().toISOString() }) - .eq('id', id) - - if (error) { - throw new Error(`Failed to update trainer: ${error.message}`) - } - } - - async deleteTrainer(id: string): Promise { - this.checkConfiguration() - - const { error } = await supabase - .from('trainers') - .delete() - .eq('id', id) - - if (error) { - throw new Error(`Failed to delete trainer: ${error.message}`) - } - } - - // Collections - async createCollection( - collection: Omit, - workoutIds: string[] - ): Promise { - this.checkConfiguration() - - const { data: collectionData, error: collectionError } = await supabase - .from('collections') - .insert(collection as any) - .select('id') - .single() - - if (collectionError) { - throw new Error(`Failed to create collection: ${collectionError.message}`) - } - - const collectionWorkouts: CollectionWorkoutInsert[] = workoutIds.map((workoutId, index) => ({ - collection_id: (collectionData as any).id, - workout_id: workoutId, - sort_order: index, - })) - - const { error: linkError } = await supabase - .from('collection_workouts') - .insert(collectionWorkouts as any) - - if (linkError) { - throw new Error(`Failed to link workouts to collection: ${linkError.message}`) - } - - return (collectionData as any).id - } - - async updateCollectionWorkouts(collectionId: string, workoutIds: string[]): Promise { - this.checkConfiguration() - - const { error: deleteError } = await supabase - .from('collection_workouts') - .delete() - .eq('collection_id', collectionId) - - if (deleteError) { - throw new Error(`Failed to remove existing workouts: ${deleteError.message}`) - } - - const collectionWorkouts: CollectionWorkoutInsert[] = workoutIds.map((workoutId, index) => ({ - collection_id: collectionId, - workout_id: workoutId, - sort_order: index, - })) - - const { error: insertError } = await supabase - .from('collection_workouts') - .insert(collectionWorkouts as any) - - if (insertError) { - throw new Error(`Failed to add new workouts: ${insertError.message}`) - } - } - - // Storage - async uploadVideo(file: File, path: string): Promise { - this.checkConfiguration() - - const { error: uploadError } = await supabase.storage - .from('videos') - .upload(path, file) - - if (uploadError) { - throw new Error(`Failed to upload video: ${uploadError.message}`) - } - - const { data: { publicUrl } } = supabase.storage - .from('videos') - .getPublicUrl(path) - - return publicUrl - } - - async uploadThumbnail(file: File, path: string): Promise { - this.checkConfiguration() - - const { error: uploadError } = await supabase.storage - .from('thumbnails') - .upload(path, file) - - if (uploadError) { - throw new Error(`Failed to upload thumbnail: ${uploadError.message}`) - } - - const { data: { publicUrl } } = supabase.storage - .from('thumbnails') - .getPublicUrl(path) - - return publicUrl - } - - async uploadAvatar(file: File, path: string): Promise { - this.checkConfiguration() - - const { error: uploadError } = await supabase.storage - .from('avatars') - .upload(path, file) - - if (uploadError) { - throw new Error(`Failed to upload avatar: ${uploadError.message}`) - } - - const { data: { publicUrl } } = supabase.storage - .from('avatars') - .getPublicUrl(path) - - return publicUrl - } - - async deleteVideo(path: string): Promise { - this.checkConfiguration() - - const { error } = await supabase.storage - .from('videos') - .remove([path]) - - if (error) { - throw new Error(`Failed to delete video: ${error.message}`) - } - } - - async deleteThumbnail(path: string): Promise { - this.checkConfiguration() - - const { error } = await supabase.storage - .from('thumbnails') - .remove([path]) - - if (error) { - throw new Error(`Failed to delete thumbnail: ${error.message}`) - } - } - - // Admin authentication - async signIn(email: string, password: string): Promise { - this.checkConfiguration() - - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }) - - if (error) { - throw new Error(`Authentication failed: ${error.message}`) - } - } - - async signOut(): Promise { - await supabase.auth.signOut() - } - - async getCurrentUser() { - const { data: { user } } = await supabase.auth.getUser() - return user - } - - async isAdmin(): Promise { - const user = await this.getCurrentUser() - if (!user) return false - - const { data, error } = await supabase - .from('admin_users') - .select('*') - .eq('id', user.id) - .single() - - return !error && !!data - } -} - -export const adminService = new AdminService()