refactor: extract player components, add stack headers, add tests
- Extract player UI into src/features/player/ components (TimerRing, BurnBar, etc.) - Add transparent stack headers for workout/[id] and program/[id] screens - Refactor workout/[id], program/[id], complete/[id] screens - Add player feature tests and useTimer integration tests - Add data layer exports and test setup improvements
This commit is contained in:
@@ -15,7 +15,6 @@ import {
|
||||
} from 'react-native'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { BlurView } from 'expo-blur'
|
||||
import { Icon, type IconName } from '@/src/shared/components/Icon'
|
||||
|
||||
@@ -24,7 +23,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useHaptics } from '@/src/shared/hooks'
|
||||
import { useActivityStore, useUserStore } from '@/src/shared/stores'
|
||||
import { getWorkoutById, getPopularWorkouts } from '@/src/shared/data'
|
||||
import { getWorkoutById, getPopularWorkouts, getTrainerById, getWorkoutAccentColor } from '@/src/shared/data'
|
||||
import { useTranslatedWorkout, useTranslatedWorkouts } from '@/src/shared/data/useTranslatedData'
|
||||
import { SyncConsentModal } from '@/src/shared/components/SyncConsentModal'
|
||||
import { enableSync } from '@/src/shared/services/sync'
|
||||
@@ -95,6 +94,7 @@ function PrimaryButton({
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
const isDark = colors.colorScheme === 'dark'
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current
|
||||
|
||||
@@ -121,14 +121,15 @@ function PrimaryButton({
|
||||
onPressOut={handlePressOut}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Animated.View style={[styles.primaryButton, { transform: [{ scale: scaleAnim }] }]}>
|
||||
<LinearGradient
|
||||
colors={[BRAND.PRIMARY, BRAND.PRIMARY_LIGHT]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<RNText style={styles.primaryButtonText}>{children}</RNText>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.primaryButton,
|
||||
{ backgroundColor: isDark ? '#FFFFFF' : '#000000', transform: [{ scale: scaleAnim }] },
|
||||
]}
|
||||
>
|
||||
<RNText style={[styles.primaryButtonText, { color: isDark ? '#000000' : '#FFFFFF' }]}>
|
||||
{children}
|
||||
</RNText>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
)
|
||||
@@ -190,11 +191,13 @@ function StatCard({
|
||||
value,
|
||||
label,
|
||||
icon,
|
||||
accentColor,
|
||||
delay = 0,
|
||||
}: {
|
||||
value: string | number
|
||||
label: string
|
||||
icon: IconName
|
||||
accentColor: string
|
||||
delay?: number
|
||||
}) {
|
||||
const colors = useThemeColors()
|
||||
@@ -215,14 +218,14 @@ function StatCard({
|
||||
return (
|
||||
<Animated.View style={[styles.statCard, { transform: [{ scale: scaleAnim }] }]}>
|
||||
<BlurView intensity={colors.glass.blurMedium} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
|
||||
<Icon name={icon} size={24} tintColor={BRAND.PRIMARY} />
|
||||
<Icon name={icon} size={24} tintColor={accentColor} />
|
||||
<RNText style={styles.statValue}>{value}</RNText>
|
||||
<RNText style={styles.statLabel}>{label}</RNText>
|
||||
</Animated.View>
|
||||
)
|
||||
}
|
||||
|
||||
function BurnBarResult({ percentile }: { percentile: number }) {
|
||||
function BurnBarResult({ percentile, accentColor }: { percentile: number; accentColor: string }) {
|
||||
const { t } = useTranslation()
|
||||
const colors = useThemeColors()
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
@@ -245,9 +248,9 @@ function BurnBarResult({ percentile }: { percentile: number }) {
|
||||
return (
|
||||
<View style={styles.burnBarContainer}>
|
||||
<RNText style={styles.burnBarTitle}>{t('screens:complete.burnBar')}</RNText>
|
||||
<RNText style={styles.burnBarResult}>{t('screens:complete.burnBarResult', { percentile })}</RNText>
|
||||
<RNText style={[styles.burnBarResult, { color: accentColor }]}>{t('screens:complete.burnBarResult', { percentile })}</RNText>
|
||||
<View style={styles.burnBarTrack}>
|
||||
<Animated.View style={[styles.burnBarFill, { width: barWidth }]} />
|
||||
<Animated.View style={[styles.burnBarFill, { width: barWidth, backgroundColor: accentColor }]} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -269,6 +272,8 @@ export default function WorkoutCompleteScreen() {
|
||||
|
||||
const rawWorkout = getWorkoutById(id ?? '1')
|
||||
const workout = useTranslatedWorkout(rawWorkout)
|
||||
const trainer = rawWorkout ? getTrainerById(rawWorkout.trainerId) : undefined
|
||||
const trainerColor = getWorkoutAccentColor(id ?? '1')
|
||||
const streak = useActivityStore((s) => s.streak)
|
||||
const history = useActivityStore((s) => s.history)
|
||||
const recentWorkouts = history.slice(0, 1)
|
||||
@@ -378,20 +383,20 @@ export default function WorkoutCompleteScreen() {
|
||||
|
||||
{/* Stats Grid */}
|
||||
<View style={styles.statsGrid}>
|
||||
<StatCard value={resultCalories} label={t('screens:complete.caloriesLabel')} icon="flame.fill" delay={100} />
|
||||
<StatCard value={resultMinutes} label={t('screens:complete.minutesLabel')} icon="clock.fill" delay={200} />
|
||||
<StatCard value="100%" label={t('screens:complete.completeLabel')} icon="checkmark.circle.fill" delay={300} />
|
||||
<StatCard value={resultCalories} label={t('screens:complete.caloriesLabel')} icon="flame.fill" accentColor={trainerColor} delay={100} />
|
||||
<StatCard value={resultMinutes} label={t('screens:complete.minutesLabel')} icon="clock.fill" accentColor={trainerColor} delay={200} />
|
||||
<StatCard value="100%" label={t('screens:complete.completeLabel')} icon="checkmark.circle.fill" accentColor={trainerColor} delay={300} />
|
||||
</View>
|
||||
|
||||
{/* Burn Bar */}
|
||||
<BurnBarResult percentile={burnBarPercentile} />
|
||||
<BurnBarResult percentile={burnBarPercentile} accentColor={trainerColor} />
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* Streak */}
|
||||
<View style={styles.streakSection}>
|
||||
<View style={styles.streakBadge}>
|
||||
<Icon name="flame.fill" size={32} tintColor={BRAND.PRIMARY} />
|
||||
<View style={[styles.streakBadge, { backgroundColor: trainerColor + '26' }]}>
|
||||
<Icon name="flame.fill" size={32} tintColor={trainerColor} />
|
||||
</View>
|
||||
<View style={styles.streakInfo}>
|
||||
<RNText style={styles.streakTitle}>{t('screens:complete.streakTitle', { count: streak.current })}</RNText>
|
||||
@@ -421,12 +426,8 @@ export default function WorkoutCompleteScreen() {
|
||||
style={styles.recommendedCard}
|
||||
>
|
||||
<BlurView intensity={colors.glass.blurLight} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
|
||||
<View style={styles.recommendedThumb}>
|
||||
<LinearGradient
|
||||
colors={[BRAND.PRIMARY, BRAND.PRIMARY_LIGHT]}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<Icon name="flame.fill" size={24} tintColor="#FFFFFF" />
|
||||
<View style={[styles.recommendedThumb, { backgroundColor: trainerColor + '20' }]}>
|
||||
<Icon name="flame.fill" size={24} tintColor={trainerColor} />
|
||||
</View>
|
||||
<RNText style={styles.recommendedTitleText} numberOfLines={1}>{w.title}</RNText>
|
||||
<RNText style={styles.recommendedDurationText}>{t('units.minUnit', { count: w.duration })}</RNText>
|
||||
@@ -500,7 +501,6 @@ function createStyles(colors: ThemeColors) {
|
||||
},
|
||||
primaryButtonText: {
|
||||
...TYPOGRAPHY.HEADLINE,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
},
|
||||
buttonIcon: {
|
||||
@@ -588,7 +588,6 @@ function createStyles(colors: ThemeColors) {
|
||||
},
|
||||
burnBarResult: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
color: BRAND.PRIMARY,
|
||||
marginTop: SPACING[1],
|
||||
marginBottom: SPACING[3],
|
||||
},
|
||||
@@ -600,7 +599,6 @@ function createStyles(colors: ThemeColors) {
|
||||
},
|
||||
burnBarFill: {
|
||||
height: '100%',
|
||||
backgroundColor: BRAND.PRIMARY,
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
@@ -622,7 +620,6 @@ function createStyles(colors: ThemeColors) {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: 'rgba(255, 107, 53, 0.15)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user