feat: onboarding flow (6 screens) + audio engine + design system

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>
This commit is contained in:
Millian Lamiaux
2026-02-17 21:52:23 +01:00
parent 31bdb1586f
commit fa189fe72e
55 changed files with 3361 additions and 320 deletions

View File

@@ -1,67 +1,156 @@
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useRouter } from 'expo-router'
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 (
<View style={[styles.container, { paddingTop: insets.top + 24 }]}>
<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" />
<Text style={styles.title}>TABATAGO</Text>
<Text style={styles.subtitle}>Entraînement Tabata</Text>
<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>
<Pressable
style={({ pressed }) => [
styles.startButton,
pressed && styles.startButtonPressed,
]}
onPress={() => router.push('/timer')}
>
<Text style={styles.startButtonText}>START</Text>
</Pressable>
</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,
backgroundColor: '#1E1E2E',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
brandArea: {
alignItems: 'center',
},
title: {
fontSize: 44,
fontWeight: '900',
color: '#FFFFFF',
letterSpacing: 6,
...TYPOGRAPHY.brandTitle,
color: BRAND.PRIMARY,
...TEXT_SHADOW.BRAND,
},
subtitle: {
fontSize: 18,
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: '500',
...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: '#F97316',
backgroundColor: BRAND.PRIMARY,
alignItems: 'center',
justifyContent: 'center',
marginTop: 60,
...SHADOW.BRAND_GLOW,
},
startButtonPressed: {
opacity: 0.7,
transform: [{ scale: 0.95 }],
},
startButtonText: {
fontSize: 32,
fontWeight: '900',
color: '#FFFFFF',
...TYPOGRAPHY.buttonHero,
color: TEXT.PRIMARY,
letterSpacing: 4,
},
})