# Story 4.6: Page Challenge - Structure et puzzle

Status: ready-for-dev

## Story

As a visiteur,
I want relever un défi optionnel avant d'accéder au contact,
so that l'accès au développeur est une récompense méritée (mais pas bloquante).

## Acceptance Criteria

1. **Given** le visiteur accède à `/challenge` (après avoir débloqué le contact) **When** la page se charge **Then** une introduction narrative "Une dernière épreuve..." s'affiche
2. **And** un puzzle logique/code simple est présenté (réordonner, compléter, décoder)
3. **And** la difficulté est calibrée : 1-3 minutes pour résoudre
4. **And** le thème est lié au développement/code
5. **And** un système d'indices est disponible (bouton "Besoin d'aide ?")
6. **And** 3 niveaux d'indices progressifs sont proposés
7. **And** après 3 indices, une option "Passer" apparaît
8. **And** le challenge est TOUJOURS skippable (bouton discret "Passer directement au contact")
9. **And** une validation avec feedback clair indique succès/échec
10. **And** une animation de succès célèbre la réussite
11. **And** `challengeCompleted` est mis à `true` dans le store si réussi

## Tasks / Subtasks

- [ ] **Task 1: Créer la page challenge** (AC: #1, #8)
  - [ ] Créer `frontend/app/pages/challenge.vue`
  - [ ] Vérifier que le contact est débloqué
  - [ ] Introduction narrative avec Le Bug
  - [ ] Bouton discret "Passer" visible en permanence

- [ ] **Task 2: Concevoir le puzzle** (AC: #2, #3, #4)
  - [ ] Puzzle type "réordonner les lignes de code"
  - [ ] Code simple : une fonction qui affiche un message
  - [ ] 5-7 lignes à réordonner dans le bon ordre
  - [ ] Thème : débloquer l'accès au développeur

- [ ] **Task 3: Créer le composant CodePuzzle** (AC: #2, #9)
  - [ ] Créer `frontend/app/components/feature/CodePuzzle.vue`
  - [ ] Drag & drop des lignes de code
  - [ ] Support tactile (mobile)
  - [ ] Validation visuelle (vert/rouge)

- [ ] **Task 4: Implémenter le système d'indices** (AC: #5, #6, #7)
  - [ ] Bouton "Besoin d'aide ?"
  - [ ] 3 indices progressifs (révèlent de plus en plus)
  - [ ] Après 3 indices : bouton "Passer" plus visible
  - [ ] Indices traduits FR/EN

- [ ] **Task 5: Implémenter l'animation de succès** (AC: #10, #11)
  - [ ] Confettis ou effet visuel de célébration
  - [ ] Message du narrateur
  - [ ] Mettre `challengeCompleted = true` dans le store
  - [ ] Navigation vers la révélation

- [ ] **Task 6: Gérer le skip** (AC: #8)
  - [ ] Skip visible en permanence (discret mais accessible)
  - [ ] Skip après indices (plus visible)
  - [ ] Dans les deux cas : navigation vers révélation

- [ ] **Task 7: Accessibilité**
  - [ ] Navigation clavier pour le drag & drop
  - [ ] aria-labels descriptifs
  - [ ] Instructions claires

- [ ] **Task 8: Tests et validation**
  - [ ] Tester le puzzle complet
  - [ ] Tester les 3 indices
  - [ ] Vérifier le skip
  - [ ] Tester sur mobile (drag & drop tactile)
  - [ ] Valider l'animation de succès

## Dev Notes

### Puzzle : Réordonner le code

Le puzzle consiste à remettre dans l'ordre les lignes d'une fonction JavaScript qui "débloque" l'accès au développeur.

```javascript
// Solution correcte
function unlockDeveloper() {
  const secret = "SKYCEL";
  const key = decode(secret);
  if (key === "ACCESS_GRANTED") {
    return showDeveloper();
  }
  return "Keep exploring...";
}
```

Les lignes sont mélangées et le visiteur doit les réordonner.

### Page challenge.vue

```vue
<!-- frontend/app/pages/challenge.vue -->
<script setup lang="ts">
const { t } = useI18n()
const router = useRouter()
const progressionStore = useProgressionStore()
const narrator = useNarrator()

// Vérifier que le contact est débloqué
if (!progressionStore.contactUnlocked) {
  navigateTo('/')
}

// États
const showIntro = ref(true)
const puzzleCompleted = ref(false)
const hintsUsed = ref(0)

// Introduction narrative
onMounted(async () => {
  await narrator.showMessage('challenge_intro')
})

function startPuzzle() {
  showIntro.value = false
}

function handlePuzzleSolved() {
  puzzleCompleted.value = true
  progressionStore.setChallengeCompleted(true)

  // Attendre l'animation puis naviguer
  setTimeout(() => {
    router.push('/revelation')
  }, 3000)
}

function skipChallenge() {
  // Skip ne marque pas comme complété
  router.push('/revelation')
}

function useHint() {
  hintsUsed.value++
}
</script>

<template>
  <div class="challenge-page min-h-screen bg-sky-dark relative">
    <!-- Bouton skip (toujours visible) -->
    <button
      v-if="!puzzleCompleted"
      type="button"
      class="absolute top-4 right-4 text-sky-text-muted hover:text-sky-text text-sm font-ui underline z-10"
      :class="{ 'text-sky-accent': hintsUsed >= 3 }"
      @click="skipChallenge"
    >
      {{ t('challenge.skip') }}
    </button>

    <!-- Introduction -->
    <Transition name="fade" mode="out-in">
      <div
        v-if="showIntro"
        class="flex flex-col items-center justify-center min-h-screen p-8"
      >
        <div class="max-w-lg text-center">
          <img
            src="/images/bug/bug-stage-4.svg"
            alt="Le Bug"
            class="w-24 h-24 mx-auto mb-6"
          />

          <h1 class="text-3xl font-ui font-bold text-sky-text mb-4">
            {{ t('challenge.title') }}
          </h1>

          <p class="font-narrative text-xl text-sky-text-muted mb-8">
            {{ t('challenge.intro') }}
          </p>

          <button
            type="button"
            class="px-8 py-3 bg-sky-accent text-white font-ui font-semibold rounded-lg hover:bg-sky-accent/90 transition-colors"
            @click="startPuzzle"
          >
            {{ t('challenge.accept') }}
          </button>
        </div>
      </div>

      <!-- Puzzle -->
      <div
        v-else-if="!puzzleCompleted"
        class="flex flex-col items-center justify-center min-h-screen p-8"
      >
        <div class="max-w-2xl w-full">
          <h2 class="text-xl font-ui font-bold text-sky-text mb-2 text-center">
            {{ t('challenge.puzzleTitle') }}
          </h2>

          <p class="text-sky-text-muted text-center mb-8">
            {{ t('challenge.puzzleInstruction') }}
          </p>

          <CodePuzzle
            @solved="handlePuzzleSolved"
            @hint-used="useHint"
            :hints-used="hintsUsed"
          />
        </div>
      </div>

      <!-- Succès -->
      <div
        v-else
        class="flex flex-col items-center justify-center min-h-screen p-8"
      >
        <ChallengeSuccess />
      </div>
    </Transition>
  </div>
</template>
```

### Composant CodePuzzle

```vue
<!-- frontend/app/components/feature/CodePuzzle.vue -->
<script setup lang="ts">
import { useDraggable } from '@vueuse/core'

const props = defineProps<{
  hintsUsed: number
}>()

const emit = defineEmits<{
  solved: []
  hintUsed: []
}>()

const { t } = useI18n()

// Lignes de code (solution)
const solution = [
  'function unlockDeveloper() {',
  '  const secret = "SKYCEL";',
  '  const key = decode(secret);',
  '  if (key === "ACCESS_GRANTED") {',
  '    return showDeveloper();',
  '  }',
  '  return "Keep exploring...";',
  '}',
]

// Lignes mélangées au départ
const shuffledLines = ref<string[]>([])
const isValidating = ref(false)
const validationResult = ref<boolean | null>(null)

// Mélanger au montage
onMounted(() => {
  shuffledLines.value = [...solution].sort(() => Math.random() - 0.5)
})

// Indices progressifs
const hints = [
  () => t('challenge.hint1'), // "La fonction commence par 'function'"
  () => t('challenge.hint2'), // "La variable 'secret' est définie en premier"
  () => t('challenge.hint3'), // "Le return final est 'Keep exploring...'"
]

const currentHint = computed(() => {
  if (props.hintsUsed === 0) return null
  return hints[Math.min(props.hintsUsed - 1, hints.length - 1)]()
})

// Drag & Drop
function onDragStart(e: DragEvent, index: number) {
  e.dataTransfer?.setData('text/plain', index.toString())
}

function onDrop(e: DragEvent, targetIndex: number) {
  e.preventDefault()
  const sourceIndex = parseInt(e.dataTransfer?.getData('text/plain') || '-1')
  if (sourceIndex === -1) return

  // Swap les lignes
  const newLines = [...shuffledLines.value]
  const temp = newLines[sourceIndex]
  newLines[sourceIndex] = newLines[targetIndex]
  newLines[targetIndex] = temp
  shuffledLines.value = newLines
}

function onDragOver(e: DragEvent) {
  e.preventDefault()
}

// Validation
function validateSolution() {
  isValidating.value = true
  validationResult.value = null

  setTimeout(() => {
    const isCorrect = shuffledLines.value.every((line, i) => line === solution[i])
    validationResult.value = isCorrect

    if (isCorrect) {
      emit('solved')
    } else {
      // Reset après 2s
      setTimeout(() => {
        validationResult.value = null
        isValidating.value = false
      }, 2000)
    }
  }, 500)
}

function requestHint() {
  if (props.hintsUsed < 3) {
    emit('hintUsed')
  }
}

// Navigation clavier
function moveLineUp(index: number) {
  if (index === 0) return
  const newLines = [...shuffledLines.value]
  const temp = newLines[index - 1]
  newLines[index - 1] = newLines[index]
  newLines[index] = temp
  shuffledLines.value = newLines
}

function moveLineDown(index: number) {
  if (index === shuffledLines.value.length - 1) return
  const newLines = [...shuffledLines.value]
  const temp = newLines[index + 1]
  newLines[index + 1] = newLines[index]
  newLines[index] = temp
  shuffledLines.value = newLines
}
</script>

<template>
  <div class="code-puzzle">
    <!-- Zone de code -->
    <div class="bg-sky-dark rounded-lg border border-sky-dark-100 p-4 font-mono text-sm mb-6">
      <div
        v-for="(line, index) in shuffledLines"
        :key="index"
        class="code-line flex items-center gap-2 p-2 rounded cursor-grab transition-all"
        :class="[
          validationResult === true && 'bg-green-500/20 border-green-500/50',
          validationResult === false && line !== solution[index] && 'bg-red-500/20 border-red-500/50',
        ]"
        draggable="true"
        @dragstart="onDragStart($event, index)"
        @drop="onDrop($event, index)"
        @dragover="onDragOver"
      >
        <!-- Numéro de ligne -->
        <span class="text-sky-text-muted select-none w-6 text-right">{{ index + 1 }}</span>

        <!-- Poignée de drag -->
        <span class="text-sky-text-muted cursor-grab">⋮⋮</span>

        <!-- Code -->
        <code class="flex-1 text-sky-accent">{{ line }}</code>

        <!-- Boutons clavier (accessibilité) -->
        <div class="flex gap-1">
          <button
            type="button"
            class="p-1 text-sky-text-muted hover:text-sky-text"
            :disabled="index === 0"
            @click="moveLineUp(index)"
            :aria-label="t('challenge.moveUp')"
          >
            ↑
          </button>
          <button
            type="button"
            class="p-1 text-sky-text-muted hover:text-sky-text"
            :disabled="index === shuffledLines.length - 1"
            @click="moveLineDown(index)"
            :aria-label="t('challenge.moveDown')"
          >
            ↓
          </button>
        </div>
      </div>
    </div>

    <!-- Indice actuel -->
    <div
      v-if="currentHint"
      class="bg-sky-accent/10 border border-sky-accent/30 rounded-lg p-4 mb-6"
    >
      <p class="text-sm text-sky-accent">
        <span class="font-semibold">{{ t('challenge.hintLabel') }}:</span>
        {{ currentHint }}
      </p>
    </div>

    <!-- Actions -->
    <div class="flex flex-wrap gap-4 justify-center">
      <!-- Bouton valider -->
      <button
        type="button"
        class="px-6 py-3 bg-sky-accent text-white font-ui font-semibold rounded-lg hover:bg-sky-accent/90 transition-colors disabled:opacity-50"
        :disabled="isValidating"
        @click="validateSolution"
      >
        {{ isValidating ? t('challenge.validating') : t('challenge.validate') }}
      </button>

      <!-- Bouton indice -->
      <button
        v-if="hintsUsed < 3"
        type="button"
        class="px-6 py-3 border border-sky-dark-100 text-sky-text font-ui rounded-lg hover:bg-sky-dark-50 transition-colors"
        @click="requestHint"
      >
        {{ t('challenge.needHint') }} ({{ hintsUsed }}/3)
      </button>
    </div>

    <!-- Message d'erreur -->
    <Transition name="fade">
      <p
        v-if="validationResult === false"
        class="text-red-400 text-center mt-4 font-ui"
      >
        {{ t('challenge.wrongOrder') }}
      </p>
    </Transition>
  </div>
</template>

<style scoped>
.code-line {
  border: 1px solid transparent;
}

.code-line:hover {
  background-color: rgba(250, 120, 79, 0.1);
}

.code-line:active {
  cursor: grabbing;
}
</style>
```

### Composant ChallengeSuccess

```vue
<!-- frontend/app/components/feature/ChallengeSuccess.vue -->
<script setup lang="ts">
import confetti from 'canvas-confetti'

const { t } = useI18n()

onMounted(() => {
  // Lancer les confettis
  confetti({
    particleCount: 100,
    spread: 70,
    origin: { y: 0.6 },
    colors: ['#fa784f', '#3b82f6', '#10b981', '#f59e0b'],
  })
})
</script>

<template>
  <div class="challenge-success text-center">
    <div class="text-6xl mb-4 animate-bounce">🎉</div>

    <h2 class="text-3xl font-ui font-bold text-sky-accent mb-4">
      {{ t('challenge.success') }}
    </h2>

    <p class="font-narrative text-xl text-sky-text mb-8">
      {{ t('challenge.successMessage') }}
    </p>

    <p class="text-sky-text-muted">
      {{ t('challenge.redirecting') }}
    </p>
  </div>
</template>
```

### Clés i18n

**fr.json :**
```json
{
  "challenge": {
    "title": "Une dernière épreuve...",
    "intro": "Avant de rencontrer le développeur, prouve que tu maîtrises les bases du code. Rien de bien méchant, promis.",
    "accept": "Relever le défi",
    "skip": "Passer directement au contact",
    "puzzleTitle": "Remets le code dans l'ordre",
    "puzzleInstruction": "Glisse les lignes pour reconstituer la fonction qui débloque l'accès au développeur.",
    "hint1": "La fonction commence par 'function unlockDeveloper() {'",
    "hint2": "La variable 'secret' est définie juste après l'accolade ouvrante",
    "hint3": "La dernière ligne avant l'accolade fermante est 'return \"Keep exploring...\";'",
    "hintLabel": "Indice",
    "needHint": "Besoin d'aide ?",
    "validate": "Vérifier",
    "validating": "Vérification...",
    "wrongOrder": "Ce n'est pas le bon ordre... Essaie encore !",
    "moveUp": "Monter",
    "moveDown": "Descendre",
    "success": "Bravo !",
    "successMessage": "Tu as prouvé ta valeur. Le chemin vers le développeur est maintenant ouvert...",
    "redirecting": "Redirection en cours..."
  }
}
```

**en.json :**
```json
{
  "challenge": {
    "title": "One last challenge...",
    "intro": "Before meeting the developer, prove you understand the basics of code. Nothing too hard, I promise.",
    "accept": "Accept the challenge",
    "skip": "Skip to contact",
    "puzzleTitle": "Put the code in order",
    "puzzleInstruction": "Drag the lines to reconstruct the function that unlocks access to the developer.",
    "hint1": "The function starts with 'function unlockDeveloper() {'",
    "hint2": "The 'secret' variable is defined right after the opening brace",
    "hint3": "The last line before the closing brace is 'return \"Keep exploring...\";'",
    "hintLabel": "Hint",
    "needHint": "Need help?",
    "validate": "Check",
    "validating": "Checking...",
    "wrongOrder": "That's not the right order... Try again!",
    "moveUp": "Move up",
    "moveDown": "Move down",
    "success": "Well done!",
    "successMessage": "You've proven your worth. The path to the developer is now open...",
    "redirecting": "Redirecting..."
  }
}
```

### Dépendances

**Cette story nécessite :**
- Story 3.5 : Store de progression (contactUnlocked, challengeCompleted)
- Story 3.3 : useNarrator

**Cette story prépare pour :**
- Story 4.7 : Révélation (destination après le challenge)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/
├── pages/
│   └── challenge.vue                    # CRÉER
└── components/feature/
    ├── CodePuzzle.vue                   # CRÉER
    └── ChallengeSuccess.vue             # CRÉER
```

**Fichiers à modifier :**
```
frontend/app/stores/progression.ts       # AJOUTER challengeCompleted
frontend/package.json                    # AJOUTER canvas-confetti
frontend/i18n/fr.json                    # AJOUTER challenge.*
frontend/i18n/en.json                    # AJOUTER challenge.*
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-4.6]
- [Source: docs/planning-artifacts/ux-design-specification.md#Challenge]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Challenge]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Durée puzzle | 1-3 minutes | Epics |
| Indices | 3 niveaux progressifs | Epics |
| Skip | Toujours disponible | Epics |
| Thème | Code/développement | Epics |

## Dev Agent Record

### Agent Model Used

{{agent_model_name_version}}

### Debug Log References

### Completion Notes List

### Change Log
| Date | Change | Author |
|------|--------|--------|
| 2026-02-04 | Story créée avec contexte complet | SM Agent |

### File List

