# Story 4.9: Challenge post-formulaire

Status: ready-for-dev

## Story

As a visiteur ayant envoyé un message,
I want m'amuser en attendant la réponse,
so that le temps d'attente est transformé en moment de jeu.

## Acceptance Criteria

1. **Given** le formulaire de contact a été envoyé avec succès **When** la confirmation s'affiche **Then** un message "En attendant que le développeur retrouve le chemin vers sa boîte mail..." est affiché
2. **And** un challenge optionnel bonus est proposé
3. **And** le challenge est différent du challenge principal (mini-jeu, quiz, exploration)
4. **And** le visiteur peut fermer et quitter à tout moment
5. **And** la participation est totalement optionnelle
6. **And** le résultat n'impacte rien (juste pour le fun)
7. **And** le narrateur commente avec humour

## Tasks / Subtasks

- [ ] **Task 1: Créer la page challenge-bonus** (AC: #1, #2, #4)
  - [ ] Créer `frontend/app/pages/challenge-bonus.vue`
  - [ ] Message d'attente humoristique
  - [ ] Présentation du mini-jeu
  - [ ] Bouton "Quitter" visible en permanence

- [ ] **Task 2: Concevoir le mini-jeu** (AC: #3, #6)
  - [ ] Quiz sur le développement web (5 questions)
  - [ ] OU : Memory avec des technos (Vue, Laravel, TypeScript, etc.)
  - [ ] OU : Snake simplifié thème code
  - [ ] Résultat juste pour le fun, pas de récompense

- [ ] **Task 3: Créer le composant BonusQuiz** (AC: #3)
  - [ ] 5 questions aléatoires sur le dev
  - [ ] Choix multiples (4 options)
  - [ ] Feedback immédiat (correct/incorrect)
  - [ ] Score à la fin

- [ ] **Task 4: Commentaires du narrateur** (AC: #7)
  - [ ] Message d'intro humoristique
  - [ ] Réactions aux réponses
  - [ ] Message de fin selon le score

- [ ] **Task 5: Navigation de sortie** (AC: #4, #5)
  - [ ] Bouton "Retour à l'accueil" visible
  - [ ] Confirmation que le message est envoyé
  - [ ] Remerciement final

- [ ] **Task 6: Tests et validation**
  - [ ] Tester le quiz complet
  - [ ] Vérifier que le résultat n'impacte rien
  - [ ] Tester la sortie à tout moment

## Dev Notes

### Page challenge-bonus.vue

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

// États
const showIntro = ref(true)
const showQuiz = ref(false)
const showResult = ref(false)
const score = ref(0)

// Afficher le message d'intro
onMounted(() => {
  narrator.showMessage('bonus_intro')
})

function startQuiz() {
  showIntro.value = false
  showQuiz.value = true
}

function handleQuizComplete(finalScore: number) {
  score.value = finalScore
  showQuiz.value = false
  showResult.value = true

  // Message du narrateur selon le score
  if (finalScore === 5) {
    narrator.showMessage('bonus_perfect')
  } else if (finalScore >= 3) {
    narrator.showMessage('bonus_good')
  } else {
    narrator.showMessage('bonus_try_again')
  }
}

function goHome() {
  router.push('/')
}
</script>

<template>
  <div class="bonus-page min-h-screen bg-sky-dark flex flex-col items-center justify-center p-8">
    <!-- Bouton quitter (toujours visible) -->
    <button
      type="button"
      class="absolute top-4 right-4 text-sky-text-muted hover:text-sky-text text-sm font-ui flex items-center gap-2"
      @click="goHome"
    >
      <span>{{ t('bonus.exit') }}</span>
      <span>→</span>
    </button>

    <!-- Intro -->
    <Transition name="fade" mode="out-in">
      <div
        v-if="showIntro"
        class="max-w-lg text-center"
      >
        <img
          src="/images/bug/bug-stage-5.svg"
          alt="Le Bug"
          class="w-24 h-24 mx-auto mb-6"
        />

        <h1 class="text-2xl font-ui font-bold text-sky-text mb-4">
          {{ t('bonus.waitingTitle') }}
        </h1>

        <p class="font-narrative text-lg text-sky-text-muted mb-8">
          {{ t('bonus.waitingMessage') }}
        </p>

        <div class="space-y-4">
          <button
            type="button"
            class="w-full px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-lg hover:bg-sky-accent/90 transition-colors"
            @click="startQuiz"
          >
            {{ t('bonus.playQuiz') }}
          </button>

          <button
            type="button"
            class="w-full px-8 py-4 border border-sky-dark-100 text-sky-text font-ui rounded-lg hover:bg-sky-dark-50 transition-colors"
            @click="goHome"
          >
            {{ t('bonus.noThanks') }}
          </button>
        </div>
      </div>

      <!-- Quiz -->
      <div
        v-else-if="showQuiz"
        class="w-full max-w-2xl"
      >
        <BonusQuiz @complete="handleQuizComplete" />
      </div>

      <!-- Résultat -->
      <div
        v-else-if="showResult"
        class="max-w-lg text-center"
      >
        <div class="text-6xl mb-4">
          {{ score === 5 ? '🏆' : score >= 3 ? '🎉' : '💪' }}
        </div>

        <h2 class="text-2xl font-ui font-bold text-sky-text mb-2">
          {{ t('bonus.resultTitle') }}
        </h2>

        <p class="text-4xl font-ui font-bold text-sky-accent mb-4">
          {{ score }} / 5
        </p>

        <p class="font-narrative text-lg text-sky-text-muted mb-8">
          {{ score === 5
            ? t('bonus.perfectMessage')
            : score >= 3
              ? t('bonus.goodMessage')
              : t('bonus.tryMessage')
          }}
        </p>

        <div class="space-y-4">
          <button
            type="button"
            class="w-full px-8 py-4 bg-sky-accent text-white font-ui font-semibold rounded-lg hover:bg-sky-accent/90 transition-colors"
            @click="showIntro = true; showResult = false"
          >
            {{ t('bonus.playAgain') }}
          </button>

          <button
            type="button"
            class="w-full px-8 py-4 border border-sky-dark-100 text-sky-text font-ui rounded-lg hover:bg-sky-dark-50 transition-colors"
            @click="goHome"
          >
            {{ t('bonus.backHome') }}
          </button>
        </div>

        <!-- Confirmation message envoyé -->
        <p class="mt-8 text-sm text-sky-text-muted">
          {{ t('bonus.messageConfirm') }}
        </p>
      </div>
    </Transition>
  </div>
</template>
```

### Composant BonusQuiz

```vue
<!-- frontend/app/components/feature/BonusQuiz.vue -->
<script setup lang="ts">
const emit = defineEmits<{
  complete: [score: number]
}>()

const { t, locale } = useI18n()

interface Question {
  question: { fr: string; en: string }
  options: { fr: string; en: string }[]
  correctIndex: number
}

// Questions du quiz
const allQuestions: Question[] = [
  {
    question: {
      fr: "Quel framework JavaScript utilise Célian pour le frontend ?",
      en: "What JavaScript framework does Célian use for the frontend?"
    },
    options: [
      { fr: "React", en: "React" },
      { fr: "Vue.js", en: "Vue.js" },
      { fr: "Angular", en: "Angular" },
      { fr: "Svelte", en: "Svelte" }
    ],
    correctIndex: 1
  },
  {
    question: {
      fr: "Quel est le nom du framework PHP backend préféré de Célian ?",
      en: "What is the name of Célian's favorite PHP backend framework?"
    },
    options: [
      { fr: "Symfony", en: "Symfony" },
      { fr: "CodeIgniter", en: "CodeIgniter" },
      { fr: "Laravel", en: "Laravel" },
      { fr: "CakePHP", en: "CakePHP" }
    ],
    correctIndex: 2
  },
  {
    question: {
      fr: "Comment s'appelle la mascotte de Skycel ?",
      en: "What is the name of Skycel's mascot?"
    },
    options: [
      { fr: "La Fourmi", en: "The Ant" },
      { fr: "Le Bug", en: "The Bug" },
      { fr: "Le Pixel", en: "The Pixel" },
      { fr: "Le Code", en: "The Code" }
    ],
    correctIndex: 1
  },
  {
    question: {
      fr: "Quelle extension de JavaScript ajoute le typage statique ?",
      en: "Which JavaScript extension adds static typing?"
    },
    options: [
      { fr: "CoffeeScript", en: "CoffeeScript" },
      { fr: "TypeScript", en: "TypeScript" },
      { fr: "Babel", en: "Babel" },
      { fr: "ESLint", en: "ESLint" }
    ],
    correctIndex: 1
  },
  {
    question: {
      fr: "Quel meta-framework Nuxt est utilisé pour ce portfolio ?",
      en: "Which Nuxt meta-framework is used for this portfolio?"
    },
    options: [
      { fr: "Nuxt 2", en: "Nuxt 2" },
      { fr: "Nuxt 3", en: "Nuxt 3" },
      { fr: "Nuxt 4", en: "Nuxt 4" },
      { fr: "Next.js", en: "Next.js" }
    ],
    correctIndex: 2
  },
  {
    question: {
      fr: "En quelle année Skycel a été créé ?",
      en: "In what year was Skycel created?"
    },
    options: [
      { fr: "2020", en: "2020" },
      { fr: "2021", en: "2021" },
      { fr: "2022", en: "2022" },
      { fr: "2023", en: "2023" }
    ],
    correctIndex: 2
  },
  {
    question: {
      fr: "Quel est l'acronyme de l'outil CSS utilitaire populaire ?",
      en: "What is the acronym of the popular utility CSS tool?"
    },
    options: [
      { fr: "Bootstrap", en: "Bootstrap" },
      { fr: "Tailwind CSS", en: "Tailwind CSS" },
      { fr: "Bulma", en: "Bulma" },
      { fr: "Foundation", en: "Foundation" }
    ],
    correctIndex: 1
  },
]

// Sélectionner 5 questions aléatoires
const questions = ref<Question[]>([])
const currentIndex = ref(0)
const score = ref(0)
const selectedOption = ref<number | null>(null)
const showFeedback = ref(false)

onMounted(() => {
  // Mélanger et prendre 5 questions
  questions.value = [...allQuestions]
    .sort(() => Math.random() - 0.5)
    .slice(0, 5)
})

const currentQuestion = computed(() => questions.value[currentIndex.value])
const progress = computed(() => ((currentIndex.value + 1) / 5) * 100)

function selectOption(index: number) {
  if (showFeedback.value) return

  selectedOption.value = index
  showFeedback.value = true

  if (index === currentQuestion.value.correctIndex) {
    score.value++
  }

  // Passer à la question suivante après délai
  setTimeout(() => {
    if (currentIndex.value < 4) {
      currentIndex.value++
      selectedOption.value = null
      showFeedback.value = false
    } else {
      emit('complete', score.value)
    }
  }, 1500)
}

function getText(obj: { fr: string; en: string }): string {
  return locale.value === 'fr' ? obj.fr : obj.en
}
</script>

<template>
  <div class="bonus-quiz">
    <!-- Barre de progression -->
    <div class="h-2 bg-sky-dark-100 rounded-full mb-8 overflow-hidden">
      <div
        class="h-full bg-sky-accent transition-all duration-300"
        :style="{ width: `${progress}%` }"
      ></div>
    </div>

    <!-- Compteur -->
    <p class="text-sm text-sky-text-muted text-center mb-4">
      {{ t('bonus.question') }} {{ currentIndex + 1 }} / 5
    </p>

    <!-- Question -->
    <div
      v-if="currentQuestion"
      class="bg-sky-dark-50 rounded-xl p-6 border border-sky-dark-100"
    >
      <h3 class="text-xl font-ui font-semibold text-sky-text mb-6">
        {{ getText(currentQuestion.question) }}
      </h3>

      <!-- Options -->
      <div class="space-y-3">
        <button
          v-for="(option, index) in currentQuestion.options"
          :key="index"
          type="button"
          class="w-full p-4 rounded-lg border text-left transition-all font-ui"
          :class="[
            selectedOption === null
              ? 'border-sky-dark-100 hover:border-sky-accent hover:bg-sky-dark'
              : selectedOption === index
                ? index === currentQuestion.correctIndex
                  ? 'border-green-500 bg-green-500/20'
                  : 'border-red-500 bg-red-500/20'
                : index === currentQuestion.correctIndex && showFeedback
                  ? 'border-green-500 bg-green-500/10'
                  : 'border-sky-dark-100 opacity-50'
          ]"
          :disabled="showFeedback"
          @click="selectOption(index)"
        >
          <span class="flex items-center gap-3">
            <span
              class="shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
              :class="[
                selectedOption === index && index === currentQuestion.correctIndex
                  ? 'bg-green-500 text-white'
                  : selectedOption === index && index !== currentQuestion.correctIndex
                    ? 'bg-red-500 text-white'
                    : 'bg-sky-dark-100 text-sky-text'
              ]"
            >
              {{ ['A', 'B', 'C', 'D'][index] }}
            </span>
            <span class="text-sky-text">{{ getText(option) }}</span>
          </span>
        </button>
      </div>

      <!-- Feedback -->
      <Transition name="fade">
        <p
          v-if="showFeedback"
          class="mt-4 text-center font-ui"
          :class="selectedOption === currentQuestion.correctIndex ? 'text-green-400' : 'text-red-400'"
        >
          {{ selectedOption === currentQuestion.correctIndex
            ? t('bonus.correct')
            : t('bonus.incorrect')
          }}
        </p>
      </Transition>
    </div>
  </div>
</template>
```

### Clés i18n

**fr.json :**
```json
{
  "bonus": {
    "exit": "Quitter",
    "waitingTitle": "Message envoyé !",
    "waitingMessage": "En attendant que le développeur retrouve le chemin vers sa boîte mail... un petit quiz pour passer le temps ?",
    "playQuiz": "Jouer au quiz",
    "noThanks": "Non merci, j'ai terminé",
    "question": "Question",
    "correct": "Bonne réponse ! 🎉",
    "incorrect": "Pas tout à fait... 😅",
    "resultTitle": "Quiz terminé !",
    "perfectMessage": "Score parfait ! Tu connais vraiment bien le développement web... et Célian !",
    "goodMessage": "Bien joué ! Tu as de bonnes bases en développement web.",
    "tryMessage": "Continue d'apprendre ! Le développement web est un voyage sans fin.",
    "playAgain": "Rejouer",
    "backHome": "Retour à l'accueil",
    "messageConfirm": "Ton message a bien été envoyé. Célian te répondra bientôt !"
  }
}
```

**en.json :**
```json
{
  "bonus": {
    "exit": "Exit",
    "waitingTitle": "Message sent!",
    "waitingMessage": "While the developer finds their way to the inbox... a little quiz to pass the time?",
    "playQuiz": "Play the quiz",
    "noThanks": "No thanks, I'm done",
    "question": "Question",
    "correct": "Correct! 🎉",
    "incorrect": "Not quite... 😅",
    "resultTitle": "Quiz completed!",
    "perfectMessage": "Perfect score! You really know web development... and Célian!",
    "goodMessage": "Well done! You have solid web development basics.",
    "tryMessage": "Keep learning! Web development is an endless journey.",
    "playAgain": "Play again",
    "backHome": "Back to home",
    "messageConfirm": "Your message was sent successfully. Célian will reply soon!"
  }
}
```

### Dépendances

**Cette story nécessite :**
- Story 4.8 : Page contact (redirection après envoi)
- Story 3.3 : useNarrator

**Cette story prépare pour :**
- Aucune (dernière story de l'Epic 4)

### Project Structure Notes

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

**Fichiers à modifier :**
```
frontend/i18n/fr.json                    # AJOUTER bonus.*
frontend/i18n/en.json                    # AJOUTER bonus.*
```

### References

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

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Type de mini-jeu | Quiz (5 questions) | Décision technique |
| Impact sur progression | Aucun | Epics |
| Sortie | Toujours possible | Epics |
| Ambiance | Humoristique | 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

