- 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>
269 lines
7.0 KiB
Vue
269 lines
7.0 KiB
Vue
<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>
|