Onboarding: - 6-screen flow: Problem → Empathy → Solution → Wow → Personalization → Paywall - useOnboarding hook with Zustand + AsyncStorage persistence - MiniTimerDemo with live 20s timer + haptics - Auto-redirect for first-time users - Mock RevenueCat for dev testing Audio: - useAudioEngine hook with expo-av - Phase sounds (count_3/2/1, beep, bell, fanfare) - Placeholder music tracks Design System: - Typography component + constants - GlassView component - Spacing, shadows, animations, borderRadius constants - Extended color palette (phase gradients, glass, surfaces) Timer: - Fix: handle 0-duration phases (immediate advance) - Enhanced TimerDisplay with phase gradients Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
157 lines
3.8 KiB
TypeScript
157 lines
3.8 KiB
TypeScript
import { useEffect, useRef } from 'react'
|
|
import {
|
|
Animated,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from 'react-native'
|
|
import { useRouter, Redirect } from 'expo-router'
|
|
import { StatusBar } from 'expo-status-bar'
|
|
import { LinearGradient } from 'expo-linear-gradient'
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
|
|
import { BRAND, TEXT, APP_GRADIENTS, ACCENT } from '@/src/shared/constants/colors'
|
|
import { TYPOGRAPHY } from '@/src/shared/constants/typography'
|
|
import { SHADOW, TEXT_SHADOW } from '@/src/shared/constants/shadows'
|
|
import { DURATION, EASING } from '@/src/shared/constants/animations'
|
|
import { useIsOnboardingComplete } from '@/src/features/onboarding/hooks/useOnboarding'
|
|
|
|
export default function HomeScreen() {
|
|
const router = useRouter()
|
|
const insets = useSafeAreaInsets()
|
|
const isOnboardingComplete = useIsOnboardingComplete()
|
|
|
|
const glowAnim = useRef(new Animated.Value(0)).current
|
|
|
|
// Show nothing while Zustand hydrates
|
|
if (isOnboardingComplete === undefined) {
|
|
return null
|
|
}
|
|
|
|
// Redirect to onboarding if not complete
|
|
if (isOnboardingComplete === false) {
|
|
return <Redirect href="/onboarding" />
|
|
}
|
|
|
|
useEffect(() => {
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(glowAnim, {
|
|
toValue: 1,
|
|
duration: DURATION.BREATH,
|
|
easing: EASING.STANDARD,
|
|
useNativeDriver: false,
|
|
}),
|
|
Animated.timing(glowAnim, {
|
|
toValue: 0,
|
|
duration: DURATION.BREATH,
|
|
easing: EASING.STANDARD,
|
|
useNativeDriver: false,
|
|
}),
|
|
])
|
|
).start()
|
|
}, [])
|
|
|
|
const glowOpacity = glowAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [0.15, 0.4],
|
|
})
|
|
|
|
const glowScale = glowAnim.interpolate({
|
|
inputRange: [0, 1],
|
|
outputRange: [1, 1.12],
|
|
})
|
|
|
|
return (
|
|
<LinearGradient
|
|
colors={APP_GRADIENTS.HOME}
|
|
style={[styles.container, { paddingTop: insets.top + 40 }]}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
>
|
|
<StatusBar style="light" />
|
|
|
|
<View style={styles.brandArea}>
|
|
<Text style={styles.title}>TABATA</Text>
|
|
<Text style={styles.subtitle}>GO</Text>
|
|
<Text style={styles.tagline}>4 minutes. Tout donner.</Text>
|
|
</View>
|
|
|
|
<View style={styles.buttonArea}>
|
|
<Animated.View
|
|
style={[
|
|
styles.buttonGlow,
|
|
{ opacity: glowOpacity, transform: [{ scale: glowScale }] },
|
|
]}
|
|
/>
|
|
<Pressable
|
|
style={({ pressed }) => [
|
|
styles.startButton,
|
|
pressed && styles.startButtonPressed,
|
|
]}
|
|
onPress={() => router.push('/timer')}
|
|
>
|
|
<Text style={styles.startButtonText}>START</Text>
|
|
</Pressable>
|
|
</View>
|
|
</LinearGradient>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
brandArea: {
|
|
alignItems: 'center',
|
|
},
|
|
title: {
|
|
...TYPOGRAPHY.brandTitle,
|
|
color: BRAND.PRIMARY,
|
|
...TEXT_SHADOW.BRAND,
|
|
},
|
|
subtitle: {
|
|
...TYPOGRAPHY.displaySmall,
|
|
color: TEXT.PRIMARY,
|
|
marginTop: -6,
|
|
},
|
|
tagline: {
|
|
...TYPOGRAPHY.caption,
|
|
color: TEXT.HINT,
|
|
fontStyle: 'italic',
|
|
marginTop: 12,
|
|
},
|
|
buttonArea: {
|
|
marginTop: 72,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
buttonGlow: {
|
|
position: 'absolute',
|
|
width: 172,
|
|
height: 172,
|
|
borderRadius: 86,
|
|
backgroundColor: ACCENT.ORANGE,
|
|
},
|
|
startButton: {
|
|
width: 160,
|
|
height: 160,
|
|
borderRadius: 80,
|
|
backgroundColor: BRAND.PRIMARY,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
...SHADOW.BRAND_GLOW,
|
|
},
|
|
startButtonPressed: {
|
|
transform: [{ scale: 0.95 }],
|
|
},
|
|
startButtonText: {
|
|
...TYPOGRAPHY.buttonHero,
|
|
color: TEXT.PRIMARY,
|
|
letterSpacing: 4,
|
|
},
|
|
})
|