diff --git a/app/CLAUDE.md b/app/CLAUDE.md new file mode 100644 index 0000000..537f2ea --- /dev/null +++ b/app/CLAUDE.md @@ -0,0 +1,26 @@ + +# Recent Activity + + + +### Feb 19, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #5001 | 9:35 AM | 🔵 | Host Wrapper Located at Root Layout Level | ~153 | +| #4964 | 9:23 AM | 🔴 | Added Host Wrapper to Root Layout | ~228 | +| #4963 | 9:22 AM | ✅ | Root layout wraps Stack in View with pure black background | ~279 | +| #4910 | 8:16 AM | 🟣 | Added Workout Detail and Complete Screen Routes | ~348 | + +### Feb 20, 2026 + +| ID | Time | T | Title | Read | +|----|------|---|-------|------| +| #5224 | 1:24 PM | ✅ | Stage v1.1 files prepared for git commit - SwiftUI Button refactoring complete | ~434 | +| #5206 | 1:03 PM | ⚖️ | SwiftUI component usage mandated for TabataFit app | ~349 | +| #5115 | 8:57 AM | 🔵 | Root Layout Stack Configuration with Screen Animations | ~256 | +| #5061 | 8:47 AM | 🔵 | Expo Router Tab Navigation Structure Found | ~196 | +| #5053 | 8:23 AM | ✅ | Completed removal of all Host wrappers from application | ~255 | +| #5052 | " | ✅ | Removed Host wrapper from root layout entirely | ~224 | +| #5019 | 8:13 AM | 🔵 | Root layout properly wraps Stack with Host component | ~198 | + \ No newline at end of file diff --git a/app/_layout.tsx b/app/_layout.tsx index f13a8c1..ab66c29 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,60 +1,88 @@ -import { useEffect } from 'react'; -import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; -import { Stack } from 'expo-router'; -import { StatusBar } from 'expo-status-bar'; -import * as SplashScreen from 'expo-splash-screen'; -import 'react-native-reanimated'; +/** + * TabataFit Root Layout + * Expo Router v3 + Inter font loading + */ -import { useColorScheme } from '@/hooks/use-color-scheme'; -import { useIsOnboardingComplete } from '@/src/features/onboarding/hooks/useOnboarding'; +import { useCallback } from 'react' +import { Stack } from 'expo-router' +import { StatusBar } from 'expo-status-bar' +import { View } from 'react-native' +import * as SplashScreen from 'expo-splash-screen' +import { + useFonts, + Inter_400Regular, + Inter_500Medium, + Inter_600SemiBold, + Inter_700Bold, + Inter_900Black, +} from '@expo-google-fonts/inter' -// Prevent splash screen from auto-hiding -SplashScreen.preventAutoHideAsync(); +import { DARK } from '@/src/shared/constants/colors' -export const unstable_settings = { - anchor: '(tabs)', -}; +SplashScreen.preventAutoHideAsync() export default function RootLayout() { - const colorScheme = useColorScheme(); - const isOnboardingComplete = useIsOnboardingComplete(); + const [fontsLoaded] = useFonts({ + Inter_400Regular, + Inter_500Medium, + Inter_600SemiBold, + Inter_700Bold, + Inter_900Black, + }) - // Hide splash screen once we have a definite state - useEffect(() => { - if (isOnboardingComplete !== undefined) { - SplashScreen.hideAsync(); + const onLayoutRootView = useCallback(async () => { + if (fontsLoaded) { + await SplashScreen.hideAsync() } - }, [isOnboardingComplete]); + }, [fontsLoaded]) - // Show nothing while Zustand hydrates from AsyncStorage - if (isOnboardingComplete === undefined) { - return null; + if (!fontsLoaded) { + return null } return ( - - - + + - - - - - - - ); + > + + + + + + + + + ) } diff --git a/app/collection/[id].tsx b/app/collection/[id].tsx new file mode 100644 index 0000000..9beec10 --- /dev/null +++ b/app/collection/[id].tsx @@ -0,0 +1,211 @@ +/** + * TabataFit Collection Detail Screen + * Shows collection info + ordered workout list + */ + +import { 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 { LinearGradient } from 'expo-linear-gradient' +import Ionicons from '@expo/vector-icons/Ionicons' + +import { useHaptics } from '@/src/shared/hooks' +import { getCollectionById, getCollectionWorkouts, getTrainerById, COLLECTION_COLORS } from '@/src/shared/data' +import { StyledText } from '@/src/shared/components/StyledText' + +import { + BRAND, + DARK, + TEXT, +} from '@/src/shared/constants/colors' +import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' +import { RADIUS } from '@/src/shared/constants/borderRadius' + +export default function CollectionDetailScreen() { + const insets = useSafeAreaInsets() + const router = useRouter() + const haptics = useHaptics() + const { id } = useLocalSearchParams<{ id: string }>() + + const collection = id ? getCollectionById(id) : null + const workouts = useMemo( + () => id ? getCollectionWorkouts(id) : [], + [id] + ) + const collectionColor = COLLECTION_COLORS[id ?? ''] ?? BRAND.PRIMARY + + const handleBack = () => { + haptics.selection() + router.back() + } + + const handleWorkoutPress = (workoutId: string) => { + haptics.buttonTap() + router.push(`/workout/${workoutId}`) + } + + if (!collection) { + return ( + + Collection not found + + ) + } + + const totalMinutes = workouts.reduce((sum, w) => sum + (w?.duration ?? 0), 0) + const totalCalories = workouts.reduce((sum, w) => sum + (w?.calories ?? 0), 0) + + return ( + + + {/* Hero Header */} + + + + + + + + + {collection.icon} + {collection.title} + {collection.description} + + + + + {workouts.length + ' workouts'} + + + + {totalMinutes + ' min total'} + + + + {totalCalories + ' cal'} + + + + + + {/* Workout List */} + + {workouts.map((workout, index) => { + if (!workout) return null + const trainer = getTrainerById(workout.trainerId) + return ( + handleWorkoutPress(workout.id)} + > + + {index + 1} + + + {workout.title} + + {trainer?.name + ' \u00B7 ' + workout.duration + ' min \u00B7 ' + workout.level} + + + + {workout.calories + ' cal'} + + + + ) + })} + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: DARK.BASE, + }, + scrollView: { + flex: 1, + }, + scrollContent: {}, + + // Hero + hero: { + height: 260, + overflow: 'hidden', + }, + backButton: { + width: 44, + height: 44, + alignItems: 'center', + justifyContent: 'center', + margin: SPACING[3], + }, + heroContent: { + position: 'absolute', + bottom: SPACING[5], + left: SPACING[5], + right: SPACING[5], + }, + heroIcon: { + fontSize: 40, + marginBottom: SPACING[2], + }, + heroStats: { + flexDirection: 'row', + gap: SPACING[4], + marginTop: SPACING[3], + }, + heroStat: { + flexDirection: 'row', + alignItems: 'center', + gap: SPACING[1], + }, + + // Workout List + workoutList: { + paddingHorizontal: LAYOUT.SCREEN_PADDING, + paddingTop: SPACING[4], + gap: SPACING[2], + }, + workoutCard: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: SPACING[3], + paddingHorizontal: SPACING[4], + backgroundColor: DARK.SURFACE, + borderRadius: RADIUS.LG, + gap: SPACING[3], + }, + workoutNumber: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + }, + workoutNumberText: { + fontSize: 15, + fontWeight: '700', + }, + workoutInfo: { + flex: 1, + gap: 2, + }, + workoutMeta: { + alignItems: 'flex-end', + gap: 4, + }, +}) diff --git a/app/workout/category/[id].tsx b/app/workout/category/[id].tsx new file mode 100644 index 0000000..15d24a0 --- /dev/null +++ b/app/workout/category/[id].tsx @@ -0,0 +1,209 @@ +/** + * 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 Ionicons from '@expo/vector-icons/Ionicons' +import { + Host, + Picker, +} from '@expo/ui/swift-ui' + +import { useHaptics } from '@/src/shared/hooks' +import { getWorkoutsByCategory, getTrainerById, CATEGORIES } from '@/src/shared/data' +import { StyledText } from '@/src/shared/components/StyledText' +import type { WorkoutCategory, WorkoutLevel } from '@/src/shared/types' + +import { + BRAND, + DARK, + TEXT, +} from '@/src/shared/constants/colors' +import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' +import { RADIUS } from '@/src/shared/constants/borderRadius' + +const LEVELS: { id: WorkoutLevel | 'all'; label: string }[] = [ + { id: 'all', label: 'All Levels' }, + { id: 'Beginner', label: 'Beginner' }, + { id: 'Intermediate', label: 'Intermediate' }, + { id: 'Advanced', label: 'Advanced' }, +] + +export default function CategoryDetailScreen() { + const insets = useSafeAreaInsets() + const router = useRouter() + const haptics = useHaptics() + const { id } = useLocalSearchParams<{ id: string }>() + + const [selectedLevelIndex, setSelectedLevelIndex] = useState(0) + + const selectedLevel = LEVELS[selectedLevelIndex].id + const category = CATEGORIES.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 => w.level === selectedLevel) + }, [allWorkouts, selectedLevel]) + + const handleBack = () => { + haptics.selection() + router.back() + } + + const handleWorkoutPress = (workoutId: string) => { + haptics.buttonTap() + router.push(`/workout/${workoutId}`) + } + + return ( + + {/* Header */} + + + + + {categoryLabel} + + + + {/* Level Filter */} + + + { + haptics.selection() + setSelectedLevelIndex(e.nativeEvent.index) + }} + variant="segmented" + options={LEVELS.map(l => l.label)} + color={BRAND.PRIMARY} + /> + + + + + {filteredWorkouts.length + ' workouts'} + + + + {filteredWorkouts.map((workout) => { + const trainer = getTrainerById(workout.trainerId) + return ( + handleWorkoutPress(workout.id)} + > + + {trainer?.name[0] ?? 'T'} + + + {workout.title} + + {trainer?.name + ' \u00B7 ' + workout.duration + ' min \u00B7 ' + workout.level} + + + + {workout.calories + ' cal'} + + + + ) + })} + + {filteredWorkouts.length === 0 && ( + + + + No workouts found + + + )} + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: DARK.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], + }, + scrollView: { + flex: 1, + }, + scrollContent: { + paddingHorizontal: LAYOUT.SCREEN_PADDING, + }, + workoutCard: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: SPACING[3], + paddingHorizontal: SPACING[4], + backgroundColor: DARK.SURFACE, + borderRadius: RADIUS.LG, + marginBottom: SPACING[2], + gap: SPACING[3], + }, + workoutAvatar: { + width: 44, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + }, + workoutInitial: { + fontSize: 18, + fontWeight: '700', + color: TEXT.PRIMARY, + }, + workoutInfo: { + flex: 1, + gap: 2, + }, + workoutMeta: { + alignItems: 'flex-end', + gap: 4, + }, + emptyState: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: SPACING[12], + }, +})