# 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([]) // 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 | 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 ``` ### Composant EasterEggCollection ```vue ``` ### 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 ``` ### 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