JavaScript for Recitation Testers
Recitation Testers: Full Code and Explanation
This post explains the JavaScript for the Kings and Queens recitation tester webpage and the The Elements tester.
Both the K&Q and Elements pages were developed by Gemini 2.5Pro, using the prompt and follow up discussion shown in Section 3. The out-the-box design was impressive: clean, colorful and using Bootstrap.
Section 4 shows the full source code.
Step-by-Step Guide to How the Page Works
1. Page Load and Initial Setup
When you first open the HTML file, the browser reads it from top to bottom. It sets up the page structure and applies the initial styles. When it reaches the <script>
tag, it waits for a specific event before running the code inside.
document.addEventListener('DOMContentLoaded', () => {
// All the JavaScript code is inside here
; })
Explanation
document.addEventListener('DOMContentLoaded', ...)
tells the browser: “Don’t run any of the JavaScript inside this block until the entire HTML page has been fully loaded and is ready.” This prevents errors that could happen if the script tried to find an HTML element (like a button) that hasn’t been created yet.
2. Storing the Data
The first thing the script does is define all the data needed for the test.
const allMonarchs = [
name: 'Alfred the Great', dates: '871-899' },
{ // ... and so on for all monarchs
;
]
const confessorIndex = allMonarchs.findIndex(m => m.name === 'Edward the Confessor');
const confessorMonarchs = allMonarchs.slice(confessorIndex);
Explanation
const allMonarchs = [...]
creates an array (a list) of JavaScript objects. Each object{...}
represents a monarch and has two properties: aname
and theirdates
. This is the master list.allMonarchs.findIndex(...)
searches through theallMonarchs
array to find the exact position (the index number) of “Edward the Confessor”.allMonarchs.slice(confessorIndex)
then creates a new array calledconfessorMonarchs
by “slicing” the original one, starting from Edward the Confessor’s position to the end.
3. Connecting JavaScript to HTML Elements
Next, the script gets references to all the important interactive parts of the HTML page so it can read from or write to them later.
// DOM Elements
const scoreboard = document.getElementById('scoreboard');
const timerEl = document.getElementById('timer');
// ... etc. for all the buttons, display areas, and radio inputs
Explanation
document.getElementById('some-id')
finds an HTML element with a specific id attribute. For example, const timerEl = document.getElementById('timer');
finds the <p id="timer">
element and stores a reference to it in a variable named timerEl
. Now, to change the timer’s text, we can just use timerEl
.
4. Setting Up the Application’s State
These variables keep track of the current status of the test.
// State
let activeList = allMonarchs;
let currentIndex = 0;
let correctCount = 0;
let incorrectCount = 0;
let isRunning = false;
Explanation
let
is used because these values will change.
activeList
: Holds the list currently being used for the test (allMonarchs
orconfessorMonarchs
).currentIndex
: Tracks which monarch in theactiveList
you are on. Starts at0
(the first one).correctCount
&incorrectCount
: Track your score.isRunning
: Atrue/false
flag that tells the code if the test is active.
5. Handling User Input (Clicks and Keystrokes)
The script sets up “event listeners” that wait for user actions.
.addEventListener('click', () => { /* ... */ });
startBtn.addEventListener('click', () => { /* ... */ });
revealBtn.addEventListener('click', () => { /* ... */ });
incorrectBtn.addEventListener('change', resetForNewList);
allMonarchsRadiodocument.addEventListener('keydown', (event) => { /* ... */ });
Explanation
.addEventListener('click', ...)
tells an element (likestartBtn
) to run a function when it’s clicked.- The radio button listeners use the ‘
change
’ event, which fires when one is selected. - The
keydown
listener is attached to the whole document, so it catches any key press.
6. The Main Functions: How the Test Works
When you click “Start”: The startBtn
’s click listener calls startGame()
.
function startGame() {
= true; // The test is now active!
isRunning // ... Resets counts and displays ...
= Date.now(); // Records the start time
startTime = setInterval(updateTimer, 1000); // Calls updateTimer every second
timerInterval }
When you click “Reveal”: The revealBtn
’s click listener fires.
.addEventListener('click', () => {
revealBtnif (!isRunning) return; // Do nothing if game hasn't started
++; // Assume correct, add 1 to score
correctCountupdateDisplay(); // Show the current monarch's details
++; // Move to the next monarch in the list
currentIndexif (currentIndex >= activeList.length) {
endGame(); // If we're at the end, finish the test
}; })
This calls updateDisplay()
to show the information:
function updateDisplay() {
const monarch = activeList[currentIndex];
.innerHTML = `...`; // Update HTML with name and dates
elementTextEl.textContent = `${currentIndex + 1} / ${activeList.length}`;
progressElupdateScores();
}
When you click “Incorrect
”: The incorrectBtn
’s click listener fires.
.addEventListener('click', () => {
incorrectBtnif (!isRunning) return;
if (correctCount > 0) {
--; // Subtract 1 from 'correct'
correctCount++; // Add 1 to 'incorrect'
incorrectCountupdateScores(); // Update the scoreboard display
}; })
When the list is finished: The endGame()
function is called. It displays your overall score and correct percentage.
function endGame() {
= false;
isRunning clearInterval(timerInterval); // Stop the timer
// ... Hides game buttons, shows start button again ...
// Calculates and displays the final summary message
const now = new Date();
const formattedDateTime = now.toLocaleString(...);
const percentage = ((correctCount / activeList.length) * 100).toFixed(1);
.innerHTML = `Test Completed at ...`;
resultsTextEl.classList.remove('hidden');
resultsEl }
Original Prompt
The original prompt was voice-to-text dictated while making my breakfast. It’s pretty rough. It was followed up with some fine tuning, but the out-the-box effort was very competent. Full conversation is a link to the full conversation.
I am learning to recite the periodic table, Tom Lehrer style. Except I am going to recite the elements in atomic number order. I would like to write a small webpage to test me in this process. The page will work as follows. It will have a heading of webpage tester, and then a big button in the middle that says start. When you click start a counter will start and you will show the counter elapsed time in the top right hand side of the page somewhere below the head. The test will be on the honor system. I will be saying the elements and then I will click the button and each time I click the button it will show in a text box the symbol and the name of the next element. so the first time I click the button it goes start then the next time I click the button it shows hydrogen H or H hydrogen. let’s do it that way. And so forth and I keep clicking the button and I am saying the words and then I’m checking that they’re correct and I guess actually there’s two buttons there there’s a button for correct that’s gonna be on the right and then next to it, there will be a button for incorrect and if I get it wrong, I will press incorrect. and then along the top We should also keep score correct number of elements and that type of thing. So hopefully you can see what I’ve got in mind there. I imagine we can do this all is just a static page with some Javascript. Please use bootstrap styling. Keep it nice and clean The buttons in the text fairly large. obviously to work on our phone an iPad or a computer. Let me know if you have any questions before we get coding.
Appendix: Full Source Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/static/kqfavicon.ico">
<title>Gemini Monarchs Recitation Tester</title>
<script src="[https://cdn.tailwindcss.com](https://cdn.tailwindcss.com)"></script>
<link rel="preconnect" href="[https://fonts.googleapis.com](https://fonts.googleapis.com)">
<link rel="preconnect" href="[https://fonts.gstatic.com](https://fonts.gstatic.com)" crossorigin>
<link href="[https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap](https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap)" rel="stylesheet">
<style>
body {font-family: 'Inter', sans-serif;
touch-action: manipulation;
}.hidden { display: none; }
.radio-label {
border: 2px solid #D1D5DB;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease-in-out;
}[type="radio"]:checked + .radio-label {
inputbackground-color: #3B82F6;
border-color: #3B82F6;
color: white;
}</style>
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col items-center justify-center min-h-screen p-4">
<div class="w-full max-w-2xl mx-auto bg-white rounded-xl shadow-lg p-6 md:p-8 text-center">
<header class="mb-6">
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Monarchs Recitation Tester</h1>
<p id="instructions" class="text-gray-500 mt-2">Select a list and press Start. Use Enter/k for Reveal, and Shift+Enter/j/l for Incorrect.</p>
</header>
<div id="scoreboard" class="hidden absolute top-4 right-4 grid grid-cols-2 gap-x-4 text-left bg-gray-50 p-3 rounded-lg shadow-sm">
<div>
<span class="text-sm font-medium text-gray-500">Correct:</span>
<span id="correct-count" class="text-lg font-bold text-green-600">0</span>
</div>
<div>
<span class="text-sm font-medium text-gray-500">Incorrect:</span>
<span id="incorrect-count" class="text-lg font-bold text-red-600">0</span>
</div>
<div class="col-span-2 mt-2">
<span class="text-sm font-medium text-gray-500">Progress:</span>
<span id="progress" class="text-lg font-bold text-gray-700">0 / 0</span>
</div>
</div>
<p id="timer" class="absolute top-4 left-4 text-lg font-semibold text-gray-700 bg-gray-50 p-3 rounded-lg shadow-sm hidden">00:00</p>
<main class="min-h-[12rem] flex items-center justify-center flex-col">
<div id="element-display" class="mb-8">
<p id="element-text" class="text-4xl md:text-5xl font-bold text-blue-600"></p>
</div>
<div id="toggle-container" class="mb-8 flex justify-center items-center space-x-4">
<input type="radio" id="allMonarchs" name="monarch-list" value="all" class="sr-only" checked>
<label for="allMonarchs" class="radio-label">All Monarchs</label>
<input type="radio" id="fromConfessor" name="monarch-list" value="confessor" class="sr-only">
<label for="fromConfessor" class="radio-label">From Edward the Confessor</label>
</div>
<button id="start-btn" class="w-full max-w-xs bg-blue-500 hover:bg-blue-600 text-white font-bold py-4 px-6 rounded-lg text-2xl transition duration-300 ease-in-out transform hover:scale-105">
Start</button>
<div id="game-buttons" class="hidden w-full flex flex-col sm:flex-row justify-center items-center gap-4">
<button id="reveal-btn" class="w-full sm:w-auto bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-8 rounded-lg text-xl transition duration-300">Reveal</button>
<button id="incorrect-btn" class="w-full sm:w-auto bg-red-500 hover:bg-red-600 text-white font-bold py-3 px-8 rounded-lg text-xl transition duration-300">Incorrect</button>
</div>
<div id="results" class="hidden mt-8 p-4 bg-green-50 border border-green-200 rounded-lg">
<p id="results-text" class="text-lg text-green-800"></p>
</div>
</main>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const allMonarchs = [
name: 'Alfred the Great', dates: '871-899' }, { name: 'Edward the Elder', dates: '899-924' },
{ name: 'Athelstan', dates: '924-939' }, { name: 'Edmund I', dates: '939-946' },
{ name: 'Eadred', dates: '946-955' }, { name: 'Eadwig', dates: '955-959' },
{ name: 'Edgar the Peaceful', dates: '959-975' }, { name: 'Edward the Martyr', dates: '975-978' },
{ name: 'Æthelred the Unready', dates: '978-1016' }, { name: 'Edmund II (Ironside)', dates: '1016' },
{ name: 'Cnut the Great', dates: '1016-1035' }, { name: 'Harold I (Harefoot)', dates: '1035-1040' },
{ name: 'Harthacnut', dates: '1040-1042' }, { name: 'Edward the Confessor', dates: '1042-1066' },
{ name: 'Harold II', dates: '1066' }, { name: 'William I (the Conqueror)', dates: '1066-1087' },
{ name: 'William II (Rufus)', dates: '1087-1100' }, { name: 'Henry I (Beauclerc)', dates: '1100-1135' },
{ name: 'Stephen', dates: '1135-1154' }, { name: 'Henry II', dates: '1154-1189' },
{ name: 'Richard I (the Lionheart)', dates: '1189-1199' }, { name: 'John (Lackland)', dates: '1199-1216' },
{ name: 'Henry III', dates: '1216-1272' }, { name: 'Edward I (Longshanks)', dates: '1272-1307' },
{ name: 'Edward II', dates: '1307-1327' }, { name: 'Edward III', dates: '1327-1377' },
{ name: 'Richard II', dates: '1377-1399' }, { name: 'Henry IV', dates: '1399-1413' },
{ name: 'Henry V', dates: '1413-1422' }, { name: 'Henry VI', dates: '1422-1461, 1470-1471' },
{ name: 'Edward IV', dates: '1461-1470, 1471-1483' }, { name: 'Edward V', dates: '1483' },
{ name: 'Richard III', dates: '1483-1485' }, { name: 'Henry VII', dates: '1485-1509' },
{ name: 'Henry VIII', dates: '1509-1547' }, { name: 'Edward VI', dates: '1547-1553' },
{ name: 'Jane (the Nine Days\' Queen)', dates: '1553' }, { name: 'Mary I (Bloody Mary)', dates: '1553-1558' },
{ name: 'Elizabeth I (the Virgin Queen)', dates: '1558-1603' }, { name: 'James I & VI', dates: '1603-1625' },
{ name: 'Charles I', dates: '1625-1649' }, { name: 'Charles II (the Merry Monarch)', dates: '1660-1685' },
{ name: 'James II & VII', dates: '1685-1688' }, { name: 'William III & Mary II', dates: '1689-1694' },
{ name: 'William III', dates: '1694-1702' }, { name: 'Anne', dates: '1702-1714' },
{ name: 'George I', dates: '1714-1727' }, { name: 'George II', dates: '1727-1760' },
{ name: 'George III', dates: '1760-1820' }, { name: 'George IV', dates: '1820-1830' },
{ name: 'William IV', dates: '1830-1837' }, { name: 'Victoria', dates: '1837-1901' },
{ name: 'Edward VII', dates: '1901-1010' }, { name: 'George V', dates: '1910-1936' },
{ name: 'Edward VIII', dates: '1936' }, { name: 'George VI', dates: '1936-1952' },
{ name: 'Elizabeth II', dates: '1952-2022' }, { name: 'Charles III', dates: '2022-Present' }
{ ;
]
const confessorIndex = allMonarchs.findIndex(m => m.name === 'Edward the Confessor');
const confessorMonarchs = allMonarchs.slice(confessorIndex);
// DOM Elements
const scoreboard = document.getElementById('scoreboard');
const timerEl = document.getElementById('timer');
const progressEl = document.getElementById('progress');
const elementTextEl = document.getElementById('element-text');
const startBtn = document.getElementById('start-btn');
const revealBtn = document.getElementById('reveal-btn');
const incorrectBtn = document.getElementById('incorrect-btn');
const gameButtons = document.getElementById('game-buttons');
const correctCountEl = document.getElementById('correct-count');
const incorrectCountEl = document.getElementById('incorrect-count');
const resultsEl = document.getElementById('results');
const resultsTextEl = document.getElementById('results-text');
const instructions = document.getElementById('instructions');
const toggleContainer = document.getElementById('toggle-container');
const allMonarchsRadio = document.getElementById('allMonarchs');
const confessorRadio = document.getElementById('fromConfessor');
// State
let activeList = allMonarchs;
let currentIndex = 0;
let correctCount = 0;
let incorrectCount = 0;
let timerInterval;
let startTime;
let isRunning = false;
// Functions
function updateTimer() {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0');
const seconds = (elapsed % 60).toString().padStart(2, '0');
.textContent = `${minutes}:${seconds}`;
timerEl
}
function updateScores() {
.textContent = correctCount;
correctCountEl.textContent = incorrectCount;
incorrectCountEl
}
function updateDisplay() {
const monarch = activeList[currentIndex];
.innerHTML = `${monarch.name}<br><span class="text-2xl md:text-3xl font-medium text-gray-500">${monarch.dates}</span>`;
elementTextEl.textContent = `${currentIndex + 1} / ${activeList.length}`;
progressElupdateScores();
}
function startGame() {
= true;
isRunning = 0;
currentIndex = 0;
correctCount = 0;
incorrectCount .innerHTML = '';
elementTextEl.classList.add('hidden');
resultsEl.classList.add('hidden');
startBtn.classList.add('hidden');
instructions.classList.add('hidden');
toggleContainer.classList.remove('hidden');
gameButtons.classList.remove('hidden');
scoreboard.classList.remove('hidden');
timerEl
updateScores();
.textContent = `0 / ${activeList.length}`;
progressEl
= Date.now();
startTime = setInterval(updateTimer, 1000);
timerInterval
}
function endGame() {
= false;
isRunning clearInterval(timerInterval);
.classList.add('hidden');
gameButtons.classList.remove('hidden');
startBtn.textContent = 'Restart';
startBtn.classList.remove('hidden');
instructions.classList.remove('hidden');
toggleContainer
const now = new Date();
const formattedDateTime = now.toLocaleString(undefined, {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
;
})
const percentage = activeList.length > 0 ? ((correctCount / activeList.length) * 100).toFixed(1) : 0;
.innerHTML = `<strong>Test Completed at ${formattedDateTime}</strong><br>
resultsTextEl ${correctCount} correct and ${incorrectCount} incorrect (${percentage}% correct).`;
.classList.remove('hidden');
resultsEl
}
function resetForNewList() {
= allMonarchsRadio.checked ? allMonarchs : confessorMonarchs;
activeList = 0;
currentIndex = 0;
correctCount = 0;
incorrectCount .innerHTML = '';
elementTextEl.classList.add('hidden');
resultsEl.textContent = 'Start';
startBtnupdateScores();
}
// Event Listeners
.addEventListener('click', () => {
startBtnif (!isRunning) {
startGame();
};
})
.addEventListener('click', () => {
revealBtnif (!isRunning) return;
++;
correctCountupdateDisplay();
++;
currentIndex
if (currentIndex >= activeList.length) {
endGame();
};
})
.addEventListener('click', () => {
incorrectBtnif (!isRunning) return;
if (correctCount > 0) {
--;
correctCount++;
incorrectCountupdateScores();
};
})
.addEventListener('change', resetForNewList);
allMonarchsRadio.addEventListener('change', resetForNewList);
confessorRadio
document.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault();
.click();
incorrectBtnelse if (event.key === 'Enter') {
} event.preventDefault();
if (isRunning) {
.click();
revealBtnelse {
} .click();
startBtn
}else if (isRunning && (event.key.toLowerCase() === 'j' || event.key.toLowerCase() === 'l')) {
} .click();
incorrectBtnelse if (event.key.toLowerCase() === 'k') {
} if (isRunning) {
.click();
revealBtnelse {
} .click();
startBtn
}
};
});
})</script>
</body>
</html>