mirror of
https://github.com/arthur-pbty/moon.git
synced 2026-06-03 15:07:31 +02:00
336 lines
13 KiB
TypeScript
336 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useLocale } from './LocaleProvider';
|
|
import { ts } from '@/lib/i18n';
|
|
|
|
interface QuizQuestion {
|
|
question: Record<string, string>;
|
|
options: Record<string, string[]>;
|
|
correctIndex: number;
|
|
explanation: Record<string, string>;
|
|
}
|
|
|
|
const QUESTIONS: QuizQuestion[] = [
|
|
{
|
|
question: {
|
|
en: 'How long is one complete lunar cycle (synodic month)?',
|
|
fr: 'Quelle est la durée d\'un cycle lunaire complet (mois synodique) ?',
|
|
},
|
|
options: {
|
|
en: ['27.32 days', '28 days', '29.53 days', '30 days'],
|
|
fr: ['27,32 jours', '28 jours', '29,53 jours', '30 jours'],
|
|
},
|
|
correctIndex: 2,
|
|
explanation: {
|
|
en: 'The synodic month lasts 29.53 days — the time between two identical phases (e.g., full moon to full moon).',
|
|
fr: 'Le mois synodique dure 29,53 jours — le temps entre deux phases identiques.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'What is the traditional name for the September full moon?',
|
|
fr: 'Quel est le nom traditionnel de la pleine lune de septembre ?',
|
|
},
|
|
options: {
|
|
en: ['Wolf Moon', 'Harvest Moon', 'Hunter\'s Moon', 'Blood Moon'],
|
|
fr: ['Lune du Loup', 'Lune des Moissons', 'Lune du Chasseur', 'Lune de Sang'],
|
|
},
|
|
correctIndex: 1,
|
|
explanation: {
|
|
en: 'The Harvest Moon rises near sunset during harvest season, providing extra light for farmers.',
|
|
fr: 'La Lune des Moissons se lève près du coucher du soleil, offrant de la lumière aux agriculteurs.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'What causes the tides on Earth?',
|
|
fr: 'Qu\'est-ce qui cause les marées sur Terre ?',
|
|
},
|
|
options: {
|
|
en: ['Wind', 'Earth\'s rotation only', 'Gravitational pull of Moon and Sun', 'Ocean currents'],
|
|
fr: ['Le vent', 'La rotation terrestre seule', 'L\'attraction gravitationnelle de la Lune et du Soleil', 'Les courants océaniques'],
|
|
},
|
|
correctIndex: 2,
|
|
explanation: {
|
|
en: 'Tides are primarily caused by the gravitational pull of the Moon and, to a lesser extent, the Sun.',
|
|
fr: 'Les marées sont principalement causées par l\'attraction gravitationnelle de la Lune et du Soleil.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'During which moon phase do we see the most light?',
|
|
fr: 'Pendant quelle phase voit-on le plus de lumière ?',
|
|
},
|
|
options: {
|
|
en: ['New Moon', 'First Quarter', 'Full Moon', 'Waning Crescent'],
|
|
fr: ['Nouvelle Lune', 'Premier Quartier', 'Pleine Lune', 'Croissant Décroissant'],
|
|
},
|
|
correctIndex: 2,
|
|
explanation: {
|
|
en: 'The full moon is 100% illuminated, reflecting maximum sunlight toward Earth.',
|
|
fr: 'La pleine lune est illuminée à 100%, reflétant le maximum de lumière solaire.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'What is a "supermoon"?',
|
|
fr: 'Qu\'est-ce qu\'une "super lune" ?',
|
|
},
|
|
options: {
|
|
en: ['A moon larger than usual', 'A full moon at its closest point to Earth (perigee)', 'Two full moons in one month', 'A full moon during an eclipse'],
|
|
fr: ['Une lune plus grande que d\'habitude', 'Une pleine lune au point le plus proche de la Terre (périgée)', 'Deux pleines lunes dans un mois', 'Une pleine lune pendant une éclipse'],
|
|
},
|
|
correctIndex: 1,
|
|
explanation: {
|
|
en: 'A supermoon occurs when the full moon coincides with perigee, appearing about 14% bigger and 30% brighter.',
|
|
fr: 'Une super lune se produit quand la pleine lune coïncide avec le périgée, paraissant ~14% plus grande.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'How much of the Moon\'s surface can we see from Earth?',
|
|
fr: 'Quelle proportion de la surface lunaire peut-on voir depuis la Terre ?',
|
|
},
|
|
options: {
|
|
en: ['50%', '41%', '59%', '100%'],
|
|
fr: ['50%', '41%', '59%', '100%'],
|
|
},
|
|
correctIndex: 2,
|
|
explanation: {
|
|
en: 'Due to libration (slight wobble), we can see about 59% of the Moon\'s surface over time, though only 50% at any given moment.',
|
|
fr: 'Grâce à la libration, on peut voir environ 59% de la surface lunaire au fil du temps.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'What are "spring tides"?',
|
|
fr: 'Que sont les "vives-eaux" ?',
|
|
},
|
|
options: {
|
|
en: ['Tides in springtime', 'Extra high tides during full and new moons', 'Tides caused by storms', 'Low tides only'],
|
|
fr: ['Les marées au printemps', 'Marées extra hautes pendant pleine et nouvelle lune', 'Marées causées par les tempêtes', 'Marées basses uniquement'],
|
|
},
|
|
correctIndex: 1,
|
|
explanation: {
|
|
en: 'Spring tides occur when Moon and Sun align (full and new moon), creating the highest and lowest tides.',
|
|
fr: 'Les vives-eaux se produisent quand Lune et Soleil sont alignés, créant les marées les plus extrêmes.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'Why does the Moon always show the same face to Earth?',
|
|
fr: 'Pourquoi la Lune montre-t-elle toujours la même face à la Terre ?',
|
|
},
|
|
options: {
|
|
en: ['It doesn\'t rotate', 'Tidal locking — rotation period equals orbital period', 'It\'s a coincidence', 'Earth\'s magnetic field keeps it locked'],
|
|
fr: ['Elle ne tourne pas', 'Verrouillage gravitationnel — période de rotation = période orbitale', 'C\'est une coïncidence', 'Le champ magnétique terrestre la maintient'],
|
|
},
|
|
correctIndex: 1,
|
|
explanation: {
|
|
en: 'Tidal locking means the Moon rotates on its axis in the same time it takes to orbit Earth (~27.3 days).',
|
|
fr: 'Le verrouillage gravitationnel fait que la Lune tourne sur elle-même en même temps qu\'elle orbite la Terre.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'What is a "blue moon"?',
|
|
fr: 'Qu\'est-ce qu\'une "lune bleue" ?',
|
|
},
|
|
options: {
|
|
en: ['A moon that appears blue', 'The second full moon in a calendar month', 'A lunar eclipse', 'A new moon visible during daytime'],
|
|
fr: ['Une lune de couleur bleue', 'La deuxième pleine lune dans un mois', 'Une éclipse lunaire', 'Une nouvelle lune visible le jour'],
|
|
},
|
|
correctIndex: 1,
|
|
explanation: {
|
|
en: 'A blue moon is the second full moon in a single calendar month, occurring roughly every 2.7 years.',
|
|
fr: 'Une lune bleue est la deuxième pleine lune d\'un même mois, survenant environ tous les 2,7 ans.',
|
|
},
|
|
},
|
|
{
|
|
question: {
|
|
en: 'What is the Moon\'s average distance from Earth?',
|
|
fr: 'Quelle est la distance moyenne Terre-Lune ?',
|
|
},
|
|
options: {
|
|
en: ['238,900 miles (384,400 km)', '150,000 miles (241,000 km)', '500,000 miles (800,000 km)', '93 million miles (150 million km)'],
|
|
fr: ['384 400 km', '241 000 km', '800 000 km', '150 millions km'],
|
|
},
|
|
correctIndex: 0,
|
|
explanation: {
|
|
en: 'The Moon orbits at an average distance of 384,400 km (238,900 miles) from Earth.',
|
|
fr: 'La Lune orbite à une distance moyenne de 384 400 km de la Terre.',
|
|
},
|
|
},
|
|
];
|
|
|
|
export default function Quiz() {
|
|
const { locale } = useLocale();
|
|
const lang = (locale === 'fr') ? 'fr' : 'en';
|
|
|
|
const [started, setStarted] = useState(false);
|
|
const [currentQ, setCurrentQ] = useState(0);
|
|
const [selected, setSelected] = useState<number | null>(null);
|
|
const [score, setScore] = useState(0);
|
|
const [finished, setFinished] = useState(false);
|
|
const [answered, setAnswered] = useState(false);
|
|
|
|
const question = QUESTIONS[currentQ];
|
|
|
|
const handleSelect = (index: number) => {
|
|
if (answered) return;
|
|
setSelected(index);
|
|
setAnswered(true);
|
|
if (index === question.correctIndex) {
|
|
setScore(s => s + 1);
|
|
}
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (currentQ + 1 >= QUESTIONS.length) {
|
|
setFinished(true);
|
|
} else {
|
|
setCurrentQ(q => q + 1);
|
|
setSelected(null);
|
|
setAnswered(false);
|
|
}
|
|
};
|
|
|
|
const restart = () => {
|
|
setStarted(true);
|
|
setCurrentQ(0);
|
|
setSelected(null);
|
|
setScore(0);
|
|
setFinished(false);
|
|
setAnswered(false);
|
|
};
|
|
|
|
const scorePercentage = Math.round((score / QUESTIONS.length) * 100);
|
|
const getScoreMessage = () => {
|
|
if (scorePercentage >= 90) return locale === 'fr' ? '🏆 Expert lunaire !' : '🏆 Lunar Expert!';
|
|
if (scorePercentage >= 70) return locale === 'fr' ? '🌟 Très bien !' : '🌟 Great job!';
|
|
if (scorePercentage >= 50) return locale === 'fr' ? '👍 Pas mal !' : '👍 Not bad!';
|
|
return locale === 'fr' ? '📚 À réviser !' : '📚 Keep learning!';
|
|
};
|
|
|
|
return (
|
|
<section id="quiz" aria-label="Moon Knowledge Quiz" className="section-container">
|
|
<div className="text-center mb-10">
|
|
<h2 className="section-title">{ts('quiz_title', locale)}</h2>
|
|
<p className="section-subtitle mx-auto">{ts('quiz_subtitle', locale)}</p>
|
|
</div>
|
|
|
|
<div className="glass-card max-w-2xl mx-auto p-4 sm:p-6 md:p-8">
|
|
{!started ? (
|
|
<div className="text-center">
|
|
<p className="text-6xl mb-6">🌙</p>
|
|
<p className="text-lg text-white/60 mb-8">
|
|
{QUESTIONS.length} {locale === 'fr' ? 'questions sur la lune et ses mystères' : 'questions about the moon and its mysteries'}
|
|
</p>
|
|
<button onClick={() => setStarted(true)} className="glow-btn text-lg px-8 py-3">
|
|
{ts('quiz_start', locale)}
|
|
</button>
|
|
</div>
|
|
) : finished ? (
|
|
<div className="text-center">
|
|
<p className="text-6xl mb-4">{scorePercentage >= 70 ? '🎉' : '🌙'}</p>
|
|
<h3 className="text-2xl font-bold mb-2">{ts('quiz_results', locale)}</h3>
|
|
<p className="text-4xl font-bold text-indigo-300 mb-2">{score}/{QUESTIONS.length}</p>
|
|
<p className="text-lg text-white/60 mb-2">{scorePercentage}%</p>
|
|
<p className="text-xl mb-8">{getScoreMessage()}</p>
|
|
|
|
{/* Score bar */}
|
|
<div className="w-full h-3 bg-white/10 rounded-full mb-8 max-w-xs mx-auto">
|
|
<div
|
|
className={`h-full rounded-full transition-all duration-1000 ${
|
|
scorePercentage >= 70 ? 'bg-green-400' : scorePercentage >= 50 ? 'bg-yellow-400' : 'bg-red-400'
|
|
}`}
|
|
style={{ width: `${scorePercentage}%` }}
|
|
/>
|
|
</div>
|
|
|
|
<button onClick={restart} className="glow-btn">
|
|
{ts('quiz_restart', locale)}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Progress */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<span className="text-sm text-white/40">
|
|
{currentQ + 1}/{QUESTIONS.length}
|
|
</span>
|
|
<div className="flex-1 mx-4 h-1.5 bg-white/10 rounded-full">
|
|
<div
|
|
className="h-full bg-indigo-400 rounded-full transition-all duration-300"
|
|
style={{ width: `${((currentQ + 1) / QUESTIONS.length) * 100}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-sm text-indigo-300">{ts('quiz_score', locale)}: {score}</span>
|
|
</div>
|
|
|
|
{/* Question */}
|
|
<h3 className="text-xl font-semibold mb-6">
|
|
{question.question[lang] || question.question.en}
|
|
</h3>
|
|
|
|
{/* Options */}
|
|
<div className="space-y-3 mb-6">
|
|
{(question.options[lang] || question.options.en).map((option, i) => (
|
|
<button
|
|
key={i}
|
|
onClick={() => handleSelect(i)}
|
|
className={`quiz-option w-full text-left ${
|
|
answered
|
|
? i === question.correctIndex
|
|
? 'correct'
|
|
: i === selected
|
|
? 'wrong'
|
|
: ''
|
|
: ''
|
|
}`}
|
|
>
|
|
<span className="flex items-center gap-3">
|
|
<span className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold border ${
|
|
answered && i === question.correctIndex
|
|
? 'bg-green-500/20 border-green-400 text-green-300'
|
|
: answered && i === selected
|
|
? 'bg-red-500/20 border-red-400 text-red-300'
|
|
: 'border-white/20 text-white/50'
|
|
}`}>
|
|
{String.fromCharCode(65 + i)}
|
|
</span>
|
|
{option}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Explanation */}
|
|
{answered && (
|
|
<div className={`p-4 rounded-xl mb-6 ${
|
|
selected === question.correctIndex
|
|
? 'bg-green-500/10 border border-green-500/20'
|
|
: 'bg-red-500/10 border border-red-500/20'
|
|
}`}>
|
|
<p className="font-semibold mb-1">
|
|
{selected === question.correctIndex ? ts('quiz_correct', locale) : ts('quiz_wrong', locale)}
|
|
</p>
|
|
<p className="text-sm text-white/70">
|
|
{question.explanation[lang] || question.explanation.en}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{answered && (
|
|
<button onClick={handleNext} className="glow-btn w-full">
|
|
{currentQ + 1 >= QUESTIONS.length ? ts('quiz_results', locale) : ts('quiz_next', locale)}
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|