feat: category/collection detail screens + Inter font loading

New screens:
- Category detail (workout/category/[id]): filtered workout list
  with SwiftUI segmented Picker for level sub-filter
- Collection detail (collection/[id]): hero header with gradient,
  stats (workouts/minutes/calories), numbered workout list

Root layout:
- Inter font loading (400-900 weights) via @expo-google-fonts
- SplashScreen integration for font loading gate
- Route config for all screens with appropriate animations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-20 13:24:35 +01:00
parent b0521ded5a
commit 2d24831f8e
4 changed files with 520 additions and 46 deletions

View File

@@ -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 (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen
name="onboarding"
options={{
<View style={{ flex: 1, backgroundColor: DARK.BASE }} onLayout={onLayoutRootView}>
<StatusBar style="light" />
<Stack
screenOptions={{
headerShown: false,
presentation: 'fullScreenModal',
animation: 'fade',
gestureEnabled: false,
contentStyle: { backgroundColor: DARK.BASE },
animation: 'slide_from_right',
}}
/>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="timer"
options={{
headerShown: false,
presentation: 'fullScreenModal',
animation: 'fade',
}}
/>
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
);
>
<Stack.Screen name="(tabs)" />
<Stack.Screen
name="workout/[id]"
options={{
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen
name="workout/category/[id]"
options={{
animation: 'slide_from_right',
}}
/>
<Stack.Screen
name="collection/[id]"
options={{
animation: 'slide_from_right',
}}
/>
<Stack.Screen
name="player/[id]"
options={{
presentation: 'fullScreenModal',
animation: 'fade',
}}
/>
<Stack.Screen
name="complete/[id]"
options={{
animation: 'fade',
}}
/>
</Stack>
</View>
)
}