From e0e02c4550c915baa9e3db4985db856d0a3391d6 Mon Sep 17 00:00:00 2001 From: Millian Lamiaux Date: Mon, 13 Apr 2026 22:19:29 +0200 Subject: [PATCH] fix: align program detail screen with Dark Medical design system Replace hardcoded #000 backgrounds with NAVY[900], use design system tokens for badges (TYPOGRAPHY.LABEL, RADIUS.SM), progress bar (RADIUS.PILL, 4px height), and CTA container (DARK.SCRIM). Co-Authored-By: Claude Opus 4.6 --- app/program/[id].tsx | 746 ++++++++++++------------------------------- 1 file changed, 207 insertions(+), 539 deletions(-) diff --git a/app/program/[id].tsx b/app/program/[id].tsx index d66e8bc..275e3af 100644 --- a/app/program/[id].tsx +++ b/app/program/[id].tsx @@ -1,592 +1,260 @@ /** - * TabataFit Program Detail Screen - * Clean scrollable layout — native header, Apple Fitness+ style + * Tabata Kine Program Detail Screen + * Displays program overview, weeks, sessions, and progression for kiné programs */ -import React, { useEffect, useRef } from 'react' -import { - View, - Text as RNText, - StyleSheet, - ScrollView, - Pressable, - Animated, -} from 'react-native' +import React from 'react' +import { View, Text, StyleSheet, ScrollView, Pressable } from 'react-native' import { Stack, useRouter, useLocalSearchParams } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { Icon } from '@/src/shared/components/Icon' -import { useTranslation } from 'react-i18next' -import { useHaptics } from '@/src/shared/hooks' -import { useProgramStore } from '@/src/shared/stores' -import { PROGRAMS } from '@/src/shared/data/programs' -import { track } from '@/src/shared/services/analytics' - -import { useThemeColors, BRAND } from '@/src/shared/theme' +import { useKineProgramStore } from '@/src/shared/stores/kineProgramStore' +import { getKineProgramById, getKineSessionsByWeek } from '@/src/shared/data/kine' +import { canAccessProgram } from '@/src/shared/services/access' +import { useUserStore } from '@/src/shared/stores/userStore' +import type { KineProgramId } from '@/src/shared/types/program' import { TYPOGRAPHY } from '@/src/shared/constants/typography' -import { SPACING, LAYOUT } from '@/src/shared/constants/spacing' +import { SPACING } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' -import { SPRING } from '@/src/shared/constants/animations' -import type { ProgramId } from '@/src/shared/types' -import type { IconName } from '@/src/shared/components/Icon' +import { TEXT, NAVY, GREEN, BORDER_COLORS, AMBER, DARK } from '@/src/shared/constants/colors' +import { withOpacity } from '@/src/shared/utils/color' -// Per-program accent colors (matches home screen cards) -const PROGRAM_ACCENT: Record = { - 'upper-body': { color: '#FF6B35', icon: 'dumbbell' }, - 'lower-body': { color: '#30D158', icon: 'figure.walk' }, - 'full-body': { color: '#5AC8FA', icon: 'flame' }, -} - -export default function ProgramDetailScreen() { +export default function KineProgramDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>() - const programId = id as ProgramId - const { t } = useTranslation('screens') - const insets = useSafeAreaInsets() const router = useRouter() - const haptics = useHaptics() - const colors = useThemeColors() - const isDark = colors.colorScheme === 'dark' + const insets = useSafeAreaInsets() - const program = PROGRAMS[programId] - const accent = PROGRAM_ACCENT[programId] ?? PROGRAM_ACCENT['full-body'] - const selectProgram = useProgramStore((s) => s.selectProgram) - const progress = useProgramStore((s) => s.programsProgress[programId]) - const isWeekUnlocked = useProgramStore((s) => s.isWeekUnlocked) - const getCurrentWorkout = useProgramStore((s) => s.getCurrentWorkout) - const completion = useProgramStore((s) => s.getProgramCompletion(programId)) + const programId = id as KineProgramId + const program = getKineProgramById(programId) - // CTA entrance animation - const ctaAnim = useRef(new Animated.Value(0)).current - useEffect(() => { - Animated.sequence([ - Animated.delay(300), - Animated.spring(ctaAnim, { - toValue: 1, - ...SPRING.GENTLE, - useNativeDriver: true, - }), - ]).start() - }, []) + const selectProgram = useKineProgramStore(s => s.selectProgram) + const progress = useKineProgramStore(s => s.programsProgress[programId]) + const isWeekUnlocked = useKineProgramStore(s => s.isWeekUnlocked) + const getCurrentSession = useKineProgramStore(s => s.getCurrentSession) + const completion = useKineProgramStore(s => s.getProgramCompletion(programId)) + const getProgramStatus = useKineProgramStore(s => s.getProgramStatus) - useEffect(() => { - if (program) { - track('program_detail_viewed', { - program_id: programId, - program_title: program.title, - }) - } - }, [programId]) + const isPremium = useUserStore(s => s.profile.subscription) !== 'free' + const canAccess = canAccessProgram(programId, isPremium) + const status = getProgramStatus(programId) if (!program) { return ( - <> - - - - {t('programs.notFound', { defaultValue: 'Program not found' })} - - - + + + Programme non trouvé + ) } const handleStartProgram = () => { - haptics.phaseChange() selectProgram(programId) - const currentWorkout = getCurrentWorkout(programId) - if (currentWorkout) { - router.push(`/workout/${currentWorkout.id}`) + const session = getCurrentSession(programId) + if (session) { + router.push(`/workout/${session.id}`) } } - const handleWorkoutPress = (workoutId: string) => { - haptics.buttonTap() - router.push(`/workout/${workoutId}`) + const handleSessionPress = (sessionId: string) => { + router.push(`/workout/${sessionId}`) } - const hasStarted = progress.completedWorkoutIds.length > 0 - const ctaBg = isDark ? '#FFFFFF' : '#000000' - const ctaTextColor = isDark ? '#000000' : '#FFFFFF' - - const ctaLabel = hasStarted - ? progress.isProgramCompleted - ? t('programs.restartProgram') - : t('programs.continueTraining') - : t('programs.startProgram') - return ( - <> - + + - - - {/* Icon + Title */} - - - + + {/* Program header */} + + + + + {program.title} + {program.description} + + {/* Tier badge */} + + + {program.tier === 'free' ? 'GRATUIT' : 'PREMIUM'} + + + + {/* Stats row */} + + + {program.durationWeeks} + Semaines - - - {program.title} - - - {program.durationWeeks} {t('programs.weeks')} · {program.totalWorkouts} {t('programs.workouts')} - + + {program.sessionsPerWeek} + Séances/sem + + + {program.totalSessions} + Séances - {/* Description */} - - {program.description} - - - {/* Stats Card */} - - - - - {program.durationWeeks} - - - {t('programs.weeks')} - + {/* Progress */} + {status === 'in-progress' && ( + + + - - - - {program.totalWorkouts} - - - {t('programs.workouts')} - - - - - - 4 - - - {t('programs.minutes')} - - - - - - {/* Equipment & Focus */} - - {program.equipment.required.length > 0 && ( - <> - - {t('programs.equipment')} - - - {program.equipment.required.map((item) => ( - - - {item} - - - ))} - {program.equipment.optional.map((item) => ( - - - {item} {t('programs.optional')} - - - ))} - - - )} - - - {t('programs.focusAreas')} - - - {program.focusAreas.map((area) => ( - - - {area} - - - ))} - - - - {/* Separator */} - - - {/* Progress (if started) */} - {hasStarted && ( - - - - {t('programs.yourProgress')} - - - {completion}% - - - - - - - {progress.completedWorkoutIds.length} {t('programs.of')} {program.totalWorkouts} {t('programs.workoutsComplete')} - + {completion}% complété )} + - {/* Training Plan */} - - {t('programs.trainingPlan')} - - - {program.weeks.map((week) => { - const isUnlocked = isWeekUnlocked(programId, week.weekNumber) - const isCurrentWeek = progress.currentWeek === week.weekNumber - const weekCompletion = week.workouts.filter((w) => - progress.completedWorkoutIds.includes(w.id) - ).length - - return ( - - {/* Week Header */} - - - - {week.title} - - {!isUnlocked && ( - - )} - {isCurrentWeek && isUnlocked && ( - - - {t('programs.current')} - - - )} - - - {week.description} - - {weekCompletion > 0 && ( - - {weekCompletion}/{week.workouts.length} {t('programs.complete')} - - )} - - - {/* Week Workouts */} - {isUnlocked && - week.workouts.map((workout, index) => { - const isCompleted = progress.completedWorkoutIds.includes(workout.id) - const isWorkoutLocked = - !isCompleted && - index > 0 && - !progress.completedWorkoutIds.includes(week.workouts[index - 1].id) && - week.weekNumber === progress.currentWeek - - return ( - - - [ - s.workoutRow, - isWorkoutLocked && { opacity: 0.4 }, - pressed && !isWorkoutLocked && { opacity: 0.6 }, - ]} - onPress={() => !isWorkoutLocked && handleWorkoutPress(workout.id)} - disabled={isWorkoutLocked} - > - - {isCompleted ? ( - - ) : isWorkoutLocked ? ( - - ) : ( - - {index + 1} - - )} - - - - {workout.title} - - - {workout.exercises.length} {t('programs.exercises')} · {workout.duration} {t('programs.min')} - - - {!isWorkoutLocked && !isCompleted && ( - - )} - - - ) - })} + {/* Principles */} + {program.principles.length > 0 && ( + + Principes + {program.principles.map((p, i) => ( + + + {p} - ) - })} - + ))} + + )} - {/* CTA */} - - [ - s.ctaButton, - { backgroundColor: ctaBg }, - pressed && { opacity: 0.85, transform: [{ scale: 0.98 }] }, - ]} - onPress={handleStartProgram} - > - - {ctaLabel} - + {/* Weeks */} + {program.weeks.map(week => { + const unlocked = isWeekUnlocked(programId, week.weekNumber) + return ( + + + Semaine {week.weekNumber}: {week.title} + {week.isDeload && ( + + Décharge + + )} + {!unlocked && ( + + )} + + {week.focus} + + {/* Sessions */} + {week.sessions.map(session => { + const isCompleted = progress?.completedSessionIds.includes(session.id) ?? false + const sessionLocked = !unlocked + + return ( + !sessionLocked && canAccess && handleSessionPress(session.id)} + disabled={sessionLocked || !canAccess} + > + + + + + Séance {session.order}: {session.title} + + + {session.blocks.length} bloc{session.blocks.length > 1 ? 's' : ''} · {session.totalDuration} min · {session.calories} cal + + + {isCompleted && } + {!canAccess && !sessionLocked && } + + + ) + })} + + ) + })} + + {/* Completion criteria */} + {program.completionCriteria.length > 0 && ( + + Critères de passage + {program.completionCriteria.map((c, i) => ( + + + {c} + + ))} + + )} + + + {/* CTA */} + + {canAccess ? ( + + + {status === 'in-progress' ? 'Continuer le programme' : status === 'completed' ? 'Recommencer' : 'Commencer le programme'} + - + ) : ( + router.push('/paywall')}> + Débloquer avec Premium + + )} - + ) } -// ─── Styles ────────────────────────────────────────────────────────────────── +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: NAVY[900] }, + scrollView: { flex: 1 }, -const s = StyleSheet.create({ - container: { - flex: 1, - }, - centered: { - alignItems: 'center', - justifyContent: 'center', - }, - scrollContent: { - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingTop: SPACING[2], - }, + heroSection: { padding: SPACING[6], alignItems: 'center' }, + iconCircle: { width: 64, height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', marginBottom: SPACING[3] }, + programTitle: { ...TYPOGRAPHY.LARGE_TITLE, color: TEXT.PRIMARY, textAlign: 'center' }, + programDescription: { ...TYPOGRAPHY.BODY, color: TEXT.SECONDARY, textAlign: 'center', marginTop: SPACING[2], lineHeight: 22 }, + tierBadge: { marginTop: SPACING[3], paddingHorizontal: SPACING[2], paddingVertical: 3, borderRadius: RADIUS.SM, borderWidth: 1 }, + tierBadgeText: { ...TYPOGRAPHY.LABEL }, - // Title - titleRow: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[3], - marginBottom: SPACING[3], - }, - programIcon: { - width: 44, - height: 44, - borderRadius: RADIUS.MD, - borderCurve: 'continuous', - alignItems: 'center', - justifyContent: 'center', - }, - titleContent: { - flex: 1, - }, - title: { - ...TYPOGRAPHY.TITLE_1, - }, - subtitle: { - ...TYPOGRAPHY.SUBHEADLINE, - marginTop: 2, - }, + statsRow: { flexDirection: 'row', marginTop: SPACING[6], gap: SPACING[8] }, + statItem: { alignItems: 'center' }, + statValue: { ...TYPOGRAPHY.TITLE_2, color: TEXT.PRIMARY }, + statLabel: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: 2 }, - // Description - description: { - ...TYPOGRAPHY.BODY, - lineHeight: 24, - marginBottom: SPACING[5], - }, + progressSection: { marginTop: SPACING[4], width: '100%' }, + progressBar: { height: 4, borderRadius: RADIUS.PILL, backgroundColor: BORDER_COLORS.DIM, overflow: 'hidden' }, + progressFill: { height: '100%', borderRadius: RADIUS.PILL }, + progressText: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[1], textAlign: 'center' }, - // Card - card: { - borderRadius: RADIUS.LG, - borderCurve: 'continuous', - overflow: 'hidden', - padding: SPACING[4], - }, + section: { paddingHorizontal: SPACING[5], marginTop: SPACING[6] }, + sectionTitle: { ...TYPOGRAPHY.HEADLINE, color: TEXT.PRIMARY, marginBottom: SPACING[3] }, + principleItem: { flexDirection: 'row', gap: SPACING[2], marginBottom: SPACING[2] }, + principleBullet: { color: TEXT.TERTIARY, fontSize: 14 }, + principleText: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.SECONDARY, flex: 1, lineHeight: 20 }, - // Stats - statsRow: { - flexDirection: 'row', - alignItems: 'center', - }, - statItem: { - flex: 1, - alignItems: 'center', - gap: 2, - }, - statValue: { - ...TYPOGRAPHY.TITLE_1, - fontVariant: ['tabular-nums'], - }, - statLabel: { - ...TYPOGRAPHY.CAPTION_2, - textTransform: 'uppercase' as const, - letterSpacing: 0.5, - }, - statDivider: { - width: StyleSheet.hairlineWidth, - height: 32, - }, + weekSection: { paddingHorizontal: SPACING[5], marginTop: SPACING[6] }, + weekHeader: { flexDirection: 'row', alignItems: 'center', gap: SPACING[2] }, + weekTitle: { ...TYPOGRAPHY.TITLE_3, color: TEXT.PRIMARY, flex: 1 }, + weekFocus: { ...TYPOGRAPHY.CAPTION_1, color: TEXT.TERTIARY, marginTop: SPACING[1], marginBottom: SPACING[3] }, + deloadBadge: { backgroundColor: withOpacity(AMBER[500], 0.2), paddingHorizontal: SPACING[2], paddingVertical: 2, borderRadius: RADIUS.SM }, + deloadText: { ...TYPOGRAPHY.LABEL, color: AMBER[500] }, - // Tags - tagsSection: { - marginTop: SPACING[5], - marginBottom: SPACING[5], - }, - tagSectionLabel: { - ...TYPOGRAPHY.FOOTNOTE, - fontWeight: '600', - marginBottom: SPACING[2], - }, - tagRow: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: SPACING[2], - }, - tag: { - paddingHorizontal: SPACING[3], - paddingVertical: SPACING[1], - borderRadius: RADIUS.FULL, - }, - tagText: { - ...TYPOGRAPHY.CAPTION_1, - }, + sessionCard: { backgroundColor: NAVY[800], borderRadius: RADIUS.MD, padding: SPACING[3], marginBottom: SPACING[2], borderWidth: 1, borderColor: BORDER_COLORS.DIM }, + sessionCardLocked: { opacity: 0.5 }, + sessionInfo: { flexDirection: 'row', alignItems: 'center', gap: SPACING[3] }, + sessionDot: { width: 8, height: 8, borderRadius: 4 }, + sessionTitle: { ...TYPOGRAPHY.SUBHEADLINE, color: TEXT.PRIMARY }, + sessionMeta: { ...TYPOGRAPHY.CAPTION_2, color: TEXT.TERTIARY, marginTop: 2 }, - // Separator - separator: { - height: StyleSheet.hairlineWidth, - marginBottom: SPACING[5], - }, - - // Progress - progressHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: SPACING[3], - }, - progressTrack: { - height: 6, - borderRadius: 3, - overflow: 'hidden', - }, - progressFill: { - height: '100%', - borderRadius: 3, - }, - - // Section title - sectionTitle: { - ...TYPOGRAPHY.TITLE_2, - marginBottom: SPACING[4], - }, - - // Week header - weekHeader: { - marginBottom: SPACING[1], - }, - weekTitleRow: { - flexDirection: 'row', - alignItems: 'center', - gap: SPACING[2], - }, - currentBadge: { - paddingHorizontal: SPACING[2], - paddingVertical: 2, - borderRadius: RADIUS.SM, - }, - currentBadgeText: { - ...TYPOGRAPHY.CAPTION_2, - fontWeight: '600', - }, - - // Workout row - workoutSep: { - height: StyleSheet.hairlineWidth, - marginLeft: SPACING[4] + 28, - }, - workoutRow: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: SPACING[3], - gap: SPACING[3], - }, - workoutIcon: { - width: 28, - alignItems: 'center', - justifyContent: 'center', - }, - workoutIndex: { - ...TYPOGRAPHY.SUBHEADLINE, - fontVariant: ['tabular-nums'], - fontWeight: '600', - }, - workoutInfo: { - flex: 1, - gap: 2, - }, - - // Bottom bar - bottomBar: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - paddingHorizontal: LAYOUT.SCREEN_PADDING, - paddingTop: SPACING[3], - }, - ctaButton: { - height: 54, - borderRadius: RADIUS.MD, - borderCurve: 'continuous', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'row', - }, - ctaText: { - ...TYPOGRAPHY.BUTTON_LARGE, - }, + ctaContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, paddingHorizontal: SPACING[5], paddingTop: SPACING[3], backgroundColor: DARK.SCRIM, borderTopWidth: 1, borderTopColor: BORDER_COLORS.DIM }, + ctaButton: { height: 52, borderRadius: RADIUS.MD, alignItems: 'center', justifyContent: 'center' }, + ctaText: { ...TYPOGRAPHY.BUTTON_MEDIUM, color: NAVY[900], letterSpacing: 0.5 }, + errorText: { color: TEXT.SECONDARY, textAlign: 'center', marginTop: 100 }, })