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:
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user