Report-Generator / templates /qtab_exam.html
Jaimodiji's picture
Upload folder using huggingface_hub
c001f24
Raw
History Blame
48 kB
{% extends "base.html" %}
{% block title %}Exam Mode - Question Table{% endblock %}
{% block head %}
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1, viewport-fit=cover">
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
--danger-gradient: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
--dark-bg: #0d1117;
--card-bg: #161b22;
--border-color: #30363d;
--text-muted: #8b949e;
}
* {
transition: all 0.3s ease;
}
html, body {
height: 100%;
height: 100dvh;
width: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: var(--dark-bg);
}
/* Hide base.html navbar in exam mode */
body > nav.navbar,
body > .navbar,
.modern-navbar {
display: none !important;
}
/* Progress Bar */
.progress-bar-container {
height: 4px;
background: var(--border-color);
position: relative;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--primary-gradient);
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.progress-bar-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* Navigation Buttons */
.nav-btn {
background: transparent;
border: none;
color: #8b949e;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.nav-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
.nav-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.nav-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.nav-btn:active::before {
width: 300px;
height: 300px;
}
/* Question Container */
.question-container {
perspective: 1000px;
}
/* Question Card */
.question-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
transform-style: preserve-3d;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s;
}
.question-card.slide-left {
animation: slideLeft 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.question-card.slide-right {
animation: slideRight 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes slideLeft {
0% { transform: translateX(100%) scale(0.8); opacity: 0; }
100% { transform: translateX(0) scale(1); opacity: 1; }
}
@keyframes slideRight {
0% { transform: translateX(-100%) scale(0.8); opacity: 0; }
100% { transform: translateX(0) scale(1); opacity: 1; }
}
/* Answer Display */
.answer-display {
font-size: clamp(3rem, 10vw, 8rem);
font-weight: 800;
background: var(--success-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2;
position: relative;
}
.answer-hidden {
filter: blur(20px);
user-select: none;
cursor: pointer;
}
.answer-reveal-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
/* Section Badge */
.section-badge {
background: var(--primary-gradient);
padding: 0.5rem 1.5rem;
border-radius: 50px;
font-weight: 600;
display: inline-block;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
/* Question Number */
.question-number {
font-size: clamp(1.5rem, 5vw, 3rem);
font-weight: 700;
color: #fff;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
/* Footer Controls */
.footer-controls {
background: var(--card-bg);
border-top: 1px solid var(--border-color);
backdrop-filter: blur(10px);
}
/* Control Buttons */
.control-btn {
background: var(--card-bg);
border: 1px solid var(--border-color);
color: #fff;
border-radius: 12px;
padding: 0.5rem 1rem;
transition: all 0.3s;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: #667eea;
color: #fff;
transform: translateY(-2px);
}
.control-btn.active {
background: var(--primary-gradient);
border-color: transparent;
}
/* Timer Display */
.timer-display {
font-family: 'SF Mono', 'Consolas', monospace;
font-size: 1.5rem;
font-weight: 600;
color: #fff;
background: rgba(0, 0, 0, 0.3);
padding: 0.5rem 1rem;
border-radius: 8px;
}
.timer-display.warning {
color: #f0ad4e;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Keyboard Hints */
.kbd-hint {
background: var(--card-bg);
border: 1px solid var(--border-color);
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-family: 'SF Mono', monospace;
font-size: 0.75rem;
}
/* Modal Styling */
.modal-content.dark-modal {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
}
/* Question Grid */
.question-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 8px;
max-height: 400px;
overflow-y: auto;
padding: 1rem;
}
.question-grid-item {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--dark-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.question-grid-item:hover {
background: rgba(102, 126, 234, 0.2);
border-color: #667eea;
transform: scale(1.1);
}
.question-grid-item.current {
background: var(--primary-gradient);
border-color: transparent;
}
.question-grid-item.marked {
border-color: #f0ad4e;
box-shadow: 0 0 10px rgba(240, 173, 78, 0.3);
}
/* Settings Panel */
.settings-panel {
position: fixed;
top: 0;
right: -400px;
width: 400px;
max-width: 100%;
height: 100vh;
background: var(--card-bg);
border-left: 1px solid var(--border-color);
z-index: 1050;
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
}
.settings-panel.open {
right: 0;
}
.settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1040;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.settings-overlay.open {
opacity: 1;
visibility: visible;
}
/* Swipe Indicator */
.swipe-indicator {
position: fixed;
top: 50%;
transform: translateY(-50%);
font-size: 4rem;
color: rgba(255, 255, 255, 0.3);
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
.swipe-indicator.left {
left: 20px;
}
.swipe-indicator.right {
right: 20px;
}
.swipe-indicator.show {
opacity: 1;
}
/* Bookmark Icon */
.bookmark-btn {
font-size: 1.5rem;
color: var(--text-muted);
cursor: pointer;
transition: all 0.3s;
}
.bookmark-btn:hover {
color: #f0ad4e;
transform: scale(1.2);
}
.bookmark-btn.active {
color: #f0ad4e;
animation: bookmarkPop 0.3s ease;
}
@keyframes bookmarkPop {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.3); }
}
/* Fullscreen Mode */
.fullscreen-mode .navbar,
.fullscreen-mode .footer-controls {
display: none !important;
}
.fullscreen-mode .question-container {
height: 100vh !important;
}
/* Mini Progress Dots */
.progress-dots {
display: flex;
gap: 4px;
justify-content: center;
flex-wrap: wrap;
max-width: 400px;
margin: 0 auto;
}
.progress-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--border-color);
transition: all 0.3s;
}
.progress-dot.active {
background: #667eea;
transform: scale(1.5);
}
.progress-dot.visited {
background: #38ef7d;
}
/* Touch Feedback */
.touch-ripple {
position: fixed;
border-radius: 50%;
background: rgba(102, 126, 234, 0.3);
transform: scale(0);
animation: ripple 0.6s linear;
pointer-events: none;
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
/* Completion Celebration */
.celebration {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9999;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--dark-bg);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #667eea;
}
/* Responsive */
@media (max-width: 768px) {
.nav-btn {
min-width: 50px !important;
font-size: 1.5rem !important;
}
.question-card {
border-radius: 16px;
margin: 0.5rem;
}
.footer-controls {
padding: 0.5rem !important;
}
.control-btn span {
display: none;
}
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex flex-column text-light" id="examContainer" style="height: 100dvh; width: 100%; overflow: hidden; position: fixed; top: 0; left: 0;">
<!-- Progress Bar -->
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progressBar" style="width: 0%"></div>
</div>
<!-- Header -->
<nav class="navbar navbar-dark border-bottom border-secondary py-2" style="background: var(--card-bg);">
<div class="container-fluid">
<span class="navbar-brand mb-0 h5 fw-bold d-flex align-items-center gap-2">
<i class="fas fa-graduation-cap"></i>
<span class="d-none d-sm-inline">Exam Mode</span>
</span>
<div class="d-flex gap-2 align-items-center">
<!-- Timer -->
<div class="timer-display d-none" id="timerDisplay">
<i class="fas fa-clock me-1"></i>
<span id="timerValue">00:00</span>
</div>
<!-- Control Buttons -->
<button class="control-btn" onclick="toggleTimer()" title="Toggle Timer" id="timerBtn">
<i class="fas fa-stopwatch"></i>
</button>
<button class="control-btn" onclick="openQuestionGrid()" title="Question Grid">
<i class="fas fa-th"></i>
</button>
<button class="control-btn" onclick="toggleSettings()" title="Settings">
<i class="fas fa-cog"></i>
</button>
<button class="control-btn" onclick="toggleFullscreen()" title="Fullscreen" id="fullscreenBtn">
<i class="fas fa-expand"></i>
</button>
<a href="{{ url_for('qtab.qtab_list') }}" class="control-btn text-danger" title="Exit">
<i class="fas fa-times"></i>
</a>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="flex-grow-1 position-relative overflow-hidden d-flex align-items-center question-container"
id="questionContainer"
tabindex="0">
<!-- Previous Button -->
<button class="nav-btn h-100 d-flex align-items-center justify-content-center"
style="min-width: 80px; font-size: 2.5rem;"
id="btnPrevLarge"
onclick="navigatePrev()">
<i class="fas fa-chevron-left"></i>
</button>
<!-- Question Display -->
<div class="flex-grow-1 h-100 d-flex align-items-center justify-content-center p-3 overflow-auto" style="overflow-y: auto;">
<div class="text-center w-100" style="max-width: 900px;" id="questionDisplay">
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<!-- Next Button -->
<button class="nav-btn h-100 d-flex align-items-center justify-content-center"
style="min-width: 80px; font-size: 2.5rem;"
id="btnNextLarge"
onclick="navigateNext()">
<i class="fas fa-chevron-right"></i>
</button>
<!-- Swipe Indicators -->
<div class="swipe-indicator left" id="swipeLeft">
<i class="fas fa-chevron-left"></i>
</div>
<div class="swipe-indicator right" id="swipeRight">
<i class="fas fa-chevron-right"></i>
</div>
</div>
<!-- Footer Controls -->
<div class="footer-controls py-2">
<div class="container-fluid">
<div class="row g-2 align-items-center">
<!-- Quick Navigation -->
<div class="col-12 col-md-4 order-md-1 text-center text-md-start">
<div class="d-flex gap-2 justify-content-center justify-content-md-start flex-wrap">
<button class="control-btn" onclick="navigateFirst()" title="First">
<i class="fas fa-step-backward"></i>
</button>
<button class="control-btn" id="shuffleBtn" onclick="toggleShuffle()" title="Shuffle">
<i class="fas fa-random"></i>
</button>
<button class="control-btn" id="hideAnswerBtn" onclick="toggleHideAnswer()" title="Hide/Show Answers">
<i class="fas fa-eye"></i>
</button>
<button class="control-btn" onclick="navigateLast()" title="Last">
<i class="fas fa-step-forward"></i>
</button>
</div>
</div>
<!-- Position & Jump -->
<div class="col-12 col-md-4 order-md-2 text-center">
<div class="d-flex align-items-center justify-content-center gap-2">
<div class="input-group input-group-sm" style="max-width: 150px;">
<input type="number" class="form-control bg-dark text-light border-secondary"
id="jumpInput" placeholder="Q#" min="1">
<button class="btn btn-primary" onclick="jumpToQuestionNumber()">
<i class="fas fa-arrow-right"></i>
</button>
</div>
<span class="text-muted small" id="positionIndicator">1 / 1</span>
</div>
</div>
<!-- Keyboard Hints -->
<div class="col-12 col-md-4 order-md-3 text-center text-md-end">
<div class="d-none d-lg-block">
<span class="text-muted small">
<span class="kbd-hint"></span> <span class="kbd-hint"></span> Navigate
<span class="ms-2"><span class="kbd-hint">Space</span> Reveal</span>
<span class="ms-2"><span class="kbd-hint">S</span> Sections</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Settings Panel -->
<div class="settings-overlay" id="settingsOverlay" onclick="toggleSettings()"></div>
<div class="settings-panel" id="settingsPanel">
<div class="p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0"><i class="fas fa-cog me-2"></i>Settings</h5>
<button class="btn btn-sm btn-outline-secondary" onclick="toggleSettings()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mb-4">
<h6 class="text-muted mb-3">Display</h6>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="settingHideAnswer">
<label class="form-check-label" for="settingHideAnswer">Hide answers by default</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="settingShowProgress" checked>
<label class="form-check-label" for="settingShowProgress">Show progress bar</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="settingAnimations" checked>
<label class="form-check-label" for="settingAnimations">Enable animations</label>
</div>
</div>
<div class="mb-4">
<h6 class="text-muted mb-3">Timer</h6>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="settingCountdown">
<label class="form-check-label" for="settingCountdown">Countdown mode</label>
</div>
<div class="mb-2">
<label class="form-label small">Countdown time (minutes)</label>
<input type="number" class="form-control form-control-sm bg-dark text-light border-secondary"
id="settingCountdownTime" value="30" min="1" max="180">
</div>
</div>
<div class="mb-4">
<h6 class="text-muted mb-3">Navigation</h6>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="settingSwipeEnabled" checked>
<label class="form-check-label" for="settingSwipeEnabled">Enable swipe gestures</label>
</div>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="settingLoopNavigation">
<label class="form-check-label" for="settingLoopNavigation">Loop navigation</label>
</div>
</div>
<div class="mb-4">
<h6 class="text-muted mb-3">Quick Actions</h6>
<button class="btn btn-outline-primary w-100 mb-2" onclick="resetProgress()">
<i class="fas fa-redo me-2"></i>Reset Progress
</button>
<button class="btn btn-outline-warning w-100 mb-2" onclick="clearBookmarks()">
<i class="fas fa-bookmark me-2"></i>Clear Bookmarks
</button>
</div>
</div>
</div>
<!-- Question Grid Modal -->
<div class="modal fade" id="questionGridModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content dark-modal text-light">
<div class="modal-header border-secondary">
<h5 class="modal-title">
<i class="fas fa-th me-2"></i>Question Navigator
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<!-- Section Filter -->
<div class="mb-3" id="sectionFilterContainer">
<select class="form-select form-select-sm bg-dark text-light border-secondary"
id="sectionFilter" onchange="filterQuestionGrid()">
<option value="">All Sections</option>
</select>
</div>
<div class="question-grid" id="questionGrid">
<!-- Populated dynamically -->
</div>
<div class="mt-3 d-flex gap-3 justify-content-center text-muted small">
<span><span class="badge" style="background: var(--primary-gradient);">&nbsp;</span> Current</span>
<span><span class="badge bg-success">&nbsp;</span> Visited</span>
<span><span class="badge" style="border: 2px solid #f0ad4e;">&nbsp;</span> Bookmarked</span>
</div>
</div>
</div>
</div>
</div>
<!-- Section Jump Modal -->
<div class="modal fade" id="sectionJumpModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content dark-modal text-light">
<div class="modal-header border-secondary">
<h5 class="modal-title">
<i class="fas fa-list me-2"></i>Jump to Section
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div class="list-group list-group-flush" id="sectionList">
<!-- Populated dynamically -->
</div>
</div>
</div>
</div>
</div>
<!-- Celebration Canvas -->
<canvas class="celebration" id="celebrationCanvas"></canvas>
<script>
// Data
const resultData = {{ result_json | tojson }};
const allImages = {{ all_images | tojson }};
// State
let allQuestions = [];
let originalQuestions = [];
let currentQuestionIndex = 0;
let visitedQuestions = new Set();
let bookmarkedQuestions = new Set();
let isShuffled = false;
let hideAnswers = false;
let animationsEnabled = true;
let lastDirection = 'right';
// Timer State
let timerInterval = null;
let timerSeconds = 0;
let timerRunning = false;
let countdownMode = false;
let countdownSeconds = 1800; // 30 minutes
// Settings
let settings = {
hideAnswer: false,
showProgress: true,
animations: true,
swipeEnabled: true,
loopNavigation: false,
countdownMode: false,
countdownTime: 30
};
// Load settings from localStorage
function loadSettings() {
const saved = localStorage.getItem('examModeSettings');
if (saved) {
settings = { ...settings, ...JSON.parse(saved) };
}
applySettings();
}
function saveSettings() {
localStorage.setItem('examModeSettings', JSON.stringify(settings));
}
function applySettings() {
document.getElementById('settingHideAnswer').checked = settings.hideAnswer;
document.getElementById('settingShowProgress').checked = settings.showProgress;
document.getElementById('settingAnimations').checked = settings.animations;
document.getElementById('settingSwipeEnabled').checked = settings.swipeEnabled;
document.getElementById('settingLoopNavigation').checked = settings.loopNavigation;
document.getElementById('settingCountdown').checked = settings.countdownMode;
document.getElementById('settingCountdownTime').value = settings.countdownTime;
hideAnswers = settings.hideAnswer;
animationsEnabled = settings.animations;
countdownMode = settings.countdownMode;
countdownSeconds = settings.countdownTime * 60;
document.querySelector('.progress-bar-container').style.display = settings.showProgress ? 'block' : 'none';
updateHideAnswerBtn();
}
// Parse questions
function parseQuestions() {
allQuestions = [];
if (!resultData || resultData.status !== 'success') return;
if (Array.isArray(resultData.data)) {
resultData.data.forEach((qa, idx) => {
allQuestions.push({
id: idx,
section: null,
questionNumber: qa.question_number,
answer: qa.answer
});
});
} else {
let idx = 0;
Object.entries(resultData.data).forEach(([section, questions]) => {
questions.forEach(qa => {
allQuestions.push({
id: idx++,
section: section,
questionNumber: qa.question_number,
answer: qa.answer
});
});
});
}
originalQuestions = [...allQuestions];
}
// Display question
function displayQuestion(direction = null) {
const container = document.getElementById('questionDisplay');
const dir = direction || lastDirection;
if (allQuestions.length === 0) {
container.innerHTML = `
<div class="question-card p-5">
<i class="fas fa-exclamation-triangle text-warning fa-3x mb-3"></i>
<h4>No Questions Available</h4>
<p class="text-muted">There are no questions to display.</p>
</div>
`;
return;
}
const q = allQuestions[currentQuestionIndex];
visitedQuestions.add(q.id);
const isBookmarked = bookmarkedQuestions.has(q.id);
const answerClass = hideAnswers ? 'answer-hidden' : '';
const cardHtml = `
<div class="question-card p-4 p-md-5 ${animationsEnabled ? (dir === 'right' ? 'slide-left' : 'slide-right') : ''}">
<!-- Top Bar -->
<div class="d-flex justify-content-between align-items-start mb-4">
${q.section ? `<span class="section-badge">${q.section}</span>` : '<span></span>'}
<i class="fas fa-bookmark bookmark-btn ${isBookmarked ? 'active' : ''}"
onclick="toggleBookmark(${q.id})" title="Bookmark"></i>
</div>
<!-- Question Number -->
<div class="question-number mb-4">Question ${q.questionNumber}</div>
<!-- Answer -->
<div class="position-relative my-5" onclick="revealAnswer(event)">
<div class="answer-display ${answerClass}" id="answerText">
${q.answer}
</div>
${hideAnswers ? `
<div class="answer-reveal-btn">
<button class="btn btn-lg btn-outline-light" onclick="revealAnswer(event)">
<i class="fas fa-eye me-2"></i>Tap to Reveal
</button>
</div>
` : ''}
</div>
<!-- Progress Indicator -->
<div class="mt-4">
<span class="badge bg-secondary px-3 py-2">
${currentQuestionIndex + 1} of ${allQuestions.length}
</span>
</div>
</div>
`;
container.innerHTML = cardHtml;
updateUI();
// Check if completed
if (currentQuestionIndex === allQuestions.length - 1 && visitedQuestions.size === allQuestions.length) {
setTimeout(() => celebrate(), 500);
}
}
// Update UI elements
function updateUI() {
// Progress bar
const progress = ((currentQuestionIndex + 1) / allQuestions.length) * 100;
document.getElementById('progressBar').style.width = `${progress}%`;
// Position indicator
document.getElementById('positionIndicator').textContent =
`${currentQuestionIndex + 1} / ${allQuestions.length}`;
// Nav buttons
const isFirst = currentQuestionIndex === 0;
const isLast = currentQuestionIndex === allQuestions.length - 1;
document.getElementById('btnPrevLarge').disabled = isFirst && !settings.loopNavigation;
document.getElementById('btnNextLarge').disabled = isLast && !settings.loopNavigation;
// Jump input
const jumpInput = document.getElementById('jumpInput');
if (jumpInput) {
jumpInput.max = allQuestions.length;
}
}
// Navigation
function navigatePrev() {
if (currentQuestionIndex > 0) {
currentQuestionIndex--;
lastDirection = 'left';
displayQuestion('left');
} else if (settings.loopNavigation) {
currentQuestionIndex = allQuestions.length - 1;
lastDirection = 'left';
displayQuestion('left');
}
focusContainer();
saveProgress();
}
function navigateNext() {
if (currentQuestionIndex < allQuestions.length - 1) {
currentQuestionIndex++;
lastDirection = 'right';
displayQuestion('right');
} else if (settings.loopNavigation) {
currentQuestionIndex = 0;
lastDirection = 'right';
displayQuestion('right');
}
focusContainer();
saveProgress();
}
function navigateFirst() {
currentQuestionIndex = 0;
lastDirection = 'left';
displayQuestion('left');
focusContainer();
saveProgress();
}
function navigateLast() {
currentQuestionIndex = allQuestions.length - 1;
lastDirection = 'right';
displayQuestion('right');
focusContainer();
saveProgress();
}
function jumpToQuestionNumber() {
const input = document.getElementById('jumpInput');
const qNum = parseInt(input.value);
if (isNaN(qNum) || qNum < 1 || qNum > allQuestions.length) {
input.classList.add('is-invalid');
input.classList.add('shake');
setTimeout(() => {
input.classList.remove('is-invalid', 'shake');
}, 500);
return;
}
const dir = qNum - 1 > currentQuestionIndex ? 'right' : 'left';
currentQuestionIndex = qNum - 1;
lastDirection = dir;
displayQuestion(dir);
input.value = '';
focusContainer();
// Close modal if open
const modal = bootstrap.Modal.getInstance(document.getElementById('questionGridModal'));
if (modal) modal.hide();
}
function jumpToSection(sectionName) {
const idx = allQuestions.findIndex(q => q.section === sectionName);
if (idx !== -1) {
const dir = idx > currentQuestionIndex ? 'right' : 'left';
currentQuestionIndex = idx;
lastDirection = dir;
displayQuestion(dir);
bootstrap.Modal.getInstance(document.getElementById('sectionJumpModal')).hide();
focusContainer();
saveProgress();
}
}
function focusContainer() {
document.getElementById('questionContainer').focus();
}
// Answer reveal
function revealAnswer(e) {
if (e) e.stopPropagation();
const answerText = document.getElementById('answerText');
if (answerText && answerText.classList.contains('answer-hidden')) {
answerText.classList.remove('answer-hidden');
const revealBtn = document.querySelector('.answer-reveal-btn');
if (revealBtn) revealBtn.remove();
}
}
// Toggle hide answer mode
function toggleHideAnswer() {
hideAnswers = !hideAnswers;
settings.hideAnswer = hideAnswers;
saveSettings();
updateHideAnswerBtn();
displayQuestion();
}
function updateHideAnswerBtn() {
const btn = document.getElementById('hideAnswerBtn');
const icon = btn.querySelector('i');
if (hideAnswers) {
btn.classList.add('active');
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
btn.classList.remove('active');
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
}
// Shuffle
function toggleShuffle() {
isShuffled = !isShuffled;
const btn = document.getElementById('shuffleBtn');
if (isShuffled) {
btn.classList.add('active');
allQuestions = [...allQuestions].sort(() => Math.random() - 0.5);
} else {
btn.classList.remove('active');
allQuestions = [...originalQuestions];
}
currentQuestionIndex = 0;
displayQuestion();
}
// Bookmarks
function toggleBookmark(id) {
if (bookmarkedQuestions.has(id)) {
bookmarkedQuestions.delete(id);
} else {
bookmarkedQuestions.add(id);
}
// Update bookmark icon
const icon = document.querySelector('.bookmark-btn');
if (icon) {
icon.classList.toggle('active');
}
saveBookmarks();
}
function saveBookmarks() {
localStorage.setItem('examBookmarks', JSON.stringify([...bookmarkedQuestions]));
}
function loadBookmarks() {
const saved = localStorage.getItem('examBookmarks');
if (saved) {
bookmarkedQuestions = new Set(JSON.parse(saved));
}
}
function clearBookmarks() {
bookmarkedQuestions.clear();
saveBookmarks();
displayQuestion();
}
// Timer
function toggleTimer() {
const timerDisplay = document.getElementById('timerDisplay');
const timerBtn = document.getElementById('timerBtn');
if (timerRunning) {
stopTimer();
timerBtn.classList.remove('active');
timerDisplay.classList.add('d-none');
} else {
startTimer();
timerBtn.classList.add('active');
timerDisplay.classList.remove('d-none');
}
}
function startTimer() {
timerRunning = true;
if (countdownMode) {
timerSeconds = countdownSeconds;
}
timerInterval = setInterval(() => {
if (countdownMode) {
timerSeconds--;
if (timerSeconds <= 0) {
stopTimer();
alert('Time is up!');
return;
}
if (timerSeconds <= 60) {
document.getElementById('timerDisplay').classList.add('warning');
}
} else {
timerSeconds++;
}
updateTimerDisplay();
}, 1000);
}
function stopTimer() {
timerRunning = false;
clearInterval(timerInterval);
}
function updateTimerDisplay() {
const mins = Math.floor(timerSeconds / 60);
const secs = timerSeconds % 60;
document.getElementById('timerValue').textContent =
`${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// Fullscreen
function toggleFullscreen() {
const container = document.getElementById('examContainer');
const btn = document.getElementById('fullscreenBtn');
if (!document.fullscreenElement) {
container.requestFullscreen().then(() => {
container.classList.add('fullscreen-mode');
btn.querySelector('i').classList.replace('fa-expand', 'fa-compress');
});
} else {
document.exitFullscreen().then(() => {
container.classList.remove('fullscreen-mode');
btn.querySelector('i').classList.replace('fa-compress', 'fa-expand');
});
}
}
// Settings Panel
function toggleSettings() {
const panel = document.getElementById('settingsPanel');
const overlay = document.getElementById('settingsOverlay');
panel.classList.toggle('open');
overlay.classList.toggle('open');
}
// Question Grid
function openQuestionGrid() {
populateQuestionGrid();
populateSectionFilter();
new bootstrap.Modal(document.getElementById('questionGridModal')).show();
}
function populateQuestionGrid(sectionFilter = '') {
const grid = document.getElementById('questionGrid');
const filtered = sectionFilter
? allQuestions.filter(q => q.section === sectionFilter)
: allQuestions;
grid.innerHTML = filtered.map((q, idx) => {
const originalIdx = allQuestions.indexOf(q);
const classes = [
'question-grid-item',
originalIdx === currentQuestionIndex ? 'current' : '',
visitedQuestions.has(q.id) ? 'visited' : '',
bookmarkedQuestions.has(q.id) ? 'marked' : ''
].filter(Boolean).join(' ');
return `
<div class="${classes}"
onclick="jumpFromGrid(${originalIdx})"
title="Question ${q.questionNumber}${q.section ? ' - ' + q.section : ''}">
${q.questionNumber}
</div>
`;
}).join('');
}
function populateSectionFilter() {
const sections = [...new Set(allQuestions.filter(q => q.section).map(q => q.section))];
const container = document.getElementById('sectionFilterContainer');
const select = document.getElementById('sectionFilter');
if (sections.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
select.innerHTML = `<option value="">All Sections</option>` +
sections.map(s => `<option value="${s}">${s}</option>`).join('');
}
function filterQuestionGrid() {
const filter = document.getElementById('sectionFilter').value;
populateQuestionGrid(filter);
}
function jumpFromGrid(idx) {
const dir = idx > currentQuestionIndex ? 'right' : 'left';
currentQuestionIndex = idx;
lastDirection = dir;
displayQuestion(dir);
bootstrap.Modal.getInstance(document.getElementById('questionGridModal')).hide();
focusContainer();
saveProgress();
}
// Section Menu
function showSectionMenu() {
const sections = [...new Set(allQuestions.filter(q => q.section).map(q => q.section))];
if (sections.length === 0) {
return;
}
const sectionList = document.getElementById('sectionList');
sectionList.innerHTML = sections.map(section => {
const count = allQuestions.filter(q => q.section === section).length;
const current = allQuestions[currentQuestionIndex]?.section === section;
return `
<button type="button" class="list-group-item list-group-item-action text-light border-secondary py-3 ${current ? 'active' : ''}"
style="background: ${current ? 'var(--primary-gradient)' : 'var(--card-bg)'};"
onclick="jumpToSection('${section.replace(/'/g, "\\'")}')">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1 fw-bold">${section}</h6>
<small class="opacity-75">${count} question${count !== 1 ? 's' : ''}</small>
</div>
<i class="fas fa-chevron-right opacity-50"></i>
</div>
</button>
`;
}).join('');
new bootstrap.Modal(document.getElementById('sectionJumpModal')).show();
}
// Reset Progress
function resetProgress() {
visitedQuestions.clear();
currentQuestionIndex = 0;
timerSeconds = 0;
updateTimerDisplay();
displayQuestion();
toggleSettings();
}
// Celebration
function celebrate() {
const canvas = document.getElementById('celebrationCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const colors = ['#667eea', '#764ba2', '#38ef7d', '#11998e', '#f0ad4e', '#eb3349'];
for (let i = 0; i < 150; i++) {
particles.push({
x: Math.random() * canvas.width,
y: canvas.height + 10,
vx: (Math.random() - 0.5) * 10,
vy: -(Math.random() * 15 + 5),
size: Math.random() * 10 + 5,
color: colors[Math.floor(Math.random() * colors.length)],
gravity: 0.3,
life: 1
});
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let alive = false;
particles.forEach(p => {
if (p.life > 0) {
alive = true;
p.x += p.vx;
p.y += p.vy;
p.vy += p.gravity;
p.life -= 0.01;
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
});
if (alive) {
requestAnimationFrame(animate);
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
animate();
}
// Touch/Swipe Gestures
let touchStartX = 0;
let touchEndX = 0;
function initSwipeGestures() {
const container = document.getElementById('questionContainer');
container.addEventListener('touchstart', (e) => {
if (!settings.swipeEnabled) return;
touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
container.addEventListener('touchmove', (e) => {
if (!settings.swipeEnabled) return;
const currentX = e.changedTouches[0].screenX;
const diff = currentX - touchStartX;
if (Math.abs(diff) > 50) {
document.getElementById(diff > 0 ? 'swipeRight' : 'swipeLeft').classList.add('show');
document.getElementById(diff > 0 ? 'swipeLeft' : 'swipeRight').classList.remove('show');
}
}, { passive: true });
container.addEventListener('touchend', (e) => {
if (!settings.swipeEnabled) return;
document.getElementById('swipeLeft').classList.remove('show');
document.getElementById('swipeRight').classList.remove('show');
touchEndX = e.changedTouches[0].screenX;
const diff = touchEndX - touchStartX;
if (Math.abs(diff) > 100) {
if (diff > 0) {
navigatePrev();
} else {
navigateNext();
}
}
}, { passive: true });
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
switch(e.key) {
case 'ArrowLeft':
e.preventDefault();
navigatePrev();
break;
case 'ArrowRight':
e.preventDefault();
navigateNext();
break;
case ' ':
e.preventDefault();
revealAnswer();
break;
case 'Escape':
if (document.getElementById('settingsPanel').classList.contains('open')) {
toggleSettings();
} else {
window.location.href = '{{ url_for("qtab.qtab_list") }}';
}
break;
case 's':
case 'S':
e.preventDefault();
showSectionMenu();
break;
case 'g':
case 'G':
e.preventDefault();
openQuestionGrid();
break;
case 'Home':
e.preventDefault();
navigateFirst();
break;
case 'End':
e.preventDefault();
navigateLast();
break;
case 'b':
case 'B':
e.preventDefault();
const q = allQuestions[currentQuestionIndex];
if (q) toggleBookmark(q.id);
break;
case 'f':
case 'F':
e.preventDefault();
toggleFullscreen();
break;
}
});
// Settings change listeners
document.querySelectorAll('#settingsPanel input').forEach(input => {
input.addEventListener('change', () => {
settings.hideAnswer = document.getElementById('settingHideAnswer').checked;
settings.showProgress = document.getElementById('settingShowProgress').checked;
settings.animations = document.getElementById('settingAnimations').checked;
settings.swipeEnabled = document.getElementById('settingSwipeEnabled').checked;
settings.loopNavigation = document.getElementById('settingLoopNavigation').checked;
settings.countdownMode = document.getElementById('settingCountdown').checked;
settings.countdownTime = parseInt(document.getElementById('settingCountdownTime').value) || 30;
saveSettings();
applySettings();
displayQuestion();
});
});
// Jump input enter key
document.getElementById('jumpInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
jumpToQuestionNumber();
}
});
// ==========================================
// PROGRESS PERSISTENCE
// ==========================================
function saveProgress() {
const progress = {
imageId: {{ image.id }},
currentIndex: currentQuestionIndex,
visited: Array.from(visitedQuestions),
timerSeconds: timerSeconds,
shuffled: shuffleMode,
shuffledOrder: shuffleMode ? allQuestions.map(q => q.id) : [],
timestamp: Date.now()
};
localStorage.setItem('qtab_exam_progress_{{ image.id }}', JSON.stringify(progress));
}
function loadProgress() {
const saved = localStorage.getItem('qtab_exam_progress_{{ image.id }}');
if (saved) {
try {
const progress = JSON.parse(saved);
// Only load if less than 24 hours old
if (Date.now() - progress.timestamp < 24 * 60 * 60 * 1000) {
currentQuestionIndex = progress.currentIndex || 0;
visitedQuestions = new Set(progress.visited || []);
timerSeconds = progress.timerSeconds || 0;
// Restore shuffled order if was shuffled
if (progress.shuffled && progress.shuffledOrder && progress.shuffledOrder.length === allQuestions.length) {
const orderMap = new Map(allQuestions.map(q => [q.id, q]));
allQuestions = progress.shuffledOrder.map(id => orderMap.get(id)).filter(Boolean);
shuffleMode = true;
document.getElementById('shuffleBtn').classList.add('active');
}
updateTimerDisplay();
return true;
}
} catch (e) {
console.error('Error loading progress:', e);
}
}
return false;
}
// Navigation functions now have saveProgress() built-in
// Initialize
parseQuestions();
loadSettings();
loadBookmarks();
initSwipeGestures();
// Load saved progress if exists
const progressLoaded = loadProgress();
if (progressLoaded) {
console.log('Progress restored from last session');
}
displayQuestion();
focusContainer();
// Save progress periodically and on visibility change
setInterval(saveProgress, 30000); // Every 30 seconds
document.addEventListener('visibilitychange', () => {
if (document.hidden) saveProgress();
});
</script>
{% endblock %}