# Skill — Exercices (useExercise + ExerciseDisplay)
> Lis ce skill AVANT d'implémenter quoi que ce soit lié aux exercices.

## Responsabilité de ce module
Gérer la bibliothèque d'exercices, la sélection selon le programme,
et l'affichage contextuel selon la phase du timer.
**Offline-first** : toutes les données et les GIFs sont dans le bundle.

---

## Architecture du module exercices

```
src/features/exercises/
  types.ts
  hooks/
    useExercise.ts            ← sélection + navigation entre exercices
    useExerciseLibrary.ts     ← recherche, filtrage, favoris
  components/
    ExerciseDisplay.tsx       ← affichage contextuel selon la phase timer
    ExerciseGif.tsx           ← Image GIF avec fallback + accessibilité
    ExerciseCard.tsx          ← carte pour la bibliothèque/programme
    ExerciseCues.tsx          ← affichage des cues de forme
  data/
    exercises.ts              ← les 38 exercices (source de vérité)
    categories.ts             ← définition des catégories
  index.ts
```

---

## Types

```typescript
// src/features/exercises/types.ts

export type ExerciseCategory =
  | 'CARDIO'
  | 'LOWER_BODY'
  | 'UPPER_BODY'
  | 'CORE'
  | 'LOW_IMPACT'
  | 'EQUIPMENT'

export type ExerciseDifficulty = 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED'

export interface Exercise {
  id: string
  name: Record<string, string>              // clés = codes langue : { fr, en, es, de, pt }
  category: ExerciseCategory
  difficulty: ExerciseDifficulty
  musclesTargeted: string[]
  description: Record<string, string>       // max 80 caractères par langue
  cues: Record<string, string[]>            // 2-3 points clés de forme par langue
  gifAsset: number | null                   // require(...) ou null si pas de GIF
  thumbnailAsset: number | null
  hasModification: boolean                  // variante plus facile disponible
  modificationId: string | null             // id de l'exercice modifié
  equipmentNeeded: string[]                 // [] = aucun matériel
  estimatedCaloriesPerMinute: number        // approximatif
}

export type ProgramMode = 'SINGLE' | 'DUO' | 'CIRCUIT_4' | 'FREE'

export interface ExerciseProgram {
  id: string
  name: Record<string, string>
  description: Record<string, string>
  mode: ProgramMode
  // Pour SINGLE : [exerciseId] — même exercice répété
  // Pour DUO : [exerciseIdA, exerciseIdB] — alternance
  // Pour CIRCUIT_4 : [a, b, c, d] — 4 exercices × 2 rounds
  // Pour FREE : exerciseId par round [r1, r2, r3, r4, r5, r6, r7, r8]
  exerciseIds: string[]
  difficulty: ExerciseDifficulty
  totalRounds: number
  isDefault: boolean                        // programmes pré-définis vs créés par user
}

// Résout l'exercice pour un round donné selon le mode du programme
export interface ExerciseState {
  current: Exercise
  next: Exercise | null                     // null sur le dernier round
  roundIndex: number                        // 0-indexé
}
```

---

## Données — les 38 exercices V1

Structure à respecter pour chaque exercice :

```typescript
// src/features/exercises/data/exercises.ts
import { Exercise } from '../types'

export const EXERCISES: Exercise[] = [
  {
    id: 'burpee_classic',
    name: { fr: 'Burpee', en: 'Burpee', es: 'Burpee', de: 'Burpee', pt: 'Burpee' },
    category: 'CARDIO',
    difficulty: 'INTERMEDIATE',
    musclesTargeted: ['quadriceps', 'pectoraux', 'épaules', 'cardio'],
    description: {
      fr: 'Position debout → squat → planche → pompe → saut vertical',
      en: 'Stand → squat → plank → push-up → jump',
      es: 'De pie → sentadilla → plancha → flexión → salto',
      de: 'Stehen → Hocke → Plank → Liegestütz → Sprung',
      pt: 'Em pé → agachamento → prancha → flexão → salto',
    },
    cues: {
      fr: ['Atterris doucement sur les orteils', 'Garde le dos droit en planche', 'Explose vers le haut'],
      en: ['Land softly on toes', 'Keep back flat in plank', 'Explode upward'],
      es: ['Aterriza suavemente', 'Espalda recta en plancha', 'Explota hacia arriba'],
      de: ['Sanft auf den Zehen landen', 'Rücken gerade halten', 'Nach oben explodieren'],
      pt: ['Aterrissar suavemente', 'Costas retas na prancha', 'Explodir para cima'],
    },
    gifAsset: require('../../../assets/exercises/burpee_classic.gif'),
    thumbnailAsset: require('../../../assets/exercises/thumbs/burpee_classic.jpg'),
    hasModification: true,
    modificationId: 'burpee_modified',
    equipmentNeeded: [],
    estimatedCaloriesPerMinute: 12,
  },
  // ... 37 autres exercices
]

// Accès rapide par ID
export const EXERCISES_MAP = Object.fromEntries(
  EXERCISES.map(ex => [ex.id, ex])
) as Record<string, Exercise>

export function getExerciseById(id: string): Exercise {
  const ex = EXERCISES_MAP[id]
  if (!ex) throw new Error(`Exercice non trouvé : ${id}`)
  return ex
}
```

**Liste des 38 exercices V1 à implémenter :**
```
CARDIO (8)        : burpee_classic, burpee_modified, jumping_jacks,
                    mountain_climbers, high_knees, jump_rope_sim,
                    box_jump_sim, lateral_shuffles

LOWER_BODY (8)    : squat_classic, squat_jump, lunge_alternating,
                    lunge_jump, glute_bridge, wall_sit, calf_raises,
                    sumo_squat

UPPER_BODY (6)    : pushup_classic, pushup_modified, pike_pushup,
                    tricep_dip, shoulder_taps, inchworm

CORE (6)          : crunch_classic, plank_hold, russian_twist,
                    bicycle_crunch, leg_raises, dead_bug

LOW_IMPACT (6)    : march_in_place, step_touch, modified_squat,
                    standing_oblique, slow_pushup, chair_stand

EQUIPMENT (4)     : kb_swing, db_thruster, resistance_band_row,
                    medicine_ball_slam
```

---

## Hook useExercise

```typescript
// src/features/exercises/hooks/useExercise.ts

export function useExercise(program: ExerciseProgram): {
  getExerciseForRound: (round: number) => ExerciseState
  currentExercise: Exercise
  nextExercise: Exercise | null
} {
  function getExerciseForRound(round: number): ExerciseState {
    // round est 1-indexé
    const index = round - 1

    let currentId: string
    let nextId: string | null

    switch (program.mode) {
      case 'SINGLE':
        currentId = program.exerciseIds[0]
        nextId = null  // même exercice → pas de "suivant" différent
        break

      case 'DUO':
        currentId = program.exerciseIds[index % 2]
        nextId = program.exerciseIds[(index + 1) % 2]
        break

      case 'CIRCUIT_4':
        currentId = program.exerciseIds[index % 4]
        nextId = program.exerciseIds[(index + 1) % 4]
        break

      case 'FREE':
        currentId = program.exerciseIds[index] ?? program.exerciseIds[0]
        nextId = program.exerciseIds[index + 1] ?? null
        break
    }

    return {
      current: getExerciseById(currentId),
      next: nextId ? getExerciseById(nextId) : null,
      roundIndex: index,
    }
  }

  return { getExerciseForRound, /* ... */ }
}
```

---

## Composant ExerciseDisplay — affichage contextuel

Le composant adapte son layout selon la phase du timer.

```typescript
interface ExerciseDisplayProps {
  phase: TimerPhase
  exercise: Exercise
  nextExercise: Exercise | null
  lang: string                    // code langue actuel
}

export function ExerciseDisplay({ phase, exercise, nextExercise, lang }: ExerciseDisplayProps) {
  switch (phase) {
    case 'GET_READY':
      return <GetReadyView exercise={exercise} lang={lang} />
      // ↑ GIF grand format + nom + "Prépare-toi !"

    case 'WORK':
      return <WorkView exercise={exercise} lang={lang} />
      // ↑ Nom en haut + 2 cues + GIF petit coin bas-droit

    case 'REST':
      return <RestView exercise={exercise} nextExercise={nextExercise} lang={lang} />
      // ↑ "Repos" + si nextExercise : "Prochain : [nom]" + vignette

    default:
      return null
  }
}
```

### GetReadyView
```
┌─────────────────────────────┐
│                             │
│    ┌─────────────────┐      │
│    │   [GIF 200x200] │      │  ← Grand GIF centré
│    └─────────────────┘      │
│                             │
│       Burpees               │  ← Nom, police bold 28px
│   ← Dos droit en planche    │  ← Cue #1
│   ← Explose vers le haut    │  ← Cue #2
│                             │
└─────────────────────────────┘
```

### WorkView
```
┌─────────────────────────────┐
│  Burpees                    │  ← Nom, 22px, coin haut-gauche
│  ← Dos droit                │  ← Cue #1, 16px
│  ← Explose vers le haut     │  ← Cue #2, 16px
│                             │
│      [Timer central]        │
│                             │
│                  ┌────┐     │
│                  │GIF │     │  ← GIF 80x80, coin bas-droit
│                  └────┘     │
└─────────────────────────────┘
```

