Created
September 15, 2025 15:05
-
-
Save manhbi18112005/30d8213e4dd582cccdfdaff02622a808 to your computer and use it in GitHub Desktop.
Grade Calculator for Examiner
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>Modern Grade Calculator</title> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| /* Custom styles to hide the number input arrows */ | |
| input::-webkit-outer-spin-button, | |
| input::-webkit-inner-spin-button { | |
| -webkit-appearance: none; | |
| margin: 0; | |
| } | |
| input[type=number] { | |
| -moz-appearance: textfield; | |
| } | |
| /* Custom style for active tab */ | |
| .tab-active { | |
| @apply bg-indigo-100 text-indigo-700; | |
| } | |
| /* Dark mode variables */ | |
| :root { | |
| --bg-primary: #f1f5f9; | |
| --bg-secondary: #ffffff; | |
| --text-primary: #1e293b; | |
| --text-secondary: #64748b; | |
| --border-color: #e2e8f0; | |
| } | |
| [data-theme="dark"] { | |
| --bg-primary: #0f172a; | |
| --bg-secondary: #1e293b; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| --border-color: #334155; | |
| } | |
| .theme-transition { | |
| transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; | |
| } | |
| /* Tooltip styles */ | |
| .tooltip { | |
| position: relative; | |
| } | |
| .tooltip:hover::after { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #1e293b; | |
| color: white; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| z-index: 1000; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-100 flex items-center justify-center min-h-screen p-4 md:p-8 theme-transition" | |
| style="background-color: var(--bg-primary);"> | |
| <!-- Main Layout Container --> | |
| <div class="w-full max-w-6xl mx-auto flex flex-col md:flex-row gap-8 md:items-start"> | |
| <!-- Sidebar with Tabs --> | |
| <aside class="w-full md:w-80 lg:w-96 bg-white rounded-2xl shadow-lg flex flex-col h-[85vh] theme-transition" | |
| style="background-color: var(--bg-secondary);"> | |
| <div class="flex border-b border-slate-200 p-6 pb-4 flex-shrink-0" | |
| style="border-color: var(--border-color);"> | |
| <button data-tab-target="#history-panel" | |
| class="tab-btn flex-1 py-2 text-center font-semibold text-slate-500 hover:bg-slate-100 rounded-t-lg transition-colors focus:outline-none">History</button> | |
| <button data-tab-target="#settings-panel" | |
| class="tab-btn flex-1 py-2 text-center font-semibold text-slate-500 hover:bg-slate-100 rounded-t-lg transition-colors focus:outline-none">Settings</button> | |
| <button data-tab-target="#goal-panel" | |
| class="tab-btn flex-1 py-2 text-center font-semibold text-slate-500 hover:bg-slate-100 rounded-t-lg transition-colors focus:outline-none">Goal</button> | |
| </div> | |
| <div class="flex-grow min-h-0 px-6 pb-6"> | |
| <!-- History Panel --> | |
| <div id="history-panel" class="tab-panel h-full flex flex-col"> | |
| <div class="flex items-center justify-between my-4 flex-shrink-0"> | |
| <h2 class="text-xl font-bold text-slate-700" style="color: var(--text-primary);">Grade History | |
| </h2> | |
| <div class="flex gap-2"> | |
| <button id="export-history" | |
| class="tooltip text-sm text-indigo-500 hover:text-indigo-700 font-semibold focus:outline-none" | |
| data-tooltip="Export history (Ctrl+E)">Export</button> | |
| <button id="import-history" | |
| class="tooltip text-sm text-indigo-500 hover:text-indigo-700 font-semibold focus:outline-none" | |
| data-tooltip="Import history">Import</button> | |
| <button id="clear-history" | |
| class="tooltip text-sm text-indigo-500 hover:text-indigo-700 font-semibold focus:outline-none" | |
| data-tooltip="Clear all (Ctrl+X)">Clear</button> | |
| </div> | |
| </div> | |
| <ul id="history-list" class="space-y-3 flex-grow overflow-y-auto pr-2"> | |
| </ul> | |
| <input type="file" id="import-file" accept=".json" class="hidden"> | |
| </div> | |
| <!-- Settings Panel --> | |
| <div id="settings-panel" class="tab-panel h-full hidden overflow-y-auto pr-2"> | |
| <h2 class="text-xl font-bold text-slate-700 mb-4" style="color: var(--text-primary);">Settings</h2> | |
| <div class="space-y-6"> | |
| <div> | |
| <label for="total-questions-setting" class="block text-sm font-medium text-slate-600 mb-1" | |
| style="color: var(--text-secondary);">Total Questions</label> | |
| <input type="number" id="total-questions-setting" min="1" | |
| class="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 theme-transition" | |
| style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color);"> | |
| </div> | |
| <div> | |
| <label for="grade-view-setting" class="block text-sm font-medium text-slate-600 mb-1" | |
| style="color: var(--text-secondary);">Grade Display</label> | |
| <select id="grade-view-setting" | |
| class="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 theme-transition" | |
| style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color);"> | |
| <option value="percentage">Percentage (100%)</option> | |
| <option value="points">Points (out of 10)</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="decimal-places-setting" class="block text-sm font-medium text-slate-600 mb-1" | |
| style="color: var(--text-secondary);">Decimal Places</label> | |
| <input type="number" id="decimal-places-setting" min="0" max="5" | |
| class="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 theme-transition" | |
| style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color);"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-600 mb-1" | |
| style="color: var(--text-secondary);">Letter Grades</label> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="show-letter-grades" class="mr-2"> | |
| <label for="show-letter-grades" class="text-sm" | |
| style="color: var(--text-secondary);">Show letter grades</label> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-600 mb-1" | |
| style="color: var(--text-secondary);">Theme</label> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="dark-mode-toggle" class="mr-2"> | |
| <label for="dark-mode-toggle" class="text-sm" style="color: var(--text-secondary);">Dark | |
| mode</label> | |
| </div> | |
| </div> | |
| <div class="text-xs text-slate-400 p-3 bg-slate-50 rounded-lg theme-transition" | |
| style="background-color: var(--bg-primary); color: var(--text-secondary);"> | |
| <strong>Keyboard Shortcuts:</strong><br> | |
| Enter: Add to history<br> | |
| Ctrl+E: Export history<br> | |
| Ctrl+X: Clear history<br> | |
| Ctrl+D: Toggle dark mode | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Grade Goal Panel --> | |
| <div id="goal-panel" class="tab-panel h-full hidden overflow-y-auto pr-2"> | |
| <h2 class="text-xl font-bold text-slate-700 mb-4" style="color: var(--text-primary);">Grade Goal | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="target-grade" class="block text-sm font-medium text-slate-600 mb-1" | |
| style="color: var(--text-secondary);">Target Grade (%)</label> | |
| <input type="number" id="target-grade" min="0" max="100" placeholder="e.g., 85" | |
| class="w-full p-2 border border-slate-300 rounded-md focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 theme-transition" | |
| style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color);"> | |
| </div> | |
| <div id="goal-result" class="mt-4 p-4 bg-slate-50 rounded-lg theme-transition" | |
| style="background-color: var(--bg-primary);"> | |
| Enter a target grade to see how many questions you can get wrong. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Calculator Section --> | |
| <main | |
| class="w-full md:flex-1 bg-white p-8 rounded-2xl shadow-lg text-center h-[85vh] flex flex-col justify-center theme-transition" | |
| style="background-color: var(--bg-secondary);"> | |
| <h1 class="text-3xl md:text-4xl font-bold text-slate-800" style="color: var(--text-primary);">Grade | |
| Calculator</h1> | |
| <p id="total-questions-info" class="text-slate-500 mt-2 mb-8" style="color: var(--text-secondary);">The test | |
| has a total of 30 questions.</p> | |
| <label for="wrong-questions" class="text-slate-600 font-medium sr-only">Enter the number of WRONG | |
| questions:</label> | |
| <input type="number" id="wrong-questions" placeholder="e.g., 5" min="0" max="30" | |
| class="w-full p-4 border border-slate-300 rounded-lg text-4xl text-center font-mono focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-shadow duration-200 theme-transition" | |
| style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-color);"> | |
| <div id="result" class="mt-6 font-bold min-h-[90px] text-slate-800 transition-opacity duration-300" | |
| style="color: var(--text-primary);"> | |
| </div> | |
| <h2 class="text-xl font-bold text-slate-700 mb-4" style="color: var(--text-primary);">Statistics | |
| </h2> | |
| <div id="stats-content" class="space-y-4"> | |
| <div class="text-center text-slate-400" style="color: var(--text-secondary);">No data available | |
| yet.</div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| const elements = { | |
| wrongInput: document.getElementById('wrong-questions'), | |
| resultDiv: document.getElementById('result'), | |
| historyList: document.getElementById('history-list'), | |
| clearHistoryBtn: document.getElementById('clear-history'), | |
| exportHistoryBtn: document.getElementById('export-history'), | |
| importHistoryBtn: document.getElementById('import-history'), | |
| importFile: document.getElementById('import-file'), | |
| totalQuestionsInfo: document.getElementById('total-questions-info'), | |
| totalQuestionsSettingInput: document.getElementById('total-questions-setting'), | |
| gradeViewSettingSelect: document.getElementById('grade-view-setting'), | |
| decimalPlacesSettingInput: document.getElementById('decimal-places-setting'), | |
| showLetterGradesCheckbox: document.getElementById('show-letter-grades'), | |
| darkModeToggle: document.getElementById('dark-mode-toggle'), | |
| targetGradeInput: document.getElementById('target-grade'), | |
| goalResult: document.getElementById('goal-result'), | |
| statsContent: document.getElementById('stats-content'), | |
| tabBtns: document.querySelectorAll('.tab-btn'), | |
| tabPanels: document.querySelectorAll('.tab-panel') | |
| }; | |
| let gradeHistory = []; | |
| let settings = { | |
| totalQuestions: 30, | |
| gradeView: 'percentage', | |
| decimalPlaces: 2, | |
| showLetterGrades: false, | |
| darkMode: false | |
| }; | |
| const gradeScale = [ | |
| { min: 97, letter: 'A+' }, | |
| { min: 93, letter: 'A' }, | |
| { min: 90, letter: 'A-' }, | |
| { min: 87, letter: 'B+' }, | |
| { min: 83, letter: 'B' }, | |
| { min: 80, letter: 'B-' }, | |
| { min: 77, letter: 'C+' }, | |
| { min: 73, letter: 'C' }, | |
| { min: 70, letter: 'C-' }, | |
| { min: 67, letter: 'D+' }, | |
| { min: 63, letter: 'D' }, | |
| { min: 60, letter: 'D-' }, | |
| { min: 0, letter: 'F' } | |
| ]; | |
| const debounce = (func, delay) => { | |
| let timeoutId; | |
| return (...args) => { | |
| clearTimeout(timeoutId); | |
| timeoutId = setTimeout(() => func.apply(null, args), delay); | |
| }; | |
| }; | |
| const Storage = { | |
| save() { | |
| localStorage.setItem('gradeHistory', JSON.stringify(gradeHistory)); | |
| localStorage.setItem('gradeSettings', JSON.stringify(settings)); | |
| }, | |
| load() { | |
| const savedHistory = localStorage.getItem('gradeHistory'); | |
| const savedSettings = localStorage.getItem('gradeSettings'); | |
| if (savedHistory) gradeHistory = JSON.parse(savedHistory); | |
| if (savedSettings) Object.assign(settings, JSON.parse(savedSettings)); | |
| } | |
| }; | |
| function getLetterGrade(percentage) { | |
| return gradeScale.find(grade => percentage >= grade.min)?.letter || 'F'; | |
| } | |
| function calculateGrade() { | |
| const numWrongStr = elements.wrongInput.value; | |
| if (!numWrongStr) return null; | |
| const numWrong = parseInt(numWrongStr); | |
| if (isNaN(numWrong) || numWrong < 0 || numWrong > settings.totalQuestions) { | |
| return { error: true }; | |
| } | |
| const correctAnswers = settings.totalQuestions - numWrong; | |
| const percentage = (correctAnswers / settings.totalQuestions) * 100; | |
| const finalGrade = settings.gradeView === 'points' ? (percentage / 10) : percentage; | |
| return { | |
| grade: finalGrade.toFixed(settings.decimalPlaces), | |
| percentage: percentage, | |
| letterGrade: getLetterGrade(percentage), | |
| correct: correctAnswers, | |
| wrong: numWrong, | |
| total: settings.totalQuestions, | |
| error: false | |
| }; | |
| } | |
| function updateDisplay() { | |
| const data = calculateGrade(); | |
| if (!data) { | |
| elements.resultDiv.innerHTML = ''; | |
| return; | |
| } | |
| if (data.error) { | |
| elements.resultDiv.innerHTML = `<span class="text-lg text-red-500 font-medium">Please enter a number between 0 and ${settings.totalQuestions}.</span>`; | |
| return; | |
| } | |
| const gradeText = settings.gradeView === 'points' | |
| ? `<div class="text-5xl">${data.grade} <span class="text-3xl text-slate-400">/ 10</span></div>` | |
| : `<div class="text-5xl">${data.grade}%</div>`; | |
| const letterGradeText = settings.showLetterGrades | |
| ? `<div class="text-3xl font-bold text-indigo-600 mt-2">${data.letterGrade}</div>` | |
| : ''; | |
| elements.resultDiv.innerHTML = ` | |
| ${gradeText} | |
| ${letterGradeText} | |
| <div class="text-2xl font-normal text-slate-500 mt-2"> | |
| <span class="text-green-500"><strong>${data.correct}</strong> Correct</span> | |
| - | |
| <span class="text-red-500"><strong>${data.wrong}</strong> Wrong</span> | |
| </div> | |
| `; | |
| } | |
| function calculateGoal() { | |
| const targetGrade = parseFloat(elements.targetGradeInput.value); | |
| if (isNaN(targetGrade) || targetGrade < 0 || targetGrade > 100) { | |
| elements.goalResult.innerHTML = 'Please enter a valid target grade between 0 and 100.'; | |
| return; | |
| } | |
| const requiredCorrect = Math.ceil((targetGrade / 100) * settings.totalQuestions); | |
| const allowedWrong = settings.totalQuestions - requiredCorrect; | |
| elements.goalResult.innerHTML = ` | |
| <div class="text-lg font-semibold" style="color: var(--text-primary);"> | |
| To achieve ${targetGrade}%: | |
| </div> | |
| <div class="mt-2" style="color: var(--text-secondary);"> | |
| You need <strong class="text-green-600">${requiredCorrect}</strong> correct answers<br> | |
| You can get <strong class="text-red-600">${Math.max(0, allowedWrong)}</strong> questions wrong | |
| </div> | |
| `; | |
| } | |
| function updateStatistics() { | |
| if (gradeHistory.length === 0) { | |
| elements.statsContent.innerHTML = '<div class="text-center text-slate-400" style="color: var(--text-secondary);">No data available yet.</div>'; | |
| return; | |
| } | |
| const grades = gradeHistory.map(item => (item.correct / item.total)); | |
| const average = grades.reduce((a, b) => a + b, 0) / grades.length; | |
| const highest = Math.max(...grades); | |
| const lowest = Math.min(...grades); | |
| const avgLetterGrade = getLetterGrade(average); | |
| elements.statsContent.innerHTML = ` | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div class="text-center p-4 bg-slate-50 rounded-lg theme-transition" style="background-color: var(--bg-primary);"> | |
| <div class="text-2xl font-bold text-indigo-600">${(average * 10).toFixed(1)}</div> | |
| <div class="text-sm" style="color: var(--text-secondary);">Average (${avgLetterGrade})</div> | |
| </div> | |
| <div class="text-center p-4 bg-slate-50 rounded-lg theme-transition" style="background-color: var(--bg-primary);"> | |
| <div class="text-2xl font-bold text-green-600">${(highest * 10).toFixed(1)}</div> | |
| <div class="text-sm" style="color: var(--text-secondary);">Highest</div> | |
| </div> | |
| <div class="text-center p-4 bg-slate-50 rounded-lg theme-transition" style="background-color: var(--bg-primary);"> | |
| <div class="text-2xl font-bold text-red-600">${(lowest * 10).toFixed(1)}</div> | |
| <div class="text-sm" style="color: var(--text-secondary);">Lowest</div> | |
| </div> | |
| <div class="text-center p-4 bg-slate-50 rounded-lg theme-transition" style="background-color: var(--bg-primary);"> | |
| <div class="text-2xl font-bold" style="color: var(--text-primary);">${gradeHistory.length}</div> | |
| <div class="text-sm" style="color: var(--text-secondary);">Total Tests</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function exportHistory() { | |
| const data = { | |
| history: gradeHistory, | |
| settings: settings, | |
| exportDate: new Date().toISOString() | |
| }; | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `grade-history-${new Date().toISOString().split('T')[0]}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| function importHistory(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function (e) { | |
| try { | |
| const data = JSON.parse(e.target.result); | |
| if (data.history && Array.isArray(data.history)) { | |
| gradeHistory = data.history; | |
| if (data.settings) { | |
| Object.assign(settings, data.settings); | |
| updateSettingsUI(); | |
| } | |
| renderHistory(); | |
| updateStatistics(); | |
| alert('History imported successfully!'); | |
| } else { | |
| alert('Invalid file format.'); | |
| } | |
| } catch (error) { | |
| alert('Error reading file.'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| } | |
| function toggleTheme() { | |
| settings.darkMode = !settings.darkMode; | |
| document.documentElement.setAttribute('data-theme', settings.darkMode ? 'dark' : 'light'); | |
| elements.darkModeToggle.checked = settings.darkMode; | |
| Storage.save(); | |
| } | |
| function renderHistory() { | |
| const fragment = document.createDocumentFragment(); | |
| if (gradeHistory.length === 0) { | |
| elements.historyList.innerHTML = `<p class="text-slate-400 text-center italic mt-4" style="color: var(--text-secondary);">No history yet.</p>`; | |
| return; | |
| } | |
| gradeHistory.forEach(item => { | |
| const li = document.createElement('li'); | |
| li.className = 'flex justify-between items-center p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors cursor-pointer theme-transition'; | |
| li.style.backgroundColor = 'var(--bg-primary)'; | |
| const letterGradeText = settings.showLetterGrades && item.letterGrade | |
| ? ` (${item.letterGrade})` | |
| : ''; | |
| li.innerHTML = ` | |
| <span class="text-slate-600" style="color: var(--text-secondary);">${item.wrong} Wrong (${item.correct}/${item.total})</span> | |
| <span class="font-bold text-slate-800 text-lg" style="color: var(--text-primary);">${item.grade}${letterGradeText}</span> | |
| `; | |
| li.addEventListener('click', () => { | |
| elements.wrongInput.value = item.wrong; | |
| updateDisplay(); | |
| }); | |
| fragment.appendChild(li); | |
| }); | |
| elements.historyList.innerHTML = ''; | |
| elements.historyList.appendChild(fragment); | |
| Storage.save(); | |
| } | |
| function updateSettingsUI() { | |
| elements.totalQuestionsSettingInput.value = settings.totalQuestions; | |
| elements.gradeViewSettingSelect.value = settings.gradeView; | |
| elements.decimalPlacesSettingInput.value = settings.decimalPlaces; | |
| elements.showLetterGradesCheckbox.checked = settings.showLetterGrades; | |
| elements.darkModeToggle.checked = settings.darkMode; | |
| elements.totalQuestionsInfo.textContent = `The test has a total of ${settings.totalQuestions} questions.`; | |
| elements.wrongInput.max = settings.totalQuestions; | |
| document.documentElement.setAttribute('data-theme', settings.darkMode ? 'dark' : 'light'); | |
| } | |
| function addToHistory() { | |
| const data = calculateGrade(); | |
| if (!data?.error) { | |
| const historyItem = { | |
| ...data, | |
| grade: settings.gradeView === 'points' ? `${data.grade} / 10` : `${data.grade}%`, | |
| timestamp: new Date().toISOString() | |
| }; | |
| gradeHistory.unshift(historyItem); | |
| renderHistory(); | |
| updateStatistics(); | |
| elements.wrongInput.value = ''; | |
| elements.resultDiv.innerHTML = ''; | |
| } | |
| } | |
| function handleTabClick(event) { | |
| const clickedTab = event.currentTarget; | |
| const targetPanelId = clickedTab.dataset.tabTarget; | |
| elements.tabBtns.forEach(btn => btn.classList.remove('tab-active')); | |
| clickedTab.classList.add('tab-active'); | |
| elements.tabPanels.forEach(panel => { | |
| panel.classList.toggle('hidden', `#${panel.id}` !== targetPanelId); | |
| }); | |
| } | |
| const debouncedUpdateDisplay = debounce(updateDisplay, 100); | |
| const debouncedCalculateGoal = debounce(calculateGoal, 300); | |
| function setupEventListeners() { | |
| elements.wrongInput.addEventListener('input', debouncedUpdateDisplay); | |
| elements.wrongInput.addEventListener('keyup', e => e.key === 'Enter' && addToHistory()); | |
| elements.clearHistoryBtn.addEventListener('click', () => { | |
| gradeHistory = []; | |
| renderHistory(); | |
| updateStatistics(); | |
| }); | |
| elements.exportHistoryBtn.addEventListener('click', exportHistory); | |
| elements.importHistoryBtn.addEventListener('click', () => elements.importFile.click()); | |
| elements.importFile.addEventListener('change', importHistory); | |
| elements.targetGradeInput.addEventListener('input', debouncedCalculateGoal); | |
| elements.totalQuestionsSettingInput.addEventListener('input', e => { | |
| const value = parseInt(e.target.value); | |
| if (value > 0) { | |
| settings.totalQuestions = value; | |
| updateSettingsUI(); | |
| updateDisplay(); | |
| calculateGoal(); | |
| Storage.save(); | |
| } | |
| }); | |
| elements.gradeViewSettingSelect.addEventListener('change', e => { | |
| settings.gradeView = e.target.value; | |
| updateDisplay(); | |
| renderHistory(); | |
| Storage.save(); | |
| }); | |
| elements.decimalPlacesSettingInput.addEventListener('input', e => { | |
| const value = parseInt(e.target.value); | |
| if (value >= 0 && value <= 5) { | |
| settings.decimalPlaces = value; | |
| updateDisplay(); | |
| Storage.save(); | |
| } | |
| }); | |
| elements.showLetterGradesCheckbox.addEventListener('change', e => { | |
| settings.showLetterGrades = e.target.checked; | |
| updateDisplay(); | |
| renderHistory(); | |
| Storage.save(); | |
| }); | |
| elements.darkModeToggle.addEventListener('change', toggleTheme); | |
| elements.tabBtns.forEach(btn => btn.addEventListener('click', handleTabClick)); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', e => { | |
| if (e.ctrlKey || e.metaKey) { | |
| switch (e.key) { | |
| case 'e': | |
| e.preventDefault(); | |
| exportHistory(); | |
| break; | |
| case 'x': | |
| e.preventDefault(); | |
| gradeHistory = []; | |
| renderHistory(); | |
| updateStatistics(); | |
| break; | |
| case 'd': | |
| e.preventDefault(); | |
| toggleTheme(); | |
| break; | |
| } | |
| } | |
| }); | |
| } | |
| function initializeApp() { | |
| Storage.load(); | |
| updateSettingsUI(); | |
| renderHistory(); | |
| updateStatistics(); | |
| setupEventListeners(); | |
| elements.tabBtns[0].classList.add('tab-active'); | |
| elements.tabPanels[0].classList.remove('hidden'); | |
| } | |
| initializeApp(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment