# Story 4.5: Easter eggs - Implémentation UI et collection

Status: ready-for-dev

## Story

As a visiteur curieux,
I want découvrir des surprises cachées et voir ma collection,
so that l'exploration est récompensée.

## Acceptance Criteria

1. **Given** des easter eggs sont placés sur différentes pages **When** le visiteur déclenche un easter egg (clic, hover, konami, scroll, sequence) **Then** une animation de découverte s'affiche (popup, effet visuel)
2. **And** la récompense est affichée (snippet de code, anecdote, image, badge)
3. **And** le narrateur réagit avec enthousiasme
4. **And** une notification "Easter egg trouvé ! (X/Y)" s'affiche
5. **And** le slug est ajouté à `easterEggsFound` dans le store
6. **And** un bouton permet de fermer et continuer
7. **Given** le visiteur accède à sa collection (via paramètres ou zone dédiée) **When** la collection s'affiche **Then** une grille montre les easter eggs trouvés et des silhouettes mystère pour les non-trouvés
8. **And** les détails sont visibles pour les découverts
9. **And** un compteur X/Y indique la progression
10. **And** un badge spécial s'affiche si 100% trouvés

## Tasks / Subtasks

- [ ] **Task 1: Créer le composable useEasterEggDetection** (AC: #1)
  - [ ] Créer `frontend/app/composables/useEasterEggDetection.ts`
  - [ ] Détecter les différents types de triggers
  - [ ] Hook pour écouter le Konami code
  - [ ] Hook pour séquences de clics
  - [ ] Détecter scroll en bas de page

- [ ] **Task 2: Créer le composant EasterEggPopup** (AC: #1, #2, #6)
  - [ ] Créer `frontend/app/components/feature/EasterEggPopup.vue`
  - [ ] Modal avec animation de découverte
  - [ ] Afficher la récompense selon le type (snippet, anecdote, image, badge)
  - [ ] Bouton fermer

- [ ] **Task 3: Créer le composant EasterEggNotification** (AC: #4)
  - [ ] Créer `frontend/app/components/feature/EasterEggNotification.vue`
  - [ ] Toast notification "Easter egg trouvé ! (X/Y)"
  - [ ] Animation d'apparition/disparition
  - [ ] Position non-bloquante

- [ ] **Task 4: Intégrer le narrateur** (AC: #3)
  - [ ] Ajouter contexte `easter_egg_found` dans l'API narrateur
  - [ ] Le narrateur réagit avec enthousiasme
  - [ ] Message différent selon le type de récompense

- [ ] **Task 5: Créer le composant EasterEggCollection** (AC: #7, #8, #9, #10)
  - [ ] Créer `frontend/app/components/feature/EasterEggCollection.vue`
  - [ ] Grille d'easter eggs (trouvés vs mystères)
  - [ ] Compteur X/Y
  - [ ] Badge spécial si 100%

- [ ] **Task 6: Placer les détecteurs sur les pages** (AC: #1)
  - [ ] Header : araignée cachée (click)
  - [ ] Landing : Konami code
  - [ ] Projets : commentaire secret (hover)
  - [ ] Parcours : scroll bottom + hover date
  - [ ] Compétences : séquence tech
  - [ ] Global : clics logo

- [ ] **Task 7: Intégrer dans les paramètres/settings** (AC: #7)
  - [ ] Ajouter un onglet ou section "Collection"
  - [ ] Accessible depuis le drawer des paramètres mobile
  - [ ] Accessible depuis le menu desktop

- [ ] **Task 8: Tests et validation**
  - [ ] Tester chaque type de trigger
  - [ ] Vérifier l'affichage des récompenses
  - [ ] Tester la collection
  - [ ] Valider le compteur
  - [ ] Tester le badge 100%

## Dev Notes

### Composable useEasterEggDetection

```typescript
// frontend/app/composables/useEasterEggDetection.ts
import type { EasterEggMeta } from './useFetchEasterEggs'

interface UseEasterEggDetectionOptions {
  onFound: (slug: string) => void
}

// Konami Code : ↑↑↓↓←→←→BA
const KONAMI_CODE = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'KeyB', 'KeyA']

export function useEasterEggDetection(options: UseEasterEggDetectionOptions) {
  const { fetchList, getByLocation } = useFetchEasterEggs()
  const progressionStore = useProgressionStore()

  // État
  const konamiIndex = ref(0)
  const clickSequence = ref<string[]>([])

  // Charger les easter eggs au montage
  onMounted(() => {
    fetchList()
    initKonamiListener()
  })

  // === Konami Code ===
  function initKonamiListener() {
    window.addEventListener('keydown', handleKonamiKey)
  }

  function handleKonamiKey(e: KeyboardEvent) {
    if (e.code === KONAMI_CODE[konamiIndex.value]) {
      konamiIndex.value++
      if (konamiIndex.value === KONAMI_CODE.length) {
        triggerEasterEgg('konami-master')
        konamiIndex.value = 0
      }
    } else {
      konamiIndex.value = 0
    }
  }

  // === Click Detection ===
  function detectClick(elementId: string, targetSlug: string, requiredClicks: number = 1) {
    const clicks = ref(0)

    function handleClick() {
      clicks.value++
      if (clicks.value >= requiredClicks) {
        triggerEasterEgg(targetSlug)
        clicks.value = 0
      }
    }

    return { handleClick, clicks }
  }

  // === Hover Detection ===
  function detectHover(targetSlug: string, hoverTime: number = 2000) {
    let timeoutId: ReturnType<typeof setTimeout> | null = null

    function handleMouseEnter() {
      timeoutId = setTimeout(() => {
        triggerEasterEgg(targetSlug)
      }, hoverTime)
    }

    function handleMouseLeave() {
      if (timeoutId) {
        clearTimeout(timeoutId)
        timeoutId = null
      }
    }

    return { handleMouseEnter, handleMouseLeave }
  }

  // === Scroll Detection ===
  function detectScrollBottom(targetSlug: string) {
    function checkScroll() {
      const scrollTop = window.scrollY
      const windowHeight = window.innerHeight
      const docHeight = document.documentElement.scrollHeight

      if (scrollTop + windowHeight >= docHeight - 50) {
        triggerEasterEgg(targetSlug)
      }
    }

    onMounted(() => {
      window.addEventListener('scroll', checkScroll, { passive: true })
    })

    onUnmounted(() => {
      window.removeEventListener('scroll', checkScroll)
    })
  }

  // === Sequence Detection ===
  function detectSequence(expectedSequence: string[], targetSlug: string) {
    function addToSequence(item: string) {
      clickSequence.value.push(item)

      // Garder seulement les N derniers
      if (clickSequence.value.length > expectedSequence.length) {
        clickSequence.value.shift()
      }

      // Vérifier si la séquence correspond
      if (clickSequence.value.length === expectedSequence.length) {
        const match = clickSequence.value.every((val, idx) => val === expectedSequence[idx])
        if (match) {
          triggerEasterEgg(targetSlug)
          clickSequence.value = []
        }
      }
    }

    return { addToSequence }
  }

  // === Trigger Easter Egg ===
  async function triggerEasterEgg(slug: string) {
    // Vérifier si déjà trouvé
    if (progressionStore.easterEggsFound.includes(slug)) {
      return
    }

    // Marquer comme trouvé
    progressionStore.markEasterEggFound(slug)

    // Notifier
    options.onFound(slug)
  }

  onUnmounted(() => {
    window.removeEventListener('keydown', handleKonamiKey)
  })

  return {
    detectClick,
    detectHover,
    detectScrollBottom,
    detectSequence,
    triggerEasterEgg,
  }
}
```

### Composant EasterEggPopup

```vue
<!-- frontend/app/components/feature/EasterEggPopup.vue -->
<script setup lang="ts">
interface EasterEggReward {
  slug: string
  reward_type: 'snippet' | 'anecdote' | 'image' | 'badge'
  reward: string
  difficulty: number
}

const props = defineProps<{
  visible: boolean
  reward: EasterEggReward | null
}>()

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

const { t } = useI18n()
const progressionStore = useProgressionStore()
const { availableEasterEggs } = useFetchEasterEggs()

const totalEasterEggs = computed(() => availableEasterEggs.value.length || 8)
const foundCount = computed(() => progressionStore.easterEggsFoundCount)

// Icône selon le type
const rewardIcon = computed(() => {
  if (!props.reward) return '🎁'
  const icons: Record<string, string> = {
    snippet: '💻',
    anecdote: '📖',
    image: '🖼️',
    badge: '🏆',
  }
  return icons[props.reward.reward_type] || '🎁'
})
</script>

<template>
  <Teleport to="body">
    <Transition name="popup">
      <div
        v-if="visible && reward"
        class="fixed inset-0 z-50 flex items-center justify-center p-4"
      >
        <!-- Overlay -->
        <div
          class="absolute inset-0 bg-black/70 backdrop-blur-sm"
          @click="emit('close')"
        ></div>

        <!-- Modal -->
        <div class="relative bg-sky-dark-50 rounded-2xl p-8 max-w-md w-full border border-sky-accent/50 shadow-2xl shadow-sky-accent/20 animate-bounce-in">
          <!-- Effet confetti/sparkles -->
          <div class="absolute -top-4 left-1/2 -translate-x-1/2 text-4xl animate-bounce">
            🎉
          </div>

          <!-- Icône du type -->
          <div class="text-6xl text-center mb-4">
            {{ rewardIcon }}
          </div>

          <!-- Titre -->
          <h2 class="text-2xl font-ui font-bold text-sky-accent text-center mb-2">
            {{ t('easterEgg.found') }}
          </h2>

          <!-- Compteur -->
          <p class="text-sm text-sky-text-muted text-center mb-6">
            {{ t('easterEgg.count', { found: foundCount, total: totalEasterEggs }) }}
          </p>

          <!-- Récompense -->
          <div class="bg-sky-dark rounded-lg p-4 mb-6">
            <!-- Snippet de code -->
            <pre
              v-if="reward.reward_type === 'snippet'"
              class="font-mono text-sm text-sky-accent overflow-x-auto"
            ><code>{{ reward.reward }}</code></pre>

            <!-- Anecdote ou texte -->
            <p
              v-else-if="reward.reward_type === 'anecdote'"
              class="font-narrative text-sky-text italic"
            >
              {{ reward.reward }}
            </p>

            <!-- Badge -->
            <div
              v-else-if="reward.reward_type === 'badge'"
              class="text-center"
            >
              <p class="font-ui text-sky-text">{{ reward.reward }}</p>
            </div>

            <!-- Image -->
            <div
              v-else-if="reward.reward_type === 'image'"
              class="text-center"
            >
              <p class="font-ui text-sky-text">{{ reward.reward }}</p>
            </div>
          </div>

          <!-- Difficulté -->
          <div class="flex items-center justify-center gap-1 mb-6">
            <span class="text-xs text-sky-text-muted mr-2">{{ t('easterEgg.difficulty') }}:</span>
            <span
              v-for="i in 5"
              :key="i"
              class="text-sm"
              :class="i <= reward.difficulty ? 'text-sky-accent' : 'text-sky-dark-100'"
            >
              ⭐
            </span>
          </div>

          <!-- Bouton fermer -->
          <button
            type="button"
            class="w-full py-3 bg-sky-accent text-white font-ui font-semibold rounded-lg hover:bg-sky-accent/90 transition-colors"
            @click="emit('close')"
          >
            {{ t('common.continue') }}
          </button>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<style scoped>
.popup-enter-active,
.popup-leave-active {
  transition: all 0.3s ease;
}

.popup-enter-from,
.popup-leave-to {
  opacity: 0;
}

.popup-enter-from .relative,
.popup-leave-to .relative {
  transform: scale(0.9);
}

@keyframes bounce-in {
  0% {
    transform: scale(0.5);
    opacity: 0;
  }
  50% {
    transform: scale(1.05);
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.animate-bounce-in {
  animation: bounce-in 0.4s ease-out;
}
</style>
```

### Composant EasterEggCollection

```vue
<!-- frontend/app/components/feature/EasterEggCollection.vue -->
<script setup lang="ts">
const { t } = useI18n()
const progressionStore = useProgressionStore()
const { availableEasterEggs, fetchList } = useFetchEasterEggs()

onMounted(() => {
  fetchList()
})

const totalEasterEggs = computed(() => availableEasterEggs.value.length || 8)
const foundCount = computed(() => progressionStore.easterEggsFoundCount)
const isComplete = computed(() => foundCount.value >= totalEasterEggs.value)

function isFound(slug: string): boolean {
  return progressionStore.easterEggsFound.includes(slug)
}

// Icône selon difficulté
function getDifficultyStars(difficulty: number): string {
  return '⭐'.repeat(difficulty) + '☆'.repeat(5 - difficulty)
}
</script>

<template>
  <div class="easter-egg-collection">
    <!-- Header avec compteur -->
    <div class="flex items-center justify-between mb-6">
      <h2 class="text-xl font-ui font-bold text-sky-text">
        {{ t('easterEgg.collection') }}
      </h2>
      <div class="flex items-center gap-2">
        <span class="text-sky-accent font-ui font-bold">{{ foundCount }}</span>
        <span class="text-sky-text-muted">/</span>
        <span class="text-sky-text-muted">{{ totalEasterEggs }}</span>
      </div>
    </div>

    <!-- Badge 100% -->
    <div
      v-if="isComplete"
      class="bg-gradient-to-r from-sky-accent to-amber-500 rounded-lg p-4 mb-6 text-center"
    >
      <span class="text-2xl">🏆</span>
      <p class="text-white font-ui font-bold mt-2">
        {{ t('easterEgg.allFound') }}
      </p>
    </div>

    <!-- Barre de progression -->
    <div class="h-2 bg-sky-dark-100 rounded-full mb-6 overflow-hidden">
      <div
        class="h-full bg-sky-accent transition-all duration-500"
        :style="{ width: `${(foundCount / totalEasterEggs) * 100}%` }"
      ></div>
    </div>

    <!-- Grille des easter eggs -->
    <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
      <div
        v-for="egg in availableEasterEggs"
        :key="egg.slug"
        class="easter-egg-card p-4 rounded-lg border transition-all"
        :class="[
          isFound(egg.slug)
            ? 'bg-sky-dark-50 border-sky-accent/50'
            : 'bg-sky-dark border-sky-dark-100 opacity-50'
        ]"
      >
        <!-- Icône ou mystère -->
        <div class="text-3xl text-center mb-2">
          {{ isFound(egg.slug) ? getTriggerIcon(egg.trigger_type) : '❓' }}
        </div>

        <!-- Nom ou mystère -->
        <p class="text-sm font-ui text-center truncate" :class="isFound(egg.slug) ? 'text-sky-text' : 'text-sky-text-muted'">
          {{ isFound(egg.slug) ? formatSlug(egg.slug) : '???' }}
        </p>

        <!-- Difficulté -->
        <p class="text-xs text-center mt-1 text-sky-text-muted">
          {{ getDifficultyStars(egg.difficulty) }}
        </p>
      </div>
    </div>

    <!-- Indice si pas tous trouvés -->
    <p
      v-if="!isComplete"
      class="text-sm text-sky-text-muted text-center mt-6 font-narrative italic"
    >
      {{ t('easterEgg.hint') }}
    </p>
  </div>
</template>

<script lang="ts">
function getTriggerIcon(trigger: string): string {
  const icons: Record<string, string> = {
    click: '👆',
    hover: '👀',
    konami: '🎮',
    scroll: '📜',
    sequence: '🔢',
  }
  return icons[trigger] || '🎁'
}

function formatSlug(slug: string): string {
  return slug
    .split('-')
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ')
}
</script>
```

### Clés i18n

**fr.json :**
```json
{
  "easterEgg": {
    "found": "Easter Egg trouvé !",
    "count": "{found} / {total} découverts",
    "difficulty": "Difficulté",
    "collection": "Ma Collection",
    "allFound": "Collection complète ! Tu es un vrai explorateur !",
    "hint": "Continue d'explorer... des surprises sont cachées partout !"
  }
}
```

**en.json :**
```json
{
  "easterEgg": {
    "found": "Easter Egg found!",
    "count": "{found} / {total} discovered",
    "difficulty": "Difficulty",
    "collection": "My Collection",
    "allFound": "Collection complete! You're a true explorer!",
    "hint": "Keep exploring... surprises are hidden everywhere!"
  }
}
```

### Intégration dans une page (exemple)

```vue
<!-- frontend/app/pages/projets.vue (extrait) -->
<script setup>
const showEasterEggPopup = ref(false)
const currentReward = ref(null)

const { validate } = useFetchEasterEggs()
const narrator = useNarrator()

const { detectHover } = useEasterEggDetection({
  onFound: async (slug) => {
    const reward = await validate(slug)
    if (reward) {
      currentReward.value = reward
      showEasterEggPopup.value = true
      narrator.showEasterEggFound()
    }
  }
})

// Hover sur le commentaire secret
const secretCommentHover = detectHover('secret-comment', 2000)
</script>

<template>
  <!-- ... contenu de la page ... -->

  <!-- Élément avec easter egg hover -->
  <span
    class="cursor-help"
    @mouseenter="secretCommentHover.handleMouseEnter"
    @mouseleave="secretCommentHover.handleMouseLeave"
  >
    /* ... */
  </span>

  <!-- Popup easter egg -->
  <EasterEggPopup
    :visible="showEasterEggPopup"
    :reward="currentReward"
    @close="showEasterEggPopup = false"
  />
</template>
```

### Dépendances

**Cette story nécessite :**
- Story 4.4 : API et store des easter eggs
- Story 3.3 : useNarrator (réaction du narrateur)

**Cette story prépare pour :**
- Story 4.8 : Page contact (statistiques de collection)

### Project Structure Notes

**Fichiers à créer :**
```
frontend/app/
├── composables/
│   └── useEasterEggDetection.ts         # CRÉER
└── components/feature/
    ├── EasterEggPopup.vue               # CRÉER
    ├── EasterEggNotification.vue        # CRÉER
    └── EasterEggCollection.vue          # CRÉER
```

**Fichiers à modifier :**
```
frontend/app/pages/projets.vue           # AJOUTER détecteurs
frontend/app/pages/parcours.vue          # AJOUTER détecteurs
frontend/app/pages/competences.vue       # AJOUTER détecteurs
frontend/app/components/layout/AppHeader.vue  # AJOUTER araignée cachée
frontend/app/components/feature/SettingsDrawer.vue  # AJOUTER collection
frontend/i18n/fr.json                    # AJOUTER easterEgg.*
frontend/i18n/en.json                    # AJOUTER easterEgg.*
```

### References

- [Source: docs/planning-artifacts/epics.md#Story-4.5]
- [Source: docs/planning-artifacts/ux-design-specification.md#Easter-Eggs-UI]
- [Source: docs/brainstorming-gamification-2026-01-26.md#Easter-Eggs]

### Technical Requirements

| Requirement | Value | Source |
|-------------|-------|--------|
| Types de triggers | click, hover, konami, scroll, sequence | Epics |
| Types de récompenses | snippet, anecdote, image, badge | Epics |
| Collection | Grille avec mystères | Epics |
| Badge 100% | Affiché si complet | 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