### RestView
```
┌─────────────────────────────┐
│                             │
│         REPOS               │  ← "REPOS" centré, grand
│                             │
│   Prochain : Mountain       │  ← Si nextExercise != null
│   Climbers                  │
│   ┌──────┐                  │
│   │ GIF  │                  │  ← Vignette 60x60 de nextExercise
│   └──────┘                  │
└─────────────────────────────┘
```

---

## Composant ExerciseGif — robuste et accessible

```typescript
interface ExerciseGifProps {
  exercise: Exercise
  size: 'small' | 'medium' | 'large'
  lang: string
}

const GIF_SIZES = { small: 80, medium: 120, large: 200 }

export function ExerciseGif({ exercise, size, lang }: ExerciseGifProps) {
  const [hasError, setHasError] = useState(false)
  const dimension = GIF_SIZES[size]

  if (!exercise.gifAsset || hasError) {
    // Fallback : icône + initiales de l'exercice
    return (
      <View
        style={[styles.fallback, { width: dimension, height: dimension }]}
        accessible={true}
        accessibilityLabel={exercise.name[lang] ?? exercise.name['en']}
      >
        <Text style={styles.fallbackIcon}>💪</Text>
        <Text style={styles.fallbackText}>{getInitials(exercise.name[lang])}</Text>
      </View>
    )
  }

  return (
    <Image
      source={exercise.gifAsset}
      style={{ width: dimension, height: dimension }}
      onError={() => setHasError(true)}
      accessible={true}
      accessibilityLabel={`Démonstration de ${exercise.name[lang]}`}
    />
  )
}
```

---

## Localisation des exercices

```typescript
// Utilitaire pour extraire la bonne langue avec fallback
export function getLocalizedText(
  record: Record<string, string>,
  lang: string
): string {
  return record[lang] ?? record['en'] ?? Object.values(record)[0] ?? ''
}

// Utilisation
const name = getLocalizedText(exercise.name, userLang)
const cues = (exercise.cues[userLang] ?? exercise.cues['en'] ?? [])
```

---

## Programmes pré-définis V1

```typescript
// src/features/exercises/data/defaultPrograms.ts

export const DEFAULT_PROGRAMS: ExerciseProgram[] = [
  {
    id: 'beginner_classic',
    name: { fr: 'Débutant Classic', en: 'Beginner Classic' },
    description: { fr: 'Parfait pour commencer le Tabata', en: 'Perfect to start Tabata' },
    mode: 'SINGLE',
    exerciseIds: ['squat_classic'],
    difficulty: 'BEGINNER',
    totalRounds: 8,
    isDefault: true,
  },
  {
    id: 'full_body_duo',
    name: { fr: 'Full Body Duo', en: 'Full Body Duo' },
    description: { fr: 'Squats + Pompes — le duo parfait', en: 'Squats + Push-ups — the perfect duo' },
    mode: 'DUO',
    exerciseIds: ['squat_jump', 'pushup_classic'],
    difficulty: 'INTERMEDIATE',
    totalRounds: 8,
    isDefault: true,
  },
  {
    id: 'cardio_blast',
    name: { fr: 'Cardio Blast', en: 'Cardio Blast' },
    description: { fr: 'Circuit intense 4 exercices cardio', en: '4-exercise intense cardio circuit' },
    mode: 'CIRCUIT_4',
    exerciseIds: ['burpee_classic', 'high_knees', 'mountain_climbers', 'jumping_jacks'],
    difficulty: 'ADVANCED',
    totalRounds: 8,
    isDefault: true,
  },
  // + programmes par objectif (perte de poids, force, low impact, etc.)
]
```

---

## Tests minimaux

```typescript
describe('useExercise', () => {
  it('MODE SINGLE : retourne le même exercice à chaque round', () => {})
  it('MODE DUO : alterne A/B correctement sur 8 rounds', () => {})
  it('MODE CIRCUIT_4 : cycle sur 4 exercices', () => {})
  it('retourne null pour nextExercise sur le dernier round en FREE', () => {})
  it('getExerciseById lance une erreur si id inconnu', () => {})
})

describe('ExerciseDisplay', () => {
  it('affiche le GIF grand format en GET_READY', () => {})
  it('affiche les cues en WORK', () => {})
  it('affiche nextExercise en REST si disponible', () => {})
  it('affiche le fallback si gifAsset est null', () => {})
  it('est accessible (accessibilityLabel présent)', () => {})
})
```

---

## Checklist assets exercices

Avant de lancer en production, vérifier :
- [ ] 38 GIFs présents dans `assets/exercises/` (200×200px, loop, < 200KB chacun)
- [ ] 38 thumbnails présents dans `assets/exercises/thumbs/` (< 30KB chacun)
- [ ] Toutes les traductions présentes pour EN, FR, ES, DE, PT
- [ ] Tous les `modificationId` pointent vers un exercice existant
- [ ] Bundle total des assets exercices < 10MB
