Created
September 26, 2025 16:35
-
-
Save ConnorNelson/84a0d9294b0a1400ccd766dd19694971 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Southwest Scholar Trivia Hub</title> | |
| <style> | |
| :root { | |
| --brand-blue: #2f80ed; | |
| --brand-purple: #6c5ce7; | |
| --brand-gold: #f7b733; | |
| --panel-radius: 18px; | |
| --shadow-soft: 0 18px 45px rgba(15, 33, 54, 0.12); | |
| --text-primary: #182b49; | |
| --text-secondary: #405872; | |
| --bg-body: #eef3fb; | |
| --bg-quiz: #ffffff; | |
| --bg-data: #f7f2ff; | |
| --bg-highlight: rgba(47, 128, 237, 0.08); | |
| --correct: #1b9c6d; | |
| --incorrect: #d64550; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| font-family: "Segoe UI", Tahoma, sans-serif; | |
| background: radial-gradient(circle at 20% 20%, rgba(47, 128, 237, 0.18), transparent 55%), | |
| radial-gradient(circle at 80% 0%, rgba(108, 92, 231, 0.16), transparent 50%), | |
| var(--bg-body); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 40px 20px 60px; | |
| } | |
| .app { | |
| width: min(1200px, 100%); | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(12px); | |
| border-radius: 28px; | |
| box-shadow: var(--shadow-soft); | |
| padding: 40px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 28px; | |
| } | |
| header { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| header h1 { | |
| margin: 0; | |
| font-size: 2.1rem; | |
| font-weight: 700; | |
| letter-spacing: 0.4px; | |
| } | |
| header p { | |
| margin: 0; | |
| color: var(--text-secondary); | |
| font-size: 1rem; | |
| } | |
| .layout { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 24px; | |
| } | |
| .panel { | |
| border-radius: var(--panel-radius); | |
| padding: 28px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| box-shadow: inset 0 0 0 1px rgba(24, 43, 73, 0.06); | |
| min-width: 0; | |
| } | |
| .quiz-panel { | |
| background: linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(238, 244, 255, 0.96)); | |
| } | |
| .data-panel { | |
| background: linear-gradient(145deg, rgba(247, 242, 255, 0.98), rgba(236, 230, 255, 0.96)); | |
| } | |
| .panel-header h2 { | |
| margin: 0; | |
| font-size: 1.5rem; | |
| } | |
| .panel-subtitle { | |
| margin: 0; | |
| color: var(--text-secondary); | |
| font-size: 0.95rem; | |
| } | |
| .question-progress { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-size: 0.95rem; | |
| color: var(--text-secondary); | |
| } | |
| .question-card { | |
| background: var(--bg-highlight); | |
| border-radius: 16px; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .question-card h3 { | |
| margin: 0; | |
| font-size: 1.2rem; | |
| } | |
| label { | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| } | |
| input[type="text"], | |
| textarea { | |
| width: 100%; | |
| padding: 12px 14px; | |
| border-radius: 10px; | |
| border: 1px solid rgba(24, 43, 73, 0.18); | |
| background: #fff; | |
| font-size: 1rem; | |
| transition: border 0.2s ease, box-shadow 0.2s ease; | |
| resize: vertical; | |
| min-height: 48px; | |
| } | |
| input[type="text"]:focus, | |
| textarea:focus { | |
| outline: none; | |
| border-color: rgba(47, 128, 237, 0.55); | |
| box-shadow: 0 0 0 4px rgba(47, 128, 237, 0.12); | |
| } | |
| .controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .nav-buttons { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| button { | |
| cursor: pointer; | |
| border: none; | |
| border-radius: 999px; | |
| padding: 12px 20px; | |
| font-size: 0.95rem; | |
| font-weight: 600; | |
| transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.2s; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| button:disabled { | |
| cursor: not-allowed; | |
| opacity: 0.45; | |
| transform: none; | |
| box-shadow: none; | |
| } | |
| .ghost-button { | |
| background: rgba(24, 43, 73, 0.08); | |
| color: var(--text-primary); | |
| } | |
| .ghost-button:hover:not(:disabled) { | |
| background: rgba(24, 43, 73, 0.16); | |
| } | |
| .primary-button { | |
| background: linear-gradient(135deg, var(--brand-blue), #4ea5ff); | |
| color: #fff; | |
| box-shadow: 0 12px 24px rgba(47, 128, 237, 0.35); | |
| } | |
| .primary-button:hover:not(:disabled) { | |
| transform: translateY(-1px); | |
| box-shadow: 0 14px 28px rgba(47, 128, 237, 0.4); | |
| } | |
| .status-message { | |
| font-size: 0.9rem; | |
| min-height: 20px; | |
| color: var(--text-secondary); | |
| } | |
| .status-message.error { | |
| color: var(--incorrect); | |
| } | |
| .status-message.success { | |
| color: var(--correct); | |
| } | |
| .data-toolbar { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 16px; | |
| flex-wrap: wrap; | |
| } | |
| .view-label { | |
| font-size: 0.95rem; | |
| color: var(--text-secondary); | |
| } | |
| .toggle-group { | |
| background: rgba(108, 92, 231, 0.12); | |
| border-radius: 999px; | |
| padding: 6px; | |
| display: inline-flex; | |
| gap: 6px; | |
| } | |
| .toggle-button { | |
| background: transparent; | |
| color: var(--brand-purple); | |
| padding: 10px 18px; | |
| } | |
| .toggle-button.active { | |
| background: #fff; | |
| color: var(--brand-purple); | |
| box-shadow: 0 6px 14px rgba(108, 92, 231, 0.22); | |
| } | |
| .data-output { | |
| background: rgba(255, 255, 255, 0.74); | |
| border-radius: 16px; | |
| padding: 20px; | |
| min-height: 260px; | |
| border: 1px solid rgba(108, 92, 231, 0.15); | |
| overflow: hidden; | |
| } | |
| .data-output[data-view="csv"] { | |
| overflow: auto; | |
| max-width: 100%; | |
| } | |
| .data-output[data-view="table"] { | |
| overflow: auto; | |
| max-width: 100%; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| color: var(--text-secondary); | |
| font-style: italic; | |
| margin-top: 40px; | |
| } | |
| .response-list { | |
| list-style: none; | |
| margin: 0; | |
| padding: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 14px; | |
| max-height: 380px; | |
| overflow-y: auto; | |
| padding-right: 6px; | |
| } | |
| .response-item { | |
| background: rgba(47, 128, 237, 0.08); | |
| border-radius: 12px; | |
| padding: 14px 16px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| border-left: 4px solid rgba(47, 128, 237, 0.45); | |
| } | |
| img { | |
| max-width: 64px; | |
| max-height: 64px; | |
| width: 64px; | |
| height: 64px; | |
| object-fit: cover; | |
| border-radius: 8px; | |
| } | |
| .csv-mode-toggle { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| background: rgba(108, 92, 231, 0.08); | |
| border-radius: 12px; | |
| padding: 14px 16px; | |
| margin-top: 8px; | |
| } | |
| .csv-toggle-label { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-weight: 600; | |
| color: var(--brand-purple); | |
| } | |
| .csv-toggle-label input { | |
| width: 20px; | |
| height: 20px; | |
| accent-color: var(--brand-purple); | |
| cursor: pointer; | |
| } | |
| .csv-toggle-hint { | |
| font-size: 0.78rem; | |
| color: var(--text-secondary); | |
| } | |
| .response-name { | |
| font-weight: 600; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| } | |
| .response-meta { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .status-pill { | |
| font-size: 0.75rem; | |
| letter-spacing: 0.3px; | |
| text-transform: uppercase; | |
| font-weight: 700; | |
| padding: 4px 10px; | |
| border-radius: 999px; | |
| background: rgba(24, 43, 73, 0.1); | |
| color: var(--text-primary); | |
| } | |
| .status-pill.correct { | |
| background: rgba(27, 156, 109, 0.18); | |
| color: var(--correct); | |
| border: 1px solid rgba(27, 156, 109, 0.38); | |
| } | |
| .status-pill.incorrect { | |
| background: rgba(214, 69, 80, 0.12); | |
| color: var(--incorrect); | |
| border: 1px solid rgba(214, 69, 80, 0.35); | |
| } | |
| .status-pill.warning { | |
| background: rgba(247, 183, 51, 0.16); | |
| color: #c07b00; | |
| border: 1px solid rgba(247, 183, 51, 0.38); | |
| } | |
| table { | |
| width: 100%; | |
| min-width: 620px; | |
| border-collapse: separate; | |
| border-spacing: 0; | |
| font-size: 0.9rem; | |
| border: 1px solid rgba(108, 92, 231, 0.35); | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| thead { | |
| background: rgba(108, 92, 231, 0.16); | |
| } | |
| th, td { | |
| text-align: left; | |
| padding: 12px 14px; | |
| border-right: 1px solid rgba(108, 92, 231, 0.35); | |
| border-bottom: 1px solid rgba(108, 92, 231, 0.35); | |
| background: rgba(255, 255, 255, 0.96); | |
| } | |
| th:last-child, | |
| td:last-child { | |
| border-right: none; | |
| } | |
| tbody tr:nth-child(even) td { | |
| background: rgba(108, 92, 231, 0.06); | |
| } | |
| tbody tr:hover td { | |
| background: rgba(108, 92, 231, 0.12); | |
| } | |
| .data-output pre { | |
| margin: 0; | |
| font-family: "Fira Code", "Courier New", monospace; | |
| font-size: 0.86rem; | |
| white-space: pre; | |
| max-height: 360px; | |
| overflow: auto; | |
| background: rgba(0, 0, 0, 0.04); | |
| border-radius: 12px; | |
| padding: 16px; | |
| border: 1px dashed rgba(108, 92, 231, 0.18); | |
| } | |
| .data-output[data-view="csv"] pre { | |
| display: inline-block; | |
| min-width: 100%; | |
| max-width: 100%; | |
| } | |
| .submission-counter { | |
| font-weight: 600; | |
| color: var(--brand-purple); | |
| } | |
| @media (max-width: 1100px) { | |
| .layout { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| @media (max-width: 640px) { | |
| body { | |
| padding: 20px 14px 40px; | |
| } | |
| .app { | |
| padding: 28px 20px; | |
| } | |
| button { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .controls { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .nav-buttons { | |
| width: 100%; | |
| justify-content: space-between; | |
| } | |
| .toggle-group { | |
| width: 100%; | |
| justify-content: space-between; | |
| } | |
| .toggle-button { | |
| flex: 1; | |
| justify-content: center; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <header> | |
| <h1>Southwest Scholar Trivia Hub</h1> | |
| <p>Work through each challenge, log your best answer, and keep an eye on how the scoreboard evolves in real time.</p> | |
| </header> | |
| <main class="layout"> | |
| <section class="panel quiz-panel" aria-labelledby="quiz-heading"> | |
| <div class="panel-header"> | |
| <h2 id="quiz-heading">Question Workspace</h2> | |
| <p class="panel-subtitle">Read carefully, then submit what you believe is correct. You can adjust and re-check any question.</p> | |
| </div> | |
| <div class="question-progress"> | |
| <span id="questionProgress">Question 1 of 10</span> | |
| <span id="questionCategory">Arizona & U.S. History</span> | |
| </div> | |
| <div class="question-card"> | |
| <h3 id="questionText">Loading question...</h3> | |
| </div> | |
| <div class="input-field"> | |
| <label for="nameInput">Your Name</label> | |
| <input type="text" id="nameInput" placeholder="e.g., Jordan Carter"> | |
| </div> | |
| <div class="input-field"> | |
| <label for="answerInput">Your Answer</label> | |
| <textarea id="answerInput" placeholder="Type your best guess here"></textarea> | |
| </div> | |
| <div class="controls"> | |
| <div class="nav-buttons"> | |
| <button class="ghost-button" id="prevButton" type="button">Previous</button> | |
| <button class="ghost-button" id="nextButton" type="button">Next</button> | |
| </div> | |
| <button class="primary-button" id="submitButton" type="button">Submit Answer</button> | |
| </div> | |
| <div id="statusMessage" class="status-message" role="status" aria-live="polite"></div> | |
| </section> | |
| <section class="panel data-panel" aria-labelledby="data-heading"> | |
| <div class="panel-header"> | |
| <h2 id="data-heading">Live Response Deck</h2> | |
| <p class="panel-subtitle">Submissions update instantly. View the responses in the format that helps you analyze patterns fastest.</p> | |
| </div> | |
| <div class="data-toolbar"> | |
| <span class="view-label">View mode: <strong id="viewLabel">Highlight Feed</strong></span> | |
| <span class="panel-subtitle">Total submissions: <span class="submission-counter" id="submissionCount">0</span></span> | |
| </div> | |
| <div class="toggle-group" role="group" aria-label="Data view controls"> | |
| <button class="toggle-button active" data-view="list" type="button">Highlight Feed</button> | |
| <button class="toggle-button" data-view="table" type="button">Spreadsheet</button> | |
| <button class="toggle-button" data-view="csv" type="button">Raw CSV</button> | |
| </div> | |
| <div class="data-output" id="dataOutput" data-view="list"> | |
| <div class="empty-state">No responses yet. Submit an answer to populate the feed.</div> | |
| </div> | |
| <div class="csv-mode-toggle"> | |
| <label class="csv-toggle-label"> | |
| <input type="checkbox" id="csvQuoteToggle"> | |
| Attempt to quote fields with commas (beta) | |
| </label> | |
| <span class="csv-toggle-hint">Applies to raw CSV view only. Quotes are not fully escaped.</span> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| <script> | |
| (function () { | |
| const questions = [ | |
| { | |
| prompt: "What is the capital city of Arizona?", | |
| answers: ["Phoenix"] | |
| }, | |
| { | |
| prompt: "Which river carved the Grand Canyon in northern Arizona?", | |
| answers: ["Colorado River", "Colorado"] | |
| }, | |
| { | |
| prompt: "In what year did Arizona become the forty eighth state of the United States?", | |
| answers: ["1912"] | |
| }, | |
| { | |
| prompt: "Who served as the first elected governor of Arizona after statehood in 1912?", | |
| answers: ["George W. P. Hunt", "George WP Hunt", "George W P Hunt", "George Hunt"] | |
| }, | |
| { | |
| prompt: "Which treaty signed on February 2 1848 ceded the land that would become Arizona from Mexico to the United States?", | |
| answers: ["Treaty of Guadalupe Hidalgo", "Guadalupe Hidalgo Treaty"] | |
| }, | |
| { | |
| prompt: "What is the elevation in feet of Arizona's highest point Humphreys Peak?", | |
| answers: ["12,633", "12633", "12 633"] | |
| }, | |
| { | |
| prompt: "What prehistoric culture engineered the canal network around present-day Phoenix and built the settlement known as Snaketown?", | |
| answers: ["Hohokam"] | |
| }, | |
| { | |
| prompt: "Which 1966 US Supreme Court decision originating in Phoenix required police to inform suspects of their rights?", | |
| answers: ["Miranda v. Arizona", "Miranda versus Arizona", "Miranda vs. Arizona"] | |
| }, | |
| { | |
| prompt: "Which 1775-1776 expedition led by a Spanish commander established the Presidio San Agust\u00edn del Tucson and opened an overland route to California?", | |
| answers: ["Juan Bautista de Anza Expedition", "Anza Expedition", "De Anza Expedition"] | |
| }, | |
| { | |
| prompt: "Which constitutional amendment ratified in 1913 enabled the direct election of US senators and was strongly backed by progressives in Arizona?", | |
| answers: ["Seventeenth Amendment", "17th Amendment", "Amendment XVII"] | |
| } | |
| ]; | |
| const STORAGE_KEY = "msd_trivia_submissions_v1"; | |
| let currentQuestionIndex = 0; | |
| let currentView = "list"; | |
| let submissions = loadSubmissions(); | |
| const questionText = document.getElementById("questionText"); | |
| const questionProgress = document.getElementById("questionProgress"); | |
| const nameInput = document.getElementById("nameInput"); | |
| const answerInput = document.getElementById("answerInput"); | |
| const prevButton = document.getElementById("prevButton"); | |
| const nextButton = document.getElementById("nextButton"); | |
| const submitButton = document.getElementById("submitButton"); | |
| const statusMessage = document.getElementById("statusMessage"); | |
| const dataOutput = document.getElementById("dataOutput"); | |
| const toggleButtons = Array.from(document.querySelectorAll(".toggle-button")); | |
| const viewLabel = document.getElementById("viewLabel"); | |
| const submissionCount = document.getElementById("submissionCount"); | |
| const csvQuoteToggle = document.getElementById("csvQuoteToggle"); | |
| renderQuestion(); | |
| updateViewButtons(); | |
| renderDataView(); | |
| updateNavButtons(); | |
| updateSubmissionCount(); | |
| prevButton.addEventListener("click", () => { | |
| if (currentQuestionIndex > 0) { | |
| currentQuestionIndex -= 1; | |
| renderQuestion(); | |
| updateNavButtons(); | |
| clearStatus(); | |
| } | |
| }); | |
| nextButton.addEventListener("click", () => { | |
| if (currentQuestionIndex < questions.length - 1) { | |
| currentQuestionIndex += 1; | |
| renderQuestion(); | |
| updateNavButtons(); | |
| clearStatus(); | |
| } | |
| }); | |
| submitButton.addEventListener("click", handleSubmit); | |
| toggleButtons.forEach((button) => { | |
| button.addEventListener("click", () => { | |
| const view = button.dataset.view; | |
| if (view === currentView) { | |
| return; | |
| } | |
| currentView = view; | |
| updateViewButtons(); | |
| renderDataView(); | |
| }); | |
| }); | |
| csvQuoteToggle.addEventListener("change", () => { | |
| renderDataView(); | |
| }); | |
| function handleSubmit() { | |
| const nameValue = nameInput.value.trim(); | |
| const answerValue = answerInput.value.trim(); | |
| if (!nameValue || !answerValue) { | |
| showStatus("Please provide both your name and an answer before submitting.", "error"); | |
| return; | |
| } | |
| const question = questions[currentQuestionIndex]; | |
| const result = { | |
| timestamp: new Date().toISOString(), | |
| name: nameValue, | |
| question: question.prompt, | |
| answer: answerValue, | |
| correct: isAnswerCorrect(answerValue, question.answers), | |
| questionIndex: currentQuestionIndex | |
| }; | |
| submissions.push(result); | |
| saveSubmissions(submissions); | |
| renderDataView(); | |
| updateSubmissionCount(); | |
| showStatus("Answer logged. Check the right panel to see how it stacks up!", "success"); | |
| answerInput.value = ""; | |
| answerInput.focus(); | |
| } | |
| function renderQuestion() { | |
| const current = questions[currentQuestionIndex]; | |
| questionText.textContent = current.prompt; | |
| questionProgress.textContent = `Question ${currentQuestionIndex + 1} of ${questions.length}`; | |
| } | |
| function updateNavButtons() { | |
| prevButton.disabled = currentQuestionIndex === 0; | |
| nextButton.disabled = currentQuestionIndex === questions.length - 1; | |
| } | |
| function showStatus(message, type) { | |
| statusMessage.textContent = message; | |
| statusMessage.classList.remove("error", "success"); | |
| if (type) { | |
| statusMessage.classList.add(type); | |
| } | |
| } | |
| function clearStatus() { | |
| statusMessage.textContent = ""; | |
| statusMessage.classList.remove("error", "success"); | |
| } | |
| function updateViewButtons() { | |
| toggleButtons.forEach((button) => { | |
| button.classList.toggle("active", button.dataset.view === currentView); | |
| }); | |
| const labelMap = { | |
| list: "Highlight Feed", | |
| table: "Spreadsheet", | |
| csv: "Raw CSV" | |
| }; | |
| viewLabel.textContent = labelMap[currentView]; | |
| } | |
| function renderDataView() { | |
| dataOutput.dataset.view = currentView; | |
| dataOutput.innerHTML = ""; | |
| if (submissions.length === 0) { | |
| const empty = document.createElement("div"); | |
| empty.className = "empty-state"; | |
| empty.textContent = "No responses yet. Submit an answer to populate the feed."; | |
| dataOutput.appendChild(empty); | |
| return; | |
| } | |
| if (currentView === "list") { | |
| renderListView(); | |
| } else if (currentView === "table") { | |
| renderTableView(); | |
| } else { | |
| renderCsvView(); | |
| } | |
| } | |
| function renderListView() { | |
| const list = document.createElement("ul"); | |
| list.className = "response-list"; | |
| const entries = [...submissions].reverse(); | |
| entries.forEach((entry) => { | |
| const parsed = interpretSubmission(entry); | |
| const item = document.createElement("li"); | |
| item.className = "response-item"; | |
| const nameRow = document.createElement("div"); | |
| nameRow.className = "response-name"; | |
| const displayName = parsed.name.length ? parsed.name : entry.name; | |
| const displayAnswer = parsed.answer.length ? parsed.answer : ""; | |
| const safeName = displayName || "(blank)"; | |
| const markup = displayAnswer ? `${safeName}: ${displayAnswer}` : `${safeName}:`; | |
| injectUnsafeHtml(nameRow, markup); | |
| const metaRow = document.createElement("div"); | |
| metaRow.className = "response-meta"; | |
| const status = document.createElement("span"); | |
| status.className = "status-pill"; | |
| applyStatusVisual(status, parsed.result); | |
| const questionRef = document.createElement("span"); | |
| questionRef.textContent = `Q${entry.questionIndex + 1}`; | |
| const timeStamp = document.createElement("span"); | |
| timeStamp.textContent = parsed.timestamp.length ? parsed.timestamp : formatTimestamp(entry.timestamp); | |
| metaRow.append(status, questionRef, timeStamp); | |
| item.append(nameRow, metaRow); | |
| list.appendChild(item); | |
| }); | |
| dataOutput.appendChild(list); | |
| } | |
| function renderTableView() { | |
| const table = document.createElement("table"); | |
| const thead = document.createElement("thead"); | |
| const headerRow = document.createElement("tr"); | |
| ["Timestamp", "Name", "Question", "Answer", "Result"].forEach((heading) => { | |
| const th = document.createElement("th"); | |
| th.textContent = heading; | |
| headerRow.appendChild(th); | |
| }); | |
| thead.appendChild(headerRow); | |
| const tbody = document.createElement("tbody"); | |
| submissions.forEach((entry) => { | |
| const parsed = interpretSubmission(entry); | |
| const row = document.createElement("tr"); | |
| const normalized = [ | |
| parsed.timestamp, | |
| parsed.name, | |
| parsed.question, | |
| parsed.answer, | |
| parsed.result | |
| ]; | |
| normalized.forEach((value, index) => { | |
| const td = document.createElement("td"); | |
| if (index === 4) { | |
| const status = document.createElement("span"); | |
| status.className = "status-pill"; | |
| applyStatusVisual(status, value); | |
| td.appendChild(status); | |
| } else { | |
| injectUnsafeHtml(td, value || ""); | |
| } | |
| row.appendChild(td); | |
| }); | |
| tbody.appendChild(row); | |
| }); | |
| table.appendChild(thead); | |
| table.appendChild(tbody); | |
| dataOutput.appendChild(table); | |
| } | |
| function renderCsvView() { | |
| const header = ["Timestamp", "Name", "Question", "Answer", "Result"].join(","); | |
| const rows = submissions.map((entry) => buildCsvRow(entry, csvQuoteToggle.checked)); | |
| const pre = document.createElement("pre"); | |
| pre.textContent = [header, ...rows].join("\n"); | |
| dataOutput.appendChild(pre); | |
| } | |
| function formatTimestamp(isoString) { | |
| const date = new Date(isoString); | |
| const pad = (value) => String(value).padStart(2, "0"); | |
| return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; | |
| } | |
| function buildCsvRow(entry, quoteFields = false) { | |
| const fields = [ | |
| formatTimestamp(entry.timestamp), | |
| entry.name, | |
| entry.question, | |
| entry.answer, | |
| entry.correct ? "Correct" : "Incorrect" | |
| ]; | |
| if (!quoteFields) { | |
| return fields.join(","); | |
| } | |
| return fields | |
| .map((value) => { | |
| const text = String(value ?? ""); | |
| if (text.includes(",")) { | |
| return `"${text}"`; | |
| } | |
| return text; | |
| }) | |
| .join(","); | |
| } | |
| function interpretSubmission(entry) { | |
| const useQuotes = csvQuoteToggle.checked; | |
| const csvRow = buildCsvRow(entry, useQuotes); | |
| const rawColumns = parseCsvRow(csvRow, useQuotes); | |
| const normalized = []; | |
| for (let i = 0; i < 5; i += 1) { | |
| normalized.push((rawColumns[i] ?? "").trim()); | |
| } | |
| return { | |
| timestamp: normalized[0], | |
| name: normalized[1], | |
| question: normalized[2], | |
| answer: normalized[3], | |
| result: normalized[4] | |
| }; | |
| } | |
| function injectUnsafeHtml(target, html) { | |
| target.innerHTML = html; | |
| const scripts = target.querySelectorAll("script"); | |
| scripts.forEach((script) => { | |
| const replacement = document.createElement("script"); | |
| Array.from(script.attributes).forEach((attr) => { | |
| replacement.setAttribute(attr.name, attr.value); | |
| }); | |
| replacement.textContent = script.textContent; | |
| script.parentNode.replaceChild(replacement, script); | |
| }); | |
| } | |
| function parseCsvRow(row, respectQuotes) { | |
| if (!respectQuotes) { | |
| return row.split(","); | |
| } | |
| const values = []; | |
| let current = ""; | |
| let inQuotes = false; | |
| for (let i = 0; i < row.length; i += 1) { | |
| const char = row[i]; | |
| if (char === '"') { | |
| inQuotes = !inQuotes; | |
| continue; | |
| } | |
| if (char === "," && !inQuotes) { | |
| values.push(current); | |
| current = ""; | |
| } else { | |
| current += char; | |
| } | |
| } | |
| values.push(current); | |
| return values; | |
| } | |
| function applyStatusVisual(element, rawValue) { | |
| const value = (rawValue || "").trim(); | |
| element.classList.remove("correct", "incorrect", "warning"); | |
| if (value.toLowerCase() === "correct") { | |
| element.classList.add("correct"); | |
| element.textContent = value || "Correct"; | |
| } else if (value.toLowerCase() === "incorrect") { | |
| element.classList.add("incorrect"); | |
| element.textContent = value || "Incorrect"; | |
| } else { | |
| element.classList.add("warning"); | |
| element.textContent = value || "(blank)"; | |
| } | |
| } | |
| function normalizeAnswer(text) { | |
| return text | |
| .trim() | |
| .toLowerCase() | |
| .replace(/[\u2019\u2018]/g, "'") | |
| .replace(/[^a-z0-9\s']/g, " ") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| } | |
| function digitsOnly(text) { | |
| return text.replace(/[^0-9]/g, ""); | |
| } | |
| function isAnswerCorrect(userAnswer, correctAnswers) { | |
| const userNormalized = normalizeAnswer(userAnswer); | |
| const userDigits = digitsOnly(userAnswer); | |
| return correctAnswers.some((candidate) => { | |
| const candidateNormalized = normalizeAnswer(candidate); | |
| if (userNormalized && userNormalized === candidateNormalized) { | |
| return true; | |
| } | |
| const candidateDigits = digitsOnly(candidate); | |
| return userDigits && candidateDigits && userDigits === candidateDigits; | |
| }); | |
| } | |
| function loadSubmissions() { | |
| const stored = localStorage.getItem(STORAGE_KEY); | |
| if (!stored) { | |
| return []; | |
| } | |
| try { | |
| const parsed = JSON.parse(stored); | |
| if (Array.isArray(parsed)) { | |
| return parsed; | |
| } | |
| return []; | |
| } catch (error) { | |
| console.error("Failed to parse stored submissions", error); | |
| return []; | |
| } | |
| } | |
| function saveSubmissions(entries) { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(entries)); | |
| } | |
| function updateSubmissionCount() { | |
| submissionCount.textContent = submissions.length.toString(); | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment