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:
214
app/_layout.tsx
214
app/_layout.tsx
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user