🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
628 lines
17 KiB
HTML
628 lines
17 KiB
HTML
{{- define "main" }}
|
|
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "WebApplication",
|
|
"name": "METAR Quiz",
|
|
"description": "{{ .Description }}",
|
|
"url": "{{ .Permalink }}",
|
|
"applicationCategory": "EducationalApplication",
|
|
"operatingSystem": "Any",
|
|
"offers": {
|
|
"@type": "Offer",
|
|
"price": "0",
|
|
"priceCurrency": "USD"
|
|
},
|
|
"author": {
|
|
"@type": "Person",
|
|
"name": "{{ .Site.Params.author }}"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.quiz-container {
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.quiz-header {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.quiz-controls {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: center;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.quiz-controls select {
|
|
padding: 0.5rem 1rem;
|
|
font-size: 1rem;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--border);
|
|
background: var(--code-bg);
|
|
color: var(--primary);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.quiz-controls select:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.start-btn, .submit-btn, .next-btn, .retake-btn, .stop-btn {
|
|
padding: 0.75rem 2rem;
|
|
font-size: 1.1rem;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
background: var(--primary);
|
|
color: var(--theme);
|
|
font-weight: 600;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.start-btn:hover, .submit-btn:hover, .next-btn:hover, .retake-btn:hover, .stop-btn:hover {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.stop-btn {
|
|
background: #666;
|
|
}
|
|
|
|
.quiz-area {
|
|
display: none;
|
|
text-align: center;
|
|
}
|
|
|
|
.quiz-area.active {
|
|
display: block;
|
|
}
|
|
|
|
.question-prompt {
|
|
font-size: 1.1rem;
|
|
color: var(--secondary);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.question-text {
|
|
font-size: 1.8rem;
|
|
font-weight: 700;
|
|
margin-bottom: 1.5rem;
|
|
color: var(--primary);
|
|
padding: 1rem;
|
|
background: var(--code-bg);
|
|
border-radius: 8px;
|
|
min-height: 3rem;
|
|
}
|
|
|
|
.answer-input {
|
|
padding: 0.75rem 1rem;
|
|
font-size: 1.2rem;
|
|
border: 2px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--code-bg);
|
|
color: var(--primary);
|
|
width: 200px;
|
|
text-align: center;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.answer-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.input-row {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.progress-text {
|
|
font-size: 1rem;
|
|
color: var(--secondary);
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.feedback {
|
|
font-size: 1.2rem;
|
|
font-weight: 600;
|
|
margin: 1rem 0;
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
display: none;
|
|
}
|
|
|
|
.feedback.correct {
|
|
display: block;
|
|
background: rgba(34, 197, 94, 0.15);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.feedback.incorrect {
|
|
display: block;
|
|
background: rgba(239, 68, 68, 0.15);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.results-area {
|
|
display: none;
|
|
text-align: center;
|
|
}
|
|
|
|
.results-area.active {
|
|
display: block;
|
|
}
|
|
|
|
.score-display {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 1rem;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.score-subtitle {
|
|
font-size: 1.2rem;
|
|
color: var(--secondary);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.results-breakdown {
|
|
text-align: left;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
margin-bottom: 1.5rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.result-item {
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.result-item.correct {
|
|
background: rgba(34, 197, 94, 0.08);
|
|
}
|
|
|
|
.result-item.incorrect {
|
|
background: rgba(239, 68, 68, 0.08);
|
|
}
|
|
|
|
.result-question {
|
|
flex: 1;
|
|
}
|
|
|
|
.result-answers {
|
|
font-family: monospace;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.result-your-answer {
|
|
color: var(--secondary);
|
|
}
|
|
|
|
.result-correct-answer {
|
|
color: #22c55e;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.result-icon {
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.shuffle-stats {
|
|
font-size: 1.1rem;
|
|
color: var(--secondary);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.shuffle-stats .correct-count {
|
|
color: #22c55e;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.shuffle-stats .incorrect-count {
|
|
color: #ef4444;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.setup-area {
|
|
text-align: center;
|
|
}
|
|
|
|
.button-row {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: center;
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|
|
|
|
<article class="quiz-container">
|
|
<header class="quiz-header">
|
|
<h1>METAR Quiz</h1>
|
|
<p class="post-description">Practice aviation weather abbreviations for METAR and TAF reports</p>
|
|
</header>
|
|
|
|
<div id="setup" class="setup-area">
|
|
<div class="quiz-controls">
|
|
<select id="modeSelect">
|
|
<option value="quiz">Quiz Mode</option>
|
|
<option value="shuffle">Shuffle Mode</option>
|
|
</select>
|
|
<select id="difficultySelect">
|
|
<option value="normal">Weather Only</option>
|
|
<option value="taf">Weather + TAF</option>
|
|
<option value="hard">All (Hard)</option>
|
|
</select>
|
|
</div>
|
|
<button class="start-btn" onclick="startQuiz()">Start</button>
|
|
</div>
|
|
|
|
<div id="quizArea" class="quiz-area">
|
|
<p class="question-prompt">What is the abbreviation for:</p>
|
|
<div id="questionText" class="question-text"></div>
|
|
|
|
<div class="input-row">
|
|
<input type="text" id="answerInput" class="answer-input" placeholder="Enter code" autocomplete="off" maxlength="10">
|
|
<button class="submit-btn" onclick="submitAnswer()">Submit</button>
|
|
</div>
|
|
|
|
<div id="feedback" class="feedback"></div>
|
|
|
|
<p id="progressText" class="progress-text"></p>
|
|
<div id="shuffleStats" class="shuffle-stats" style="display: none;">
|
|
Correct: <span class="correct-count" id="correctCount">0</span> |
|
|
Wrong: <span class="incorrect-count" id="wrongCount">0</span>
|
|
</div>
|
|
|
|
<div class="button-row" id="stopRow" style="display: none;">
|
|
<button class="stop-btn" onclick="stopShuffle()">Stop</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="resultsArea" class="results-area">
|
|
<div id="scoreDisplay" class="score-display"></div>
|
|
<p id="scoreSubtitle" class="score-subtitle"></p>
|
|
<div id="resultsBreakdown" class="results-breakdown"></div>
|
|
<button class="retake-btn" onclick="retakeQuiz()">Retake Quiz</button>
|
|
</div>
|
|
</article>
|
|
|
|
<script>
|
|
(function() {
|
|
// METAR Data
|
|
const PRECIPITATION = [
|
|
{ abbr: "RA", desc: "Rain" },
|
|
{ abbr: "SN", desc: "Snow" },
|
|
{ abbr: "DZ", desc: "Drizzle" },
|
|
{ abbr: "GR", desc: "Hail" },
|
|
{ abbr: "GS", desc: "Small Hail/Snow Pellets" },
|
|
{ abbr: "PL", desc: "Ice Pellets" },
|
|
{ abbr: "SG", desc: "Snow Grains" },
|
|
{ abbr: "IC", desc: "Ice Crystals" },
|
|
{ abbr: "UP", desc: "Unknown Precipitation" }
|
|
];
|
|
|
|
const DESCRIPTORS = [
|
|
{ abbr: "TS", desc: "Thunderstorm" },
|
|
{ abbr: "SH", desc: "Showers" },
|
|
{ abbr: "FZ", desc: "Freezing" },
|
|
{ abbr: "BL", desc: "Blowing" },
|
|
{ abbr: "DR", desc: "Drifting" },
|
|
{ abbr: "MI", desc: "Shallow" },
|
|
{ abbr: "BC", desc: "Patches" },
|
|
{ abbr: "PR", desc: "Partial" }
|
|
];
|
|
|
|
const OBSCURATIONS = [
|
|
{ abbr: "FG", desc: "Fog" },
|
|
{ abbr: "BR", desc: "Mist" },
|
|
{ abbr: "HZ", desc: "Haze" },
|
|
{ abbr: "FU", desc: "Smoke" },
|
|
{ abbr: "DU", desc: "Widespread Dust" },
|
|
{ abbr: "SA", desc: "Sand" },
|
|
{ abbr: "VA", desc: "Volcanic Ash" },
|
|
{ abbr: "PY", desc: "Spray" }
|
|
];
|
|
|
|
const SKY_CONDITIONS = [
|
|
{ abbr: "SKC", desc: "Sky Clear" },
|
|
{ abbr: "CLR", desc: "Clear (automated)" },
|
|
{ abbr: "FEW", desc: "Few (1-2 oktas)" },
|
|
{ abbr: "SCT", desc: "Scattered (3-4 oktas)" },
|
|
{ abbr: "BKN", desc: "Broken (5-7 oktas)" },
|
|
{ abbr: "OVC", desc: "Overcast (8 oktas)" },
|
|
{ abbr: "VV", desc: "Vertical Visibility" },
|
|
{ abbr: "CB", desc: "Cumulonimbus" },
|
|
{ abbr: "TCU", desc: "Towering Cumulus" }
|
|
];
|
|
|
|
const OTHER_WEATHER = [
|
|
{ abbr: "VC", desc: "Vicinity" },
|
|
{ abbr: "FC", desc: "Funnel Cloud" },
|
|
{ abbr: "+FC", desc: "Tornado/Waterspout" },
|
|
{ abbr: "PO", desc: "Dust/Sand Whirls" },
|
|
{ abbr: "SQ", desc: "Squall" },
|
|
{ abbr: "SS", desc: "Sandstorm" },
|
|
{ abbr: "DS", desc: "Dust Storm" }
|
|
];
|
|
|
|
const TAF_CODES = [
|
|
{ abbr: "BECMG", desc: "Becoming" },
|
|
{ abbr: "TEMPO", desc: "Temporary" },
|
|
{ abbr: "PROB30", desc: "30% Probability" },
|
|
{ abbr: "PROB40", desc: "40% Probability" },
|
|
{ abbr: "FM", desc: "From (time)" },
|
|
{ abbr: "NSW", desc: "No Significant Weather" },
|
|
{ abbr: "CAVOK", desc: "Ceiling and Visibility OK" }
|
|
];
|
|
|
|
const NON_WEATHER = [
|
|
{ abbr: "FMH-1", desc: "Federal Meteorological Handbook No. 1" },
|
|
{ abbr: "FMH2", desc: "Federal Meteorological Handbook No. 2" },
|
|
{ abbr: "FIBI", desc: "Filed But Impracticable to Transmit" },
|
|
{ abbr: "METAR", desc: "Meteorological Aerodrome Report" },
|
|
{ abbr: "SPECI", desc: "Special (unscheduled observation)" },
|
|
{ abbr: "AUTO", desc: "Automated observation" },
|
|
{ abbr: "COR", desc: "Correction" },
|
|
{ abbr: "RMK", desc: "Remarks" },
|
|
{ abbr: "AO1", desc: "Automated without precipitation sensor" },
|
|
{ abbr: "AO2", desc: "Automated with precipitation sensor" },
|
|
{ abbr: "SLP", desc: "Sea Level Pressure" },
|
|
{ abbr: "NOSIG", desc: "No Significant Change" }
|
|
];
|
|
|
|
// State
|
|
let mode = 'quiz';
|
|
let difficulty = 'normal';
|
|
let questions = [];
|
|
let currentIndex = 0;
|
|
let answers = [];
|
|
let correctCount = 0;
|
|
let incorrectCount = 0;
|
|
let waitingForNext = false;
|
|
|
|
// DOM Elements
|
|
const setup = document.getElementById('setup');
|
|
const quizArea = document.getElementById('quizArea');
|
|
const resultsArea = document.getElementById('resultsArea');
|
|
const questionText = document.getElementById('questionText');
|
|
const answerInput = document.getElementById('answerInput');
|
|
const feedback = document.getElementById('feedback');
|
|
const progressText = document.getElementById('progressText');
|
|
const shuffleStats = document.getElementById('shuffleStats');
|
|
const stopRow = document.getElementById('stopRow');
|
|
const correctCountEl = document.getElementById('correctCount');
|
|
const wrongCountEl = document.getElementById('wrongCount');
|
|
const scoreDisplay = document.getElementById('scoreDisplay');
|
|
const scoreSubtitle = document.getElementById('scoreSubtitle');
|
|
const resultsBreakdown = document.getElementById('resultsBreakdown');
|
|
const modeSelect = document.getElementById('modeSelect');
|
|
const difficultySelect = document.getElementById('difficultySelect');
|
|
|
|
// Fisher-Yates shuffle
|
|
function shuffle(array) {
|
|
const arr = [...array];
|
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Build questions with intensity variants for precipitation
|
|
function buildQuestions() {
|
|
let items = [];
|
|
|
|
// Add precipitation with intensity variants
|
|
PRECIPITATION.forEach(p => {
|
|
// Random intensity: 0 = light, 1 = moderate, 2 = heavy
|
|
const intensity = Math.floor(Math.random() * 3);
|
|
if (intensity === 0) {
|
|
items.push({ abbr: "-" + p.abbr, desc: "Light " + p.desc });
|
|
} else if (intensity === 2) {
|
|
items.push({ abbr: "+" + p.abbr, desc: "Heavy " + p.desc });
|
|
} else {
|
|
items.push(p);
|
|
}
|
|
});
|
|
|
|
// Add other weather categories (actual weather phenomena)
|
|
items = items.concat(DESCRIPTORS, OBSCURATIONS, SKY_CONDITIONS, OTHER_WEATHER);
|
|
|
|
// Add TAF format codes for taf and hard modes
|
|
if (difficulty === 'taf' || difficulty === 'hard') {
|
|
items = items.concat(TAF_CODES);
|
|
}
|
|
|
|
// Add non-weather codes for hard mode only
|
|
if (difficulty === 'hard') {
|
|
items = items.concat(NON_WEATHER);
|
|
}
|
|
|
|
return shuffle(items);
|
|
}
|
|
|
|
// Start quiz
|
|
window.startQuiz = function() {
|
|
mode = modeSelect.value;
|
|
difficulty = difficultySelect.value;
|
|
questions = buildQuestions();
|
|
currentIndex = 0;
|
|
answers = [];
|
|
correctCount = 0;
|
|
incorrectCount = 0;
|
|
waitingForNext = false;
|
|
|
|
setup.style.display = 'none';
|
|
resultsArea.classList.remove('active');
|
|
quizArea.classList.add('active');
|
|
|
|
if (mode === 'shuffle') {
|
|
shuffleStats.style.display = 'block';
|
|
stopRow.style.display = 'flex';
|
|
progressText.style.display = 'none';
|
|
} else {
|
|
shuffleStats.style.display = 'none';
|
|
stopRow.style.display = 'none';
|
|
progressText.style.display = 'block';
|
|
}
|
|
|
|
showQuestion();
|
|
};
|
|
|
|
// Show current question
|
|
function showQuestion() {
|
|
if (mode === 'quiz' && currentIndex >= questions.length) {
|
|
showResults();
|
|
return;
|
|
}
|
|
|
|
// For shuffle mode, loop back
|
|
if (mode === 'shuffle' && currentIndex >= questions.length) {
|
|
questions = buildQuestions();
|
|
currentIndex = 0;
|
|
}
|
|
|
|
const q = questions[currentIndex];
|
|
questionText.textContent = q.desc;
|
|
answerInput.value = '';
|
|
answerInput.focus();
|
|
feedback.className = 'feedback';
|
|
feedback.style.display = 'none';
|
|
waitingForNext = false;
|
|
|
|
if (mode === 'quiz') {
|
|
progressText.textContent = `Question ${currentIndex + 1} of ${questions.length}`;
|
|
}
|
|
|
|
correctCountEl.textContent = correctCount;
|
|
wrongCountEl.textContent = incorrectCount;
|
|
}
|
|
|
|
// Submit answer
|
|
window.submitAnswer = function() {
|
|
if (waitingForNext) return;
|
|
|
|
const userAnswer = answerInput.value.trim().toUpperCase();
|
|
if (!userAnswer) return;
|
|
|
|
// Prevent double-submit in shuffle mode
|
|
if (mode === 'shuffle') {
|
|
waitingForNext = true;
|
|
}
|
|
|
|
const q = questions[currentIndex];
|
|
const correctAnswer = q.abbr.toUpperCase();
|
|
const isCorrect = userAnswer === correctAnswer;
|
|
|
|
if (mode === 'quiz') {
|
|
answers.push({
|
|
question: q.desc,
|
|
userAnswer: userAnswer,
|
|
correctAnswer: q.abbr,
|
|
isCorrect: isCorrect
|
|
});
|
|
currentIndex++;
|
|
|
|
if (currentIndex >= questions.length) {
|
|
showResults();
|
|
} else {
|
|
showQuestion();
|
|
}
|
|
} else {
|
|
// Shuffle mode - show feedback and auto-advance
|
|
if (isCorrect) {
|
|
correctCount++;
|
|
feedback.textContent = 'Correct!';
|
|
feedback.className = 'feedback correct';
|
|
} else {
|
|
incorrectCount++;
|
|
feedback.textContent = `Incorrect. The answer is: ${q.abbr}`;
|
|
feedback.className = 'feedback incorrect';
|
|
}
|
|
feedback.style.display = 'block';
|
|
|
|
correctCountEl.textContent = correctCount;
|
|
wrongCountEl.textContent = incorrectCount;
|
|
|
|
// Auto-advance after showing feedback
|
|
currentIndex++;
|
|
setTimeout(() => {
|
|
showQuestion();
|
|
}, isCorrect ? 800 : 1500);
|
|
}
|
|
};
|
|
|
|
// Stop shuffle
|
|
window.stopShuffle = function() {
|
|
quizArea.classList.remove('active');
|
|
setup.style.display = 'block';
|
|
};
|
|
|
|
// Show results (quiz mode)
|
|
function showResults() {
|
|
quizArea.classList.remove('active');
|
|
resultsArea.classList.add('active');
|
|
|
|
const correct = answers.filter(a => a.isCorrect).length;
|
|
const total = answers.length;
|
|
const percentage = Math.round((correct / total) * 100);
|
|
|
|
scoreDisplay.textContent = `${percentage}%`;
|
|
scoreSubtitle.textContent = `${correct} out of ${total} correct`;
|
|
|
|
let html = '';
|
|
answers.forEach(a => {
|
|
const statusClass = a.isCorrect ? 'correct' : 'incorrect';
|
|
const icon = a.isCorrect ? '✓' : '✗';
|
|
html += `
|
|
<div class="result-item ${statusClass}">
|
|
<span class="result-question">${a.question}</span>
|
|
<span class="result-answers">
|
|
<span class="result-your-answer">${a.userAnswer}</span>
|
|
${!a.isCorrect ? ` → <span class="result-correct-answer">${a.correctAnswer}</span>` : ''}
|
|
</span>
|
|
<span class="result-icon">${icon}</span>
|
|
</div>
|
|
`;
|
|
});
|
|
resultsBreakdown.innerHTML = html;
|
|
}
|
|
|
|
// Retake quiz
|
|
window.retakeQuiz = function() {
|
|
resultsArea.classList.remove('active');
|
|
setup.style.display = 'block';
|
|
};
|
|
|
|
// Enter key to submit
|
|
answerInput.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
submitAnswer();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
{{- end }}{{/* end main */}}
|