feat: integrate theme and i18n across all screens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Millian Lamiaux
2026-02-21 00:05:14 +01:00
parent f17125e231
commit f80798069b
17 changed files with 3127 additions and 2402 deletions

View File

@@ -1,13 +1,18 @@
/**
* TabataFit Root Layout
* Expo Router v3 + Inter font loading
* Waits for font + store hydration before rendering
*/
import { useCallback } from 'react'
import '@/src/shared/i18n'
import '@/src/shared/i18n/types'
import { useState, useEffect, 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 * as Notifications from 'expo-notifications'
import {
useFonts,
Inter_400Regular,
@@ -17,11 +22,29 @@ import {
Inter_900Black,
} from '@expo-google-fonts/inter'
import { DARK } from '@/src/shared/constants/colors'
import { PostHogProvider } from 'posthog-react-native'
import { ThemeProvider, useThemeColors } from '@/src/shared/theme'
import { useUserStore } from '@/src/shared/stores'
import { useNotifications } from '@/src/shared/hooks'
import { initializePurchases } from '@/src/shared/services/purchases'
import { initializeAnalytics, getPostHogClient } from '@/src/shared/services/analytics'
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: false,
shouldPlaySound: false,
shouldSetBadge: false,
shouldShowBanner: false,
shouldShowList: false,
}),
})
SplashScreen.preventAutoHideAsync()
export default function RootLayout() {
function RootLayoutInner() {
const colors = useThemeColors()
const [fontsLoaded] = useFonts({
Inter_400Regular,
Inter_500Medium,
@@ -30,59 +53,106 @@ export default function RootLayout() {
Inter_900Black,
})
useNotifications()
// Wait for persisted store to hydrate from AsyncStorage
const [hydrated, setHydrated] = useState(useUserStore.persist.hasHydrated())
useEffect(() => {
const unsub = useUserStore.persist.onFinishHydration(() => setHydrated(true))
return unsub
}, [])
// Initialize RevenueCat + PostHog after hydration
useEffect(() => {
if (hydrated) {
initializePurchases().catch((err) => {
console.error('Failed to initialize RevenueCat:', err)
})
initializeAnalytics().catch((err) => {
console.error('Failed to initialize PostHog:', err)
})
}
}, [hydrated])
const onLayoutRootView = useCallback(async () => {
if (fontsLoaded) {
if (fontsLoaded && hydrated) {
await SplashScreen.hideAsync()
}
}, [fontsLoaded])
}, [fontsLoaded, hydrated])
if (!fontsLoaded) {
if (!fontsLoaded || !hydrated) {
return null
}
const content = (
<View style={{ flex: 1, backgroundColor: colors.bg.base }} onLayout={onLayoutRootView}>
<StatusBar style={colors.statusBarStyle} />
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: colors.bg.base },
animation: 'slide_from_right',
}}
>
<Stack.Screen name="(tabs)" />
<Stack.Screen
name="onboarding"
options={{
animation: 'fade',
}}
/>
<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>
)
// Skip PostHogProvider in dev to avoid SDK errors without a real API key
if (__DEV__) {
return content
}
return (
<View style={{ flex: 1, backgroundColor: DARK.BASE }} onLayout={onLayoutRootView}>
<StatusBar style="light" />
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: DARK.BASE },
animation: 'slide_from_right',
}}
>
<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>
<PostHogProvider client={getPostHogClient() ?? undefined} autocapture={{ captureScreens: true }}>
{content}
</PostHogProvider>
)
}
export default function RootLayout() {
return (
<ThemeProvider>
<RootLayoutInner />
</ThemeProvider>
)
}