🎉 Init monorepo Nuxt 4 + Laravel 12 (Story 1.1)
Setup complet de l'infrastructure projet : - Frontend Nuxt 4 (SSR, TypeScript, i18n, Pinia, TailwindCSS) - Backend Laravel 12 API-only avec middleware X-API-Key et CORS - Design tokens (sky-dark, sky-accent, sky-text) et polices (Merriweather, Inter) - Documentation planning et implementation artifacts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,662 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user