refactor screens, navigation & player for new architecture

Simplify Home, Activity, Profile, Complete, Player, and Program screens
to work with the new Supabase-driven data layer. Update root and tab
layouts. Add Settings, Terms, and Zone routes. Add OfflineBanner
component and progressStore. Update all player sub-components to use
the refreshed design system tokens.
This commit is contained in:
Millian Lamiaux
2026-04-21 21:50:48 +02:00
parent 04b83fc419
commit 5888aac08e
26 changed files with 1836 additions and 2772 deletions

View File

@@ -1,33 +1,26 @@
/**
* TabataFit Root Layout
* Expo Router v3 + Inter font loading
* Waits for font + store hydration before rendering
* Expo Router v3 — SF Pro system font (no custom font loading)
* Waits for store hydration before rendering
*/
import '@/src/shared/i18n'
import '@/src/shared/i18n/types'
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback, Component } from 'react'
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import { View } from 'react-native'
import { View, Text, Pressable, StyleSheet } from 'react-native'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
import {
useFonts,
Inter_400Regular,
Inter_500Medium,
Inter_600SemiBold,
Inter_700Bold,
Inter_900Black,
} from '@expo-google-fonts/inter'
import { PostHogProvider } from 'posthog-react-native'
import { ThemeProvider, useThemeColors } from '@/src/shared/theme'
import { TEXT, NAVY } from '@/src/shared/constants/colors'
import { TEXT, NAVY, GREEN } from '@/src/shared/constants/colors'
import { useUserStore } from '@/src/shared/stores'
import { useNotifications } from '@/src/shared/hooks'
import { OfflineBanner } from '@/src/shared/components/OfflineBanner'
import { initializePurchases } from '@/src/shared/services/purchases'
import { initializeAnalytics, getPostHogClient, trackScreen } from '@/src/shared/services/analytics'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
@@ -44,6 +37,84 @@ Notifications.setNotificationHandler({
SplashScreen.preventAutoHideAsync()
// ─── Error Boundary (F-108) ────────────────────────────────────────────────
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
class ErrorBoundary extends Component<{ children: React.ReactNode }, ErrorBoundaryState> {
state: ErrorBoundaryState = { hasError: false, error: null }
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, info.componentStack)
}
private handleRetry = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
return (
<View style={errorStyles.container}>
<Text style={errorStyles.emoji}></Text>
<Text style={errorStyles.title}>Something went wrong</Text>
<Text style={errorStyles.message}>
{this.state.error?.message ?? 'An unexpected error occurred.'}
</Text>
<Pressable style={errorStyles.button} onPress={this.handleRetry}>
<Text style={errorStyles.buttonText}>Try again</Text>
</Pressable>
</View>
)
}
return this.props.children
}
}
const errorStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: NAVY[900],
alignItems: 'center',
justifyContent: 'center',
padding: 32,
},
emoji: { fontSize: 48, marginBottom: 16 },
title: {
fontSize: 22,
fontWeight: '700',
color: TEXT.PRIMARY,
marginBottom: 8,
},
message: {
fontSize: 15,
fontWeight: '400',
color: TEXT.SECONDARY,
textAlign: 'center',
marginBottom: 24,
lineHeight: 20,
},
button: {
backgroundColor: '#00C896',
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
borderCurve: 'continuous',
minHeight: 44,
},
buttonText: {
fontSize: 17,
fontWeight: '600',
color: NAVY[900],
},
})
// Create React Query Client
const queryClient = new QueryClient({
defaultOptions: {
@@ -59,14 +130,6 @@ const queryClient = new QueryClient({
function RootLayoutInner() {
const colors = useThemeColors()
const [fontsLoaded] = useFonts({
Inter_400Regular,
Inter_500Medium,
Inter_600SemiBold,
Inter_700Bold,
Inter_900Black,
})
useNotifications()
// Wait for persisted store to hydrate from AsyncStorage
@@ -90,12 +153,12 @@ function RootLayoutInner() {
}, [hydrated])
const onLayoutRootView = useCallback(async () => {
if (fontsLoaded && hydrated) {
if (hydrated) {
await SplashScreen.hideAsync()
}
}, [fontsLoaded, hydrated])
}, [hydrated])
if (!fontsLoaded || !hydrated) {
if (!hydrated) {
return null
}
@@ -103,85 +166,62 @@ function RootLayoutInner() {
<QueryClientProvider client={queryClient}>
<View style={{ flex: 1, backgroundColor: colors.bg.base }} onLayout={onLayoutRootView}>
<StatusBar style={colors.statusBarStyle} />
<OfflineBanner />
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: colors.bg.base },
animation: 'slide_from_right',
animation: 'default',
headerBackButtonDisplayMode: 'minimal',
headerTintColor: GREEN[500],
headerStyle: { backgroundColor: colors.bg.base },
headerShadowVisible: false,
headerTitleStyle: { fontWeight: '600', fontSize: 17, color: colors.text.primary },
}}
>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="onboarding"
options={{
animation: 'fade',
}}
options={{ animation: 'fade' }}
/>
<Stack.Screen
name="workout/[id]"
options={{
headerShown: true,
headerStyle: { backgroundColor: colors.bg.base },
headerShadowVisible: false,
headerTitle: '',
headerBackButtonDisplayMode: 'minimal',
headerTintColor: colors.text.primary,
animation: 'slide_from_right',
}}
/>
<Stack.Screen
name="workout/category/[id]"
options={{
animation: 'slide_from_right',
}}
/>
<Stack.Screen
name="workout/body-zone/[id]"
options={{
headerShown: true,
headerStyle: { backgroundColor: colors.bg.base },
headerShadowVisible: false,
headerTitle: '',
headerBackButtonDisplayMode: 'minimal',
headerTintColor: colors.text.primary,
animation: 'slide_from_right',
}}
name="zone/[bodyZone]"
options={{ headerShown: true, headerTitle: '' }}
/>
<Stack.Screen
name="program/[id]"
options={{
headerShown: true,
headerStyle: { backgroundColor: colors.bg.base },
headerShadowVisible: false,
headerTitle: '',
headerBackButtonDisplayMode: 'minimal',
headerTintColor: colors.text.primary,
animation: 'slide_from_right',
}}
/>
<Stack.Screen
name="collection/[id]"
options={{
animation: 'slide_from_right',
}}
/>
<Stack.Screen
name="assessment"
options={{
animation: 'slide_from_right',
}}
options={{ headerShown: true, headerTitle: '' }}
/>
<Stack.Screen
name="player/[id]"
options={{
presentation: 'fullScreenModal',
animation: 'fade',
}}
options={{ presentation: 'fullScreenModal', animation: 'fade' }}
/>
<Stack.Screen
name="complete/[id]"
options={{ animation: 'fade' }}
/>
<Stack.Screen
name="settings"
options={{
animation: 'fade',
presentation: 'formSheet',
sheetGrabberVisible: true,
sheetAllowedDetents: [0.75, 1.0],
}}
/>
<Stack.Screen
name="terms"
options={{ headerShown: true, headerTitle: '' }}
/>
<Stack.Screen
name="privacy"
options={{ headerShown: true, headerTitle: '' }}
/>
<Stack.Screen
name="paywall"
options={{
presentation: 'formSheet',
sheetGrabberVisible: true,
sheetAllowedDetents: [0.85, 1.0],
}}
/>
</Stack>
@@ -211,8 +251,10 @@ function RootLayoutInner() {
export default function RootLayout() {
return (
<ThemeProvider>
<RootLayoutInner />
</ThemeProvider>
<ErrorBoundary>
<ThemeProvider>
<RootLayoutInner />
</ThemeProvider>
</ErrorBoundary>
)
}