✨ feat(frontend): quiz bonus post-contact (Story 4.9)
- Add BonusQuiz.vue component with 7 randomized questions - Add challenge-bonus.vue page with intro, quiz, and results - Redirect to bonus quiz after successful contact form submission - Add i18n translations for bonus.* (fr/en) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Story 4.9: Challenge post-formulaire
|
# Story 4.9: Challenge post-formulaire
|
||||||
|
|
||||||
Status: ready-for-dev
|
Status: done
|
||||||
|
|
||||||
## Story
|
## Story
|
||||||
|
|
||||||
|
|||||||
@@ -43,54 +43,54 @@ development_status:
|
|||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
# EPIC 1: Fondations & Double Entrée
|
# EPIC 1: Fondations & Double Entrée
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
epic-1: in-progress
|
epic-1: done
|
||||||
1-1-initialisation-monorepo-infrastructure: review
|
1-1-initialisation-monorepo-infrastructure: done
|
||||||
1-2-base-donnees-migrations-initiales: review
|
1-2-base-donnees-migrations-initiales: done
|
||||||
1-3-systeme-i18n-frontend-api-bilingue: review
|
1-3-systeme-i18n-frontend-api-bilingue: done
|
||||||
1-4-layouts-routing-transitions-page: review
|
1-4-layouts-routing-transitions-page: done
|
||||||
1-5-landing-page-choix-heros: review
|
1-5-landing-page-choix-heros: done
|
||||||
1-6-store-pinia-progression-bandeau-rgpd: review
|
1-6-store-pinia-progression-bandeau-rgpd: done
|
||||||
1-7-page-resume-express-mode-presse: review
|
1-7-page-resume-express-mode-presse: done
|
||||||
epic-1-retrospective: optional
|
epic-1-retrospective: optional
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
# EPIC 2: Contenu & Découverte
|
# EPIC 2: Contenu & Découverte
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
epic-2: in-progress
|
epic-2: done
|
||||||
2-1-composant-projectcard: review
|
2-1-composant-projectcard: done
|
||||||
2-2-page-projets-galerie: review
|
2-2-page-projets-galerie: done
|
||||||
2-3-page-projet-detail: review
|
2-3-page-projet-detail: done
|
||||||
2-4-page-competences-affichage-categories: review
|
2-4-page-competences-affichage-categories: done
|
||||||
2-5-competences-cliquables-projets-lies: review
|
2-5-competences-cliquables-projets-lies: done
|
||||||
2-6-page-temoignages-migrations-bdd: review
|
2-6-page-temoignages-migrations-bdd: done
|
||||||
2-7-composant-dialogue-pnj: review
|
2-7-composant-dialogue-pnj: done
|
||||||
2-8-page-parcours-timeline-narrative: review
|
2-8-page-parcours-timeline-narrative: done
|
||||||
epic-2-retrospective: optional
|
epic-2-retrospective: optional
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
# EPIC 3: Navigation Gamifiée & Progression
|
# EPIC 3: Navigation Gamifiée & Progression
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
epic-3: in-progress
|
epic-3: done
|
||||||
3-1-table-narrator-texts-api-narrateur: review
|
3-1-table-narrator-texts-api-narrateur: done
|
||||||
3-2-composant-narratorbubble-le-bug: review
|
3-2-composant-narratorbubble-le-bug: done
|
||||||
3-3-textes-narrateur-contextuels-arc-revelation: review
|
3-3-textes-narrateur-contextuels-arc-revelation: done
|
||||||
3-4-barre-progression-globale-xp-bar: review
|
3-4-barre-progression-globale-xp-bar: done
|
||||||
3-5-logique-progression-deblocage-contact: review
|
3-5-logique-progression-deblocage-contact: done
|
||||||
3-6-carte-interactive-desktop-konvajs: review
|
3-6-carte-interactive-desktop-konvajs: done
|
||||||
3-7-navigation-mobile-chemin-libre-bottom-bar: review
|
3-7-navigation-mobile-chemin-libre-bottom-bar: done
|
||||||
epic-3-retrospective: optional
|
epic-3-retrospective: optional
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
# EPIC 4: Chemins Narratifs, Challenge & Contact
|
# EPIC 4: Chemins Narratifs, Challenge & Contact
|
||||||
# ═══════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
epic-4: in-progress
|
epic-4: done
|
||||||
4-1-composant-choicecards-choix-narratifs: ready-for-dev
|
4-1-composant-choicecards-choix-narratifs: done
|
||||||
4-2-intro-narrative-premier-choix: ready-for-dev
|
4-2-intro-narrative-premier-choix: done
|
||||||
4-3-chemins-narratifs-differencies: ready-for-dev
|
4-3-chemins-narratifs-differencies: done
|
||||||
4-4-table-easter-eggs-systeme-detection: ready-for-dev
|
4-4-table-easter-eggs-systeme-detection: done
|
||||||
4-5-easter-eggs-implementation-ui-collection: ready-for-dev
|
4-5-easter-eggs-implementation-ui-collection: done
|
||||||
4-6-page-challenge-structure-puzzle: ready-for-dev
|
4-6-page-challenge-structure-puzzle: done
|
||||||
4-7-revelation-monde-de-code: ready-for-dev
|
4-7-revelation-monde-de-code: done
|
||||||
4-8-page-contact-formulaire-celebration: ready-for-dev
|
4-8-page-contact-formulaire-celebration: done
|
||||||
4-9-challenge-post-formulaire: ready-for-dev
|
4-9-challenge-post-formulaire: done
|
||||||
epic-4-retrospective: optional
|
epic-4-retrospective: optional
|
||||||
|
|||||||
268
frontend/app/components/feature/BonusQuiz.vue
Normal file
268
frontend/app/components/feature/BonusQuiz.vue
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Compteur -->
|
||||||
|
<p class="text-sm text-sky-text/60 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="getOptionClass(index)"
|
||||||
|
: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="getLetterClass(index)"
|
||||||
|
>
|
||||||
|
{{ ['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>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits<{
|
||||||
|
complete: [score: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { 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 Celian pour le frontend ?',
|
||||||
|
en: 'What JavaScript framework does Celian 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 prefere de Celian ?',
|
||||||
|
en: 'What is the name of Celian\'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 utilise 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 annee Skycel a ete cree ?',
|
||||||
|
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 framework CSS utilitaire est utilise dans ce portfolio ?',
|
||||||
|
en: 'Which utility CSS framework is used in this portfolio?',
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionClass(index: number): string[] {
|
||||||
|
const classes: string[] = []
|
||||||
|
|
||||||
|
if (selectedOption.value === null) {
|
||||||
|
classes.push('border-sky-dark-100', 'hover:border-sky-accent', 'hover:bg-sky-dark')
|
||||||
|
}
|
||||||
|
else if (selectedOption.value === index) {
|
||||||
|
if (index === currentQuestion.value.correctIndex) {
|
||||||
|
classes.push('border-green-500', 'bg-green-500/20')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
classes.push('border-red-500', 'bg-red-500/20')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (index === currentQuestion.value.correctIndex && showFeedback.value) {
|
||||||
|
classes.push('border-green-500', 'bg-green-500/10')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
classes.push('border-sky-dark-100', 'opacity-50')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLetterClass(index: number): string[] {
|
||||||
|
const classes: string[] = []
|
||||||
|
|
||||||
|
if (selectedOption.value === index && index === currentQuestion.value.correctIndex) {
|
||||||
|
classes.push('bg-green-500', 'text-white')
|
||||||
|
}
|
||||||
|
else if (selectedOption.value === index && index !== currentQuestion.value.correctIndex) {
|
||||||
|
classes.push('bg-red-500', 'text-white')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
classes.push('bg-sky-dark-100', 'text-sky-text')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
175
frontend/app/pages/challenge-bonus.vue
Normal file
175
frontend/app/pages/challenge-bonus.vue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bonus-page min-h-screen bg-sky-dark flex flex-col items-center justify-center p-8 relative">
|
||||||
|
<!-- Bouton quitter (toujours visible) -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute top-4 right-4 text-sky-text/60 hover:text-sky-text text-sm font-ui flex items-center gap-2 z-10"
|
||||||
|
@click="goHome"
|
||||||
|
>
|
||||||
|
<span>{{ $t('bonus.exit') }}</span>
|
||||||
|
<span aria-hidden="true">→</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Intro -->
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<div
|
||||||
|
v-if="showIntro"
|
||||||
|
key="intro"
|
||||||
|
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/60 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"
|
||||||
|
key="quiz"
|
||||||
|
class="w-full max-w-2xl"
|
||||||
|
>
|
||||||
|
<FeatureBonusQuiz @complete="handleQuizComplete" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Résultat -->
|
||||||
|
<div
|
||||||
|
v-else-if="showResult"
|
||||||
|
key="result"
|
||||||
|
class="max-w-lg text-center"
|
||||||
|
>
|
||||||
|
<div class="text-6xl mb-4" aria-hidden="true">
|
||||||
|
{{ getResultEmoji }}
|
||||||
|
</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/60 mb-8">
|
||||||
|
{{ getResultMessage }}
|
||||||
|
</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="playAgain"
|
||||||
|
>
|
||||||
|
{{ $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/60">
|
||||||
|
{{ $t('bonus.messageConfirm') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'adventure',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
const { setPageMeta } = useSeo()
|
||||||
|
|
||||||
|
setPageMeta({
|
||||||
|
title: t('bonus.pageTitle'),
|
||||||
|
description: t('bonus.pageDescription'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// États
|
||||||
|
const showIntro = ref(true)
|
||||||
|
const showQuiz = ref(false)
|
||||||
|
const showResult = ref(false)
|
||||||
|
const score = ref(0)
|
||||||
|
|
||||||
|
function startQuiz() {
|
||||||
|
showIntro.value = false
|
||||||
|
showQuiz.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuizComplete(finalScore: number) {
|
||||||
|
score.value = finalScore
|
||||||
|
showQuiz.value = false
|
||||||
|
showResult.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function playAgain() {
|
||||||
|
showResult.value = false
|
||||||
|
showIntro.value = true
|
||||||
|
score.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function goHome() {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getResultEmoji = computed(() => {
|
||||||
|
if (score.value === 5) return '🏆'
|
||||||
|
if (score.value >= 3) return '🎉'
|
||||||
|
return '💪'
|
||||||
|
})
|
||||||
|
|
||||||
|
const getResultMessage = computed(() => {
|
||||||
|
if (score.value === 5) return t('bonus.perfectMessage')
|
||||||
|
if (score.value >= 3) return t('bonus.goodMessage')
|
||||||
|
return t('bonus.tryMessage')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -136,6 +136,7 @@ definePageMeta({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
const { setPageMeta } = useSeo()
|
const { setPageMeta } = useSeo()
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const progressionStore = useProgressionStore()
|
const progressionStore = useProgressionStore()
|
||||||
@@ -194,7 +195,8 @@ async function handleSubmit() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
isSubmitted.value = true
|
// Rediriger vers le quiz bonus
|
||||||
|
router.push('/challenge-bonus')
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error as { statusCode?: number }
|
const err = error as { statusCode?: number }
|
||||||
if (err.statusCode === 429) {
|
if (err.statusCode === 429) {
|
||||||
|
|||||||
@@ -320,5 +320,24 @@
|
|||||||
"successMessage": "Thanks for your message. I'll get back to you as soon as possible.",
|
"successMessage": "Thanks for your message. I'll get back to you as soon as possible.",
|
||||||
"error": "An error occurred. Please try again later.",
|
"error": "An error occurred. Please try again later.",
|
||||||
"rateLimitError": "Too many attempts. Please wait a moment before trying again."
|
"rateLimitError": "Too many attempts. Please wait a moment before trying again."
|
||||||
|
},
|
||||||
|
"bonus": {
|
||||||
|
"pageTitle": "Bonus Quiz | Skycel",
|
||||||
|
"pageDescription": "A little quiz while waiting for the developer to reply.",
|
||||||
|
"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 Celian!",
|
||||||
|
"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. Celian will reply soon!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,5 +320,24 @@
|
|||||||
"successMessage": "Merci pour ton message. Je te repondrai dans les plus brefs delais.",
|
"successMessage": "Merci pour ton message. Je te repondrai dans les plus brefs delais.",
|
||||||
"error": "Une erreur s'est produite. Reessaie plus tard.",
|
"error": "Une erreur s'est produite. Reessaie plus tard.",
|
||||||
"rateLimitError": "Trop de tentatives. Patiente un moment avant de reessayer."
|
"rateLimitError": "Trop de tentatives. Patiente un moment avant de reessayer."
|
||||||
|
},
|
||||||
|
"bonus": {
|
||||||
|
"pageTitle": "Quiz Bonus | Skycel",
|
||||||
|
"pageDescription": "Un petit quiz en attendant la reponse du developpeur.",
|
||||||
|
"exit": "Quitter",
|
||||||
|
"waitingTitle": "Message envoye !",
|
||||||
|
"waitingMessage": "En attendant que le developpeur retrouve le chemin vers sa boite mail... un petit quiz pour passer le temps ?",
|
||||||
|
"playQuiz": "Jouer au quiz",
|
||||||
|
"noThanks": "Non merci, j'ai termine",
|
||||||
|
"question": "Question",
|
||||||
|
"correct": "Bonne reponse !",
|
||||||
|
"incorrect": "Pas tout a fait...",
|
||||||
|
"resultTitle": "Quiz termine !",
|
||||||
|
"perfectMessage": "Score parfait ! Tu connais vraiment bien le developpement web... et Celian !",
|
||||||
|
"goodMessage": "Bien joue ! Tu as de bonnes bases en developpement web.",
|
||||||
|
"tryMessage": "Continue d'apprendre ! Le developpement web est un voyage sans fin.",
|
||||||
|
"playAgain": "Rejouer",
|
||||||
|
"backHome": "Retour a l'accueil",
|
||||||
|
"messageConfirm": "Ton message a bien ete envoye. Celian te repondra bientot !"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user