JavaScript for Recitation Testers

programming
llm
Explanation of the JavaScript behind recitation / memory tester websites for the Elements and Kings and Queens of England.
Author

Stephen J. Mildenhall

Published

2025-09-03

Modified

2025-09-03

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: a name and their dates. This is the master list.
  • allMonarchs.findIndex(...) searches through the allMonarchs array to find the exact position (the index number) of “Edward the Confessor”.
  • allMonarchs.slice(confessorIndex) then creates a new array called confessorMonarchs 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 or confessorMonarchs).
  • currentIndex: Tracks which monarch in the activeList you are on. Starts at 0 (the first one).
  • correctCount & incorrectCount: Track your score.
  • isRunning: A true/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.

startBtn.addEventListener('click', () => { /* ... */ });
revealBtn.addEventListener('click', () => { /* ... */ });
incorrectBtn.addEventListener('click', () => { /* ... */ });
allMonarchsRadio.addEventListener('change', resetForNewList);
document.addEventListener('keydown', (event) => { /* ... */ });

Explanation

  • .addEventListener('click', ...) tells an element (like startBtn) 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() {
    isRunning = true; // The test is now active!
    // ... Resets counts and displays ...
    startTime = Date.now(); // Records the start time
    timerInterval = setInterval(updateTimer, 1000); // Calls updateTimer every second
}

When you click “Reveal”: The revealBtn’s click listener fires.

revealBtn.addEventListener('click', () => {
    if (!isRunning) return; // Do nothing if game hasn't started
    correctCount++;      // Assume correct, add 1 to score
    updateDisplay();     // Show the current monarch's details
    currentIndex++;      // Move to the next monarch in the list
    if (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];
    elementTextEl.innerHTML = `...`; // Update HTML with name and dates
    progressEl.textContent = `${currentIndex + 1} / ${activeList.length}`;
    updateScores();
}

When you click “Incorrect”: The incorrectBtn’s click listener fires.

incorrectBtn.addEventListener('click', () => {
    if (!isRunning) return;
    if (correctCount > 0) {
        correctCount--;     // Subtract 1 from 'correct'
        incorrectCount++;   // Add 1 to 'incorrect'
        updateScores();     // Update the scoreboard display
    }
});

When the list is finished: The endGame() function is called. It displays your overall score and correct percentage.

function endGame() {
    isRunning = false;
    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);
    resultsTextEl.innerHTML = `Test Completed at ...`;
    resultsEl.classList.remove('hidden');
}

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;
        }
        input[type="radio"]:checked + .radio-label {
            background-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');
                timerEl.textContent = `${minutes}:${seconds}`;
            }

            function updateScores() {
                correctCountEl.textContent = correctCount;
                incorrectCountEl.textContent = incorrectCount;
            }

            function updateDisplay() {
                const monarch = activeList[currentIndex];
                elementTextEl.innerHTML = `${monarch.name}<br><span class="text-2xl md:text-3xl font-medium text-gray-500">${monarch.dates}</span>`;
                progressEl.textContent = `${currentIndex + 1} / ${activeList.length}`;
                updateScores();
            }

            function startGame() {
                isRunning = true;
                currentIndex = 0;
                correctCount = 0;
                incorrectCount = 0;
                elementTextEl.innerHTML = '';
                resultsEl.classList.add('hidden');
                startBtn.classList.add('hidden');
                instructions.classList.add('hidden');
                toggleContainer.classList.add('hidden');
                gameButtons.classList.remove('hidden');
                scoreboard.classList.remove('hidden');
                timerEl.classList.remove('hidden');

                updateScores();
                progressEl.textContent = `0 / ${activeList.length}`;

                startTime = Date.now();
                timerInterval = setInterval(updateTimer, 1000);
            }

            function endGame() {
                isRunning = false;
                clearInterval(timerInterval);
                gameButtons.classList.add('hidden');
                startBtn.classList.remove('hidden');
                startBtn.textContent = 'Restart';
                instructions.classList.remove('hidden');
                toggleContainer.classList.remove('hidden');

                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;

                resultsTextEl.innerHTML = `<strong>Test Completed at ${formattedDateTime}</strong><br>
                    ${correctCount} correct and ${incorrectCount} incorrect (${percentage}% correct).`;
                resultsEl.classList.remove('hidden');
            }

            function resetForNewList() {
                activeList = allMonarchsRadio.checked ? allMonarchs : confessorMonarchs;
                currentIndex = 0;
                correctCount = 0;
                incorrectCount = 0;
                elementTextEl.innerHTML = '';
                resultsEl.classList.add('hidden');
                startBtn.textContent = 'Start';
                updateScores();
            }

            // Event Listeners
            startBtn.addEventListener('click', () => {
                if (!isRunning) {
                    startGame();
                }
            });

            revealBtn.addEventListener('click', () => {
                if (!isRunning) return;

                correctCount++;
                updateDisplay();
                currentIndex++;

                if (currentIndex >= activeList.length) {
                    endGame();
                }
            });

            incorrectBtn.addEventListener('click', () => {
                if (!isRunning) return;
                if (correctCount > 0) {
                    correctCount--;
                    incorrectCount++;
                    updateScores();
                }
            });

            allMonarchsRadio.addEventListener('change', resetForNewList);
            confessorRadio.addEventListener('change', resetForNewList);

            document.addEventListener('keydown', (event) => {
                if (event.key === 'Enter' && event.shiftKey) {
                    event.preventDefault();
                    incorrectBtn.click();
                } else if (event.key === 'Enter') {
                    event.preventDefault();
                    if (isRunning) {
                        revealBtn.click();
                    } else {
                        startBtn.click();
                    }
                } else if (isRunning && (event.key.toLowerCase() === 'j' || event.key.toLowerCase() === 'l')) {
                    incorrectBtn.click();
                } else if (event.key.toLowerCase() === 'k') {
                     if (isRunning) {
                        revealBtn.click();
                    } else {
                        startBtn.click();
                    }
                }
            });
        });
    </script>
</body>
</html>