✨ feat(epic-4): chemins narratifs, easter eggs, challenge et contact
Epic 4: Chemins Narratifs, Challenge & Contact Stories implementees: - 4.1: Composant ChoiceCards pour choix narratifs binaires - 4.2: Sequence d'intro narrative avec Le Bug - 4.3: Chemins narratifs differencies avec useNarrativePath - 4.4: Table easter_eggs et systeme de detection (API + composable) - 4.5: Easter eggs UI (popup, notification, collection) - 4.6: Page challenge avec puzzle de code - 4.7: Page revelation "Monde de Code" - 4.8: Page contact avec formulaire et stats Fichiers crees: - Frontend: ChoiceCards, IntroSequence, ZoneEndChoice, EasterEggPopup, CodePuzzle, ChallengeSuccess, CodeWorld, et pages intro/challenge/revelation - API: EasterEggController, Model, Migration, Seeder Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
162
frontend/app/components/feature/EasterEggPopup.vue
Normal file
162
frontend/app/components/feature/EasterEggPopup.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<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')"
|
||||
/>
|
||||
|
||||
<!-- 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"
|
||||
>
|
||||
<!-- Confetti -->
|
||||
<div class="absolute -top-4 left-1/2 -translate-x-1/2 text-4xl animate-bounce">
|
||||
<span aria-hidden="true">*</span>
|
||||
</div>
|
||||
|
||||
<!-- Type icon -->
|
||||
<div class="text-6xl text-center mb-4">
|
||||
{{ rewardIcon }}
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="text-2xl font-ui font-bold text-sky-accent text-center mb-2">
|
||||
{{ $t('easterEgg.found') }}
|
||||
</h2>
|
||||
|
||||
<!-- Counter -->
|
||||
<p class="text-sm text-sky-text/60 text-center mb-6">
|
||||
{{ $t('easterEgg.count', { found: foundCount, total: totalEasterEggs }) }}
|
||||
</p>
|
||||
|
||||
<!-- Reward -->
|
||||
<div class="bg-sky-dark rounded-lg p-4 mb-6">
|
||||
<!-- Code snippet -->
|
||||
<pre
|
||||
v-if="reward.reward_type === 'snippet'"
|
||||
class="font-mono text-sm text-sky-accent overflow-x-auto whitespace-pre-wrap"
|
||||
><code>{{ reward.reward }}</code></pre>
|
||||
|
||||
<!-- Anecdote -->
|
||||
<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>
|
||||
|
||||
<!-- Difficulty -->
|
||||
<div class="flex items-center justify-center gap-1 mb-6">
|
||||
<span class="text-xs text-sky-text/60 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>
|
||||
|
||||
<!-- Close button -->
|
||||
<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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { EasterEggReward } from '~/composables/useFetchEasterEggs'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
reward: EasterEggReward | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const progressionStore = useProgressionStore()
|
||||
const { availableEasterEggs } = useFetchEasterEggs()
|
||||
|
||||
const totalEasterEggs = computed(() => availableEasterEggs.value.length || 8)
|
||||
const foundCount = computed(() => progressionStore.easterEggsFoundCount)
|
||||
|
||||
// Icon based on reward type
|
||||
const rewardIcon = computed(() => {
|
||||
if (!props.reward) return ''
|
||||
const icons: Record<string, string> = {
|
||||
snippet: '</>', // Code icon
|
||||
anecdote: '!', // Story icon
|
||||
image: '[]', // Image icon
|
||||
badge: '*', // Trophy icon
|
||||
}
|
||||
return icons[props.reward.reward_type] || ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user