Spaces:
Running
Running
make me a full working mobile-first html css javascript project for a school anti-cheating exam app called "Nova ExamGuard". it must be a single-page web app (spa style) where all buttons and links work — no dead buttons, no reloads, everything switches screens using javascript. everything must work just by opening index.html (no backend). include all pages inside one continuous html file, with css and js either inline or in the same file. design it for phones (max-width 420px) with clean modern ui, rounded corners, and bottom navigation.
98d40b7 verified | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Nova ExamGuard</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <style> | |
| :root { | |
| --primary: #4361ee; | |
| --secondary: #3a0ca3; | |
| --danger: #ef233c; | |
| --success: #2ec4b6; | |
| } | |
| * { | |
| -webkit-tap-highlight-color: transparent; | |
| -webkit-user-select: none; | |
| user-select: none; | |
| } | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| background-color: #f8f9fa; | |
| color: #333; | |
| margin: 0; | |
| padding: 0; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| .page { | |
| display: none; | |
| padding: 20px; | |
| padding-bottom: 80px; | |
| min-height: calc(100vh - 80px); | |
| } | |
| .page.active { | |
| display: block; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .nav-tab.active { | |
| color: var(--primary); | |
| } | |
| .nav-tab.active svg { | |
| stroke: var(--primary); | |
| } | |
| .exam-card { | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| .exam-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| } | |
| .webcam-container { | |
| background-color: #e9ecef; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| .status-badge { | |
| font-size: 12px; | |
| padding: 4px 8px; | |
| border-radius: 20px; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0,0,0,0.5); | |
| z-index: 100; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .modal-content { | |
| background-color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| max-width: 90%; | |
| width: 100%; | |
| max-width: 350px; | |
| animation: modalFadeIn 0.3s ease; | |
| } | |
| @keyframes modalFadeIn { | |
| from { opacity: 0; transform: translateY(-20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .question-option { | |
| transition: background-color 0.2s; | |
| } | |
| .question-option:hover { | |
| background-color: #f1f3f5; | |
| } | |
| .question-option.selected { | |
| background-color: #e6f7ff; | |
| border-color: var(--primary); | |
| } | |
| .suspicious-count { | |
| background-color: var(--danger); | |
| color: white; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| position: absolute; | |
| top: -5px; | |
| right: -5px; | |
| } | |
| </style> | |
| </head> | |
| <body class="relative"> | |
| <!-- Login Page --> | |
| <div id="login-page" class="page active flex flex-col justify-center items-center p-6"> | |
| <div class="w-full max-w-xs"> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-800 mb-2">Nova ExamGuard</h1> | |
| <p class="text-gray-600">Secure exam proctoring system</p> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md p-6 mb-4"> | |
| <div class="mb-6"> | |
| <label for="student-id" class="block text-sm font-medium text-gray-700 mb-1">Student ID</label> | |
| <input type="text" id="student-id" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"> | |
| </div> | |
| <div class="mb-6"> | |
| <label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label> | |
| <input type="password" id="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"> | |
| </div> | |
| <button id="login-btn" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-blue-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"> | |
| Login | |
| </button> | |
| <div class="text-center mt-4"> | |
| <a href="#" id="forgot-password-link" class="text-sm text-blue-600 hover:underline">Forgot password?</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Forgot Password Modal --> | |
| <div id="forgot-password-modal" class="modal"> | |
| <div class="modal-content"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-medium">Reset Password</h3> | |
| <button id="close-modal" class="text-gray-500 hover:text-gray-700"> | |
| <i data-feather="x"></i> | |
| </button> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="reset-email" class="block text-sm font-medium text-gray-700 mb-1">Email Address</label> | |
| <input type="email" id="reset-email" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition"> | |
| </div> | |
| <button id="send-reset-link" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-blue-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 mb-4"> | |
| Send Reset Link | |
| </button> | |
| <div id="reset-success" class="hidden text-center text-green-600 text-sm mb-4"> | |
| Reset link has been sent to your email (simulation only) | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main App (hidden until login) --> | |
| <div id="app-container" class="hidden"> | |
| <!-- Dashboard Page --> | |
| <div id="dashboard-page" class="page"> | |
| <div class="mb-6"> | |
| <div class="flex items-center mb-4"> | |
| <div class="w-16 h-16 rounded-full bg-gray-200 overflow-hidden mr-4"> | |
| <img src="http://static.photos/people/200x200/1" alt="Student" class="w-full h-full object-cover"> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-semibold" id="student-name">John Doe</h1> | |
| <span id="status-badge" class="status-badge bg-blue-100 text-blue-800 inline-block">No active exam</span> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md p-6"> | |
| <h2 class="text-lg font-medium mb-4">Upcoming Exams</h2> | |
| <div class="space-y-3"> | |
| <div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg"> | |
| <div> | |
| <h3 class="font-medium">Math 101</h3> | |
| <p class="text-sm text-gray-600">Tomorrow, 10:00 AM</p> | |
| </div> | |
| <span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Upcoming</span> | |
| </div> | |
| <div class="flex justify-between items-center p-3 bg-gray-50 rounded-lg"> | |
| <div> | |
| <h3 class="font-medium">Science 201</h3> | |
| <p class="text-sm text-gray-600">Next week, 2:00 PM</p> | |
| </div> | |
| <span class="text-xs bg-gray-100 text-gray-800 px-2 py-1 rounded">Scheduled</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Exams Page --> | |
| <div id="exams-page" class="page"> | |
| <h1 class="text-xl font-semibold mb-6">Available Exams</h1> | |
| <div class="space-y-4" id="exams-list"> | |
| <!-- Exams will be populated via JS --> | |
| </div> | |
| </div> | |
| <!-- Take Exam Page --> | |
| <div id="take-exam-page" class="page"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h1 id="exam-title" class="text-xl font-semibold">Exam Title</h1> | |
| <div id="exam-timer" class="bg-gray-800 text-white px-3 py-1 rounded-lg text-sm">00:30:00</div> | |
| </div> | |
| <div class="webcam-container mb-4 relative"> | |
| <video id="webcam" autoplay muted playsinline class="w-full h-auto"></video> | |
| <div class="absolute bottom-2 left-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">Live Proctoring</div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md p-6 mb-4"> | |
| <div id="exam-questions"> | |
| <!-- Questions will be populated via JS --> | |
| </div> | |
| <div class="mt-6"> | |
| <button id="submit-exam-btn" class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-blue-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"> | |
| Submit Exam | |
| </button> | |
| </div> | |
| </div> | |
| <div id="warning-banner" class="hidden bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4 rounded-lg"> | |
| <p>Warning: Suspicious activity detected (tab change/fullscreen exit). This incident has been recorded.</p> | |
| </div> | |
| <div id="fullscreen-warning" class="hidden bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-4 rounded-lg"> | |
| <p>Please return to fullscreen mode to continue your exam.</p> | |
| </div> | |
| </div> | |
| <!-- Pending Page --> | |
| <div id="pending-page" class="page"> | |
| <h1 class="text-xl font-semibold mb-6">Submitted Exams</h1> | |
| <div class="space-y-4" id="pending-exams-list"> | |
| <!-- Submitted exams will be populated via JS --> | |
| </div> | |
| </div> | |
| <!-- Profile Page --> | |
| <div id="profile-page" class="page"> | |
| <div class="flex justify-center mb-6"> | |
| <div class="w-24 h-24 rounded-full bg-gray-200 overflow-hidden"> | |
| <img src="http://static.photos/people/200x200/1" alt="Profile" class="w-full h-full object-cover"> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl shadow-md p-6 mb-4"> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-500">Full Name</label> | |
| <p id="profile-name" class="text-gray-800 font-medium">John Doe</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-500">Student ID</label> | |
| <p id="profile-id" class="text-gray-800 font-medium">S123456</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-500">Email</label> | |
| <p id="profile-email" class="text-gray-800 font-medium">john.doe@university.edu</p> | |
| </div> | |
| </div> | |
| <button id="edit-profile-btn" class="w-full mt-6 bg-gray-100 text-gray-800 py-2 px-4 rounded-lg font-medium hover:bg-gray-200 transition duration-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50"> | |
| Edit Profile | |
| </button> | |
| <button id="logout-btn" class="w-full mt-4 bg-red-100 text-red-600 py-2 px-4 rounded-lg font-medium hover:bg-red-200 transition duration-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"> | |
| Logout | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Bottom Navigation --> | |
| <nav class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex justify-around py-2 px-4"> | |
| <button class="nav-tab active flex flex-col items-center py-2 px-4 text-xs" data-page="dashboard-page"> | |
| <i data-feather="home" class="mb-1"></i> | |
| <span>Home</span> | |
| </button> | |
| <button class="nav-tab flex flex-col items-center py-2 px-4 text-xs" data-page="exams-page"> | |
| <i data-feather="book" class="mb-1"></i> | |
| <span>Exams</span> | |
| </button> | |
| <button class="nav-tab flex flex-col items-center py-2 px-4 text-xs" data-page="take-exam-page"> | |
| <i data-feather="edit-3" class="mb-1"></i> | |
| <span>Take Exam</span> | |
| </button> | |
| <button class="nav-tab flex flex-col items-center py-2 px-4 text-xs" data-page="pending-page"> | |
| <i data-feather="clock" class="mb-1"></i> | |
| <span>Pending</span> | |
| </button> | |
| <button class="nav-tab flex flex-col items-center py-2 px-4 text-xs" data-page="profile-page"> | |
| <i data-feather="user" class="mb-1"></i> | |
| <span>Profile</span> | |
| </button> | |
| </nav> | |
| </div> | |
| <script> | |
| // App State | |
| const state = { | |
| currentUser: null, | |
| currentExam: null, | |
| examTimer: null, | |
| suspiciousActions: 0, | |
| webcamInterval: null, | |
| snapshots: [], | |
| examInProgress: false, | |
| isFullscreen: false | |
| }; | |
| // Sample Exams Data | |
| const exams = [ | |
| { | |
| id: 'exam-1', | |
| title: 'Mathematics Final Exam', | |
| subject: 'Math 101', | |
| date: '2023-06-15', | |
| time: '10:00 AM', | |
| duration: 30, // minutes | |
| questions: [ | |
| { | |
| id: 1, | |
| type: 'mcq', | |
| question: 'What is the derivative of x²?', | |
| options: ['x', '2x', 'x²', '2x²'], | |
| correctAnswer: 1 | |
| }, | |
| { | |
| id: 2, | |
| type: 'mcq', | |
| question: 'What is the value of π (pi) rounded to two decimal places?', | |
| options: ['3.14', '3.16', '3.12', '3.18'], | |
| correctAnswer: 0 | |
| }, | |
| { | |
| id: 3, | |
| type: 'short', | |
| question: 'Explain the Pythagorean theorem.', | |
| correctAnswer: 'In a right-angled triangle, the square of the hypotenuse is equal to the sum of the squares of the other two sides.' | |
| }, | |
| { | |
| id: 4, | |
| type: 'mcq', | |
| question: 'What is the solution to the equation 2x + 5 = 15?', | |
| options: ['x = 5', 'x = 10', 'x = 7.5', 'x = 3'], | |
| correctAnswer: 0 | |
| }, | |
| { | |
| id: 5, | |
| type: 'short', | |
| question: 'What is the quadratic formula?', | |
| correctAnswer: 'x = [-b ± √(b² - 4ac)] / 2a' | |
| } | |
| ] | |
| }, | |
| { | |
| id: 'exam-2', | |
| title: 'Science Midterm Exam', | |
| subject: 'Science 201', | |
| date: '2023-06-20', | |
| time: '02:00 PM', | |
| duration: 45, | |
| questions: [ | |
| { | |
| id: 1, | |
| type: 'mcq', | |
| question: 'What is the chemical symbol for gold?', | |
| options: ['Go', 'Gd', 'Au', 'Ag'], | |
| correctAnswer: 2 | |
| }, | |
| { | |
| id: 2, | |
| type: 'mcq', | |
| question: 'Which planet is known as the Red Planet?', | |
| options: ['Venus', 'Mars', 'Jupiter', 'Saturn'], | |
| correctAnswer: 1 | |
| }, | |
| { | |
| id: 3, | |
| type: 'short', | |
| question: 'What is Newton\'s First Law of Motion?', | |
| correctAnswer: 'An object in motion stays in motion unless acted upon by an external force.' | |
| } | |
| ] | |
| } | |
| ]; | |
| // DOM Elements | |
| const loginPage = document.getElementById('login-page'); | |
| const appContainer = document.getElementById('app-container'); | |
| const studentIdInput = document.getElementById('student-id'); | |
| const passwordInput = document.getElementById('password'); | |
| const loginBtn = document.getElementById('login-btn'); | |
| const forgotPasswordLink = document.getElementById('forgot-password-link'); | |
| const forgotPasswordModal = document.getElementById('forgot-password-modal'); | |
| const closeModalBtn = document.getElementById('close-modal'); | |
| const sendResetLinkBtn = document.getElementById('send-reset-link'); | |
| const resetSuccess = document.getElementById('reset-success'); | |
| const resetEmailInput = document.getElementById('reset-email'); | |
| const studentNameElement = document.getElementById('student-name'); | |
| const statusBadge = document.getElementById('status-badge'); | |
| const examsList = document.getElementById('exams-list'); | |
| const examTitle = document.getElementById('exam-title'); | |
| const examTimer = document.getElementById('exam-timer'); | |
| const examQuestions = document.getElementById('exam-questions'); | |
| const submitExamBtn = document.getElementById('submit-exam-btn'); | |
| const warningBanner = document.getElementById('warning-banner'); | |
| const fullscreenWarning = document.getElementById('fullscreen-warning'); | |
| const pendingExamsList = document.getElementById('pending-exams-list'); | |
| const profileName = document.getElementById('profile-name'); | |
| const profileId = document.getElementById('profile-id'); | |
| const profileEmail = document.getElementById('profile-email'); | |
| const editProfileBtn = document.getElementById('edit-profile-btn'); | |
| const logoutBtn = document.getElementById('logout-btn'); | |
| const webcamElement = document.getElementById('webcam'); | |
| const navTabs = document.querySelectorAll('.nav-tab'); | |
| const pages = document.querySelectorAll('.page'); | |
| // Initialize Feather Icons | |
| document.addEventListener('DOMContentLoaded', function() { | |
| feather.replace(); | |
| // Check if user is already logged in (from localStorage) | |
| const savedUser = localStorage.getItem('examGuardUser'); | |
| if (savedUser) { | |
| state.currentUser = JSON.parse(savedUser); | |
| login(); | |
| } | |
| // Load pending exams from localStorage | |
| loadPendingExams(); | |
| }); | |
| // Event Listeners | |
| loginBtn.addEventListener('click', handleLogin); | |
| forgotPasswordLink.addEventListener('click', showForgotPasswordModal); | |
| closeModalBtn.addEventListener('click', closeForgotPasswordModal); | |
| sendResetLinkBtn.addEventListener('click', sendResetLink); | |
| submitExamBtn.addEventListener('click', submitExam); | |
| editProfileBtn.addEventListener('click', editProfile); | |
| logoutBtn.addEventListener('click', logout); | |
| // Navigation tabs | |
| navTabs.forEach(tab => { | |
| tab.addEventListener('click', function() { | |
| const pageId = this.getAttribute('data-page'); | |
| showPage(pageId); | |
| // Update active tab | |
| navTabs.forEach(t => t.classList.remove('active')); | |
| this.classList.add('active'); | |
| }); | |
| }); | |
| // Visibility change detection (anti-cheat) | |
| document.addEventListener('visibilitychange', function() { | |
| if (state.examInProgress && document.visibilityState === 'hidden') { | |
| handleSuspiciousActivity('Tab change detected'); | |
| } | |
| }); | |
| // Fullscreen change detection | |
| document.addEventListener('fullscreenchange', function() { | |
| state.isFullscreen = !!document.fullscreenElement; | |
| if (state.examInProgress && !state.isFullscreen) { | |
| handleSuspiciousActivity('Fullscreen exit detected'); | |
| fullscreenWarning.classList.remove('hidden'); | |
| } else { | |
| fullscreenWarning.classList.add('hidden'); | |
| } | |
| }); | |
| // Right click prevention | |
| document.addEventListener('contextmenu', function(e) { | |
| if (state.examInProgress) { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Right click attempted'); | |
| } | |
| }); | |
| // Text selection prevention | |
| document.addEventListener('selectstart', function(e) { | |
| if (state.examInProgress) { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Text selection attempted'); | |
| } | |
| }); | |
| // Copy/paste prevention | |
| document.addEventListener('copy', function(e) { | |
| if (state.examInProgress) { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Copy attempted'); | |
| } | |
| }); | |
| document.addEventListener('paste', function(e) { | |
| if (state.examInProgress) { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Paste attempted'); | |
| } | |
| }); | |
| // Functions | |
| function handleLogin() { | |
| const studentId = studentIdInput.value.trim(); | |
| const password = passwordInput.value.trim(); | |
| if (!studentId || !password) { | |
| alert('Please enter both student ID and password'); | |
| return; | |
| } | |
| // Mock login - accept any credentials | |
| state.currentUser = { | |
| id: studentId, | |
| name: studentId === 'admin' ? 'Admin User' : `Student ${studentId}`, | |
| email: `${studentId}@university.edu` | |
| }; | |
| // Save to localStorage | |
| localStorage.setItem('examGuardUser', JSON.stringify(state.currentUser)); | |
| login(); | |
| } | |
| function login() { | |
| // Hide login page, show app | |
| loginPage.classList.remove('active'); | |
| appContainer.classList.remove('hidden'); | |
| // Update user info | |
| studentNameElement.textContent = state.currentUser.name; | |
| profileName.textContent = state.currentUser.name; | |
| profileId.textContent = state.currentUser.id; | |
| profileEmail.textContent = state.currentUser.email; | |
| // Show dashboard by default | |
| showPage('dashboard-page'); | |
| // Populate exams list | |
| renderExamsList(); | |
| } | |
| function logout() { | |
| // Clear user session | |
| state.currentUser = null; | |
| localStorage.removeItem('examGuardUser'); | |
| // Reset form | |
| studentIdInput.value = ''; | |
| passwordInput.value = ''; | |
| // Hide app, show login page | |
| appContainer.classList.add('hidden'); | |
| loginPage.classList.add('active'); | |
| // Reset active tab | |
| navTabs.forEach(tab => tab.classList.remove('active')); | |
| navTabs[0].classList.add('active'); | |
| // Stop any ongoing exam | |
| stopExam(); | |
| } | |
| function showForgotPasswordModal() { | |
| forgotPasswordModal.style.display = 'flex'; | |
| } | |
| function closeForgotPasswordModal() { | |
| forgotPasswordModal.style.display = 'none'; | |
| resetSuccess.classList.add('hidden'); | |
| } | |
| function sendResetLink() { | |
| const email = resetEmailInput.value.trim(); | |
| if (!email) { | |
| alert('Please enter your email address'); | |
| return; | |
| } | |
| // Simulate sending reset link | |
| resetSuccess.classList.remove('hidden'); | |
| } | |
| function showPage(pageId) { | |
| pages.forEach(page => page.classList.remove('active')); | |
| document.getElementById(pageId).classList.add('active'); | |
| // Special handling for certain pages | |
| if (pageId === 'exams-page') { | |
| renderExamsList(); | |
| } else if (pageId === 'pending-page') { | |
| loadPendingExams(); | |
| } | |
| } | |
| function renderExamsList() { | |
| examsList.innerHTML = ''; | |
| exams.forEach(exam => { | |
| const examCard = document.createElement('div'); | |
| examCard.className = 'exam-card bg-white rounded-xl shadow-md overflow-hidden'; | |
| const isPending = checkIfExamPending(exam.id); | |
| examCard.innerHTML = ` | |
| <div class="p-6"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <h2 class="text-lg font-semibold">${exam.subject}</h2> | |
| ${isPending ? '<span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">Submitted</span>' : ''} | |
| </div> | |
| <p class="text-sm text-gray-600 mb-4">${exam.date} • ${exam.time}</p> | |
| <button data-exam-id="${exam.id}" class="take-exam-btn w-full bg-blue-600 text-white py-2 px-4 rounded-lg font-medium hover:bg-blue-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 ${isPending ? 'opacity-50 cursor-not-allowed' : ''}" ${isPending ? 'disabled' : ''}> | |
| ${isPending ? 'Already Submitted' : 'Take Exam'} | |
| </button> | |
| </div> | |
| `; | |
| examsList.appendChild(examCard); | |
| }); | |
| // Add event listeners to take exam buttons | |
| document.querySelectorAll('.take-exam-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| if (this.disabled) return; | |
| const examId = this.getAttribute('data-exam-id'); | |
| startExam(examId); | |
| }); | |
| }); | |
| } | |
| function checkIfExamPending(examId) { | |
| const pendingExams = JSON.parse(localStorage.getItem('pendingExams') || '[]'); | |
| return pendingExams.some(exam => exam.examId === examId); | |
| } | |
| function startExam(examId) { | |
| // Find the exam | |
| const exam = exams.find(e => e.id === examId); | |
| if (!exam) return; | |
| state.currentExam = exam; | |
| state.examInProgress = true; | |
| state.suspiciousActions = 0; | |
| state.snapshots = []; | |
| // Update UI | |
| examTitle.textContent = exam.title; | |
| updateExamTimer(exam.duration * 60); // Convert minutes to seconds | |
| // Render questions | |
| renderExamQuestions(exam.questions); | |
| // Start webcam | |
| startWebcam(); | |
| // Start snapshot interval | |
| state.webcamInterval = setInterval(takeSnapshot, 30000); // Every 30 seconds | |
| // Request fullscreen | |
| requestFullscreen(); | |
| // Show take exam page | |
| showPage('take-exam-page'); | |
| // Start timer | |
| let timeLeft = exam.duration * 60; // in seconds | |
| state.examTimer = setInterval(() => { | |
| timeLeft--; | |
| updateExamTimer(timeLeft); | |
| if (timeLeft <= 0) { | |
| submitExam(); | |
| } | |
| }, 1000); | |
| // Update status badge | |
| statusBadge.textContent = 'Exam in progress'; | |
| statusBadge.className = 'status-badge bg-green-100 text-green-800 inline-block'; | |
| } | |
| function stopExam() { | |
| if (state.examTimer) { | |
| clearInterval(state.examTimer); | |
| state.examTimer = null; | |
| } | |
| if (state.webcamInterval) { | |
| clearInterval(state.webcamInterval); | |
| state.webcamInterval = null; | |
| } | |
| // Stop webcam | |
| stopWebcam(); | |
| state.examInProgress = false; | |
| state.currentExam = null; | |
| // Reset status badge | |
| statusBadge.textContent = 'No active exam'; | |
| statusBadge.className = 'status-badge bg-blue-100 text-blue-800 inline-block'; | |
| // Hide warnings | |
| warningBanner.classList.add('hidden'); | |
| fullscreenWarning.classList.add('hidden'); | |
| } | |
| function updateExamTimer(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| examTimer.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| // Change color when time is running out | |
| if (seconds <= 300) { // 5 minutes | |
| examTimer.className = 'bg-red-600 text-white px-3 py-1 rounded-lg text-sm'; | |
| } else { | |
| examTimer.className = 'bg-gray-800 text-white px-3 py-1 rounded-lg text-sm'; | |
| } | |
| } | |
| function renderExamQuestions(questions) { | |
| examQuestions.innerHTML = ''; | |
| questions.forEach((q, index) => { | |
| const questionElement = document.createElement('div'); | |
| questionElement.className = 'mb-6'; | |
| if (q.type === 'mcq') { | |
| questionElement.innerHTML = ` | |
| <h3 class="font-medium mb-2">${index + 1}. ${q.question}</h3> | |
| <div class="space-y-2"> | |
| ${q.options.map((option, i) => ` | |
| <div class="question-option p-3 border border-gray-300 rounded-lg cursor-pointer" data-question-id="${q.id}" data-option-id="${i}"> | |
| ${option} | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } else { | |
| questionElement.innerHTML = ` | |
| <h3 class="font-medium mb-2">${index + 1}. ${q.question}</h3> | |
| <textarea class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition" data-question-id="${q.id}" rows="3" placeholder="Type your answer here..."></textarea> | |
| `; | |
| } | |
| examQuestions.appendChild(questionElement); | |
| }); | |
| // Add event listeners to MCQ options | |
| document.querySelectorAll('.question-option').forEach(option => { | |
| option.addEventListener('click', function() { | |
| // Deselect all options for this question first | |
| const questionId = this.getAttribute('data-question-id'); | |
| document.querySelectorAll(`.question-option[data-question-id="${questionId}"]`).forEach(el => { | |
| el.classList.remove('selected'); | |
| }); | |
| // Select clicked option | |
| this.classList.add('selected'); | |
| }); | |
| }); | |
| } | |
| function startWebcam() { | |
| navigator.mediaDevices.getUserMedia({ video: true }) | |
| .then(stream => { | |
| webcamElement.srcObject = stream; | |
| }) | |
| .catch(err => { | |
| console.error('Error accessing webcam:', err); | |
| }); | |
| } | |
| function stopWebcam() { | |
| if (webcamElement.srcObject) { | |
| webcamElement.srcObject.getTracks().forEach(track => track.stop()); | |
| webcamElement.srcObject = null; | |
| } | |
| } | |
| function takeSnapshot() { | |
| // In a real app, this would capture and upload the webcam frame | |
| // For this demo, we'll just store a fake snapshot | |
| state.snapshots.push({ | |
| timestamp: new Date().toISOString(), | |
| suspicious: state.suspiciousActions > 0 | |
| }); | |
| console.log('Snapshot taken at', new Date().toISOString()); | |
| } | |
| function requestFullscreen() { | |
| if (!document.fullscreenElement) { | |
| document.documentElement.requestFullscreen().catch(err => { | |
| console.error('Error attempting to enable fullscreen:', err); | |
| }); | |
| } | |
| } | |
| function handleSuspiciousActivity(reason) { | |
| console.log('Suspicious activity:', reason); | |
| state.suspiciousActions++; | |
| // Show warning banner | |
| warningBanner.classList.remove('hidden'); | |
| // Hide after 5 seconds | |
| setTimeout(() => { | |
| warningBanner.classList.add('hidden'); | |
| }, 5000); | |
| } | |
| function submitExam() { | |
| if (!state.currentExam) return; | |
| // Collect answers | |
| const answers = []; | |
| // Multiple choice answers | |
| document.querySelectorAll('.question-option.selected').forEach(option => { | |
| answers.push({ | |
| questionId: parseInt(option.getAttribute('data-question-id')), | |
| answer: option.getAttribute('data-option-id') | |
| }); | |
| }); | |
| // Short answer responses | |
| document.querySelectorAll('textarea').forEach(textarea => { | |
| const answer = textarea.value.trim(); | |
| if (answer) { | |
| answers.push({ | |
| questionId: parseInt(textarea.getAttribute('data-question-id')), | |
| answer: answer | |
| }); | |
| } | |
| }); | |
| // Create exam result | |
| const examResult = { | |
| examId: state.currentExam.id, | |
| title: state.currentExam.title, | |
| subject: state.currentExam.subject, | |
| date: new Date().toISOString(), | |
| status: 'pending', | |
| suspiciousCount: state.suspiciousActions, | |
| snapshots: state.snapshots, | |
| answers: answers | |
| }; | |
| // Save to localStorage | |
| const pendingExams = JSON.parse(localStorage.getItem('pendingExams') || '[]'); | |
| pendingExams.push(examResult); | |
| localStorage.setItem('pendingExams', JSON.stringify(pendingExams)); | |
| // Stop exam | |
| stopExam(); | |
| // Show pending page | |
| showPage('pending-page'); | |
| } | |
| function loadPendingExams() { | |
| const pendingExams = JSON.parse(localStorage.getItem('pendingExams') || '[]'); | |
| pendingExamsList.innerHTML = ''; | |
| if (pendingExams.length === 0) { | |
| pendingExamsList.innerHTML = '<p class="text-gray-500 text-center py-4">No submitted exams yet</p>'; | |
| return; | |
| } | |
| pendingExams.forEach((exam, index) => { | |
| const examElement = document.createElement('div'); | |
| examElement.className = 'bg-white rounded-xl shadow-md overflow-hidden relative'; | |
| // Format date | |
| const examDate = new Date(exam.date); | |
| const formattedDate = examDate.toLocaleDateString('en-US', { | |
| month: 'short', | |
| day: 'numeric', | |
| year: 'numeric' | |
| }); | |
| examElement.innerHTML = ` | |
| <div class="p-6"> | |
| <h2 class="text-lg font-semibold mb-1">${exam.subject}</h2> | |
| <p class="text-sm text-gray-600 mb-2">Submitted on ${formattedDate}</p> | |
| <p class="text-sm mb-2">Status: <span class="font-medium">${exam.status === 'pending' ? 'Pending Review' : exam.status}</span></p> | |
| ${exam.suspiciousCount > 0 ? ` | |
| <div class="flex items-center text-sm mb-2"> | |
| <span class="text-red-600">${exam.suspiciousCount} suspicious actions detected</span> | |
| ${exam.suspiciousCount > 3 ? '<i data-feather="alert-triangle" class="ml-1 text-red-600 w-4 h-4"></i>' : ''} | |
| </div> | |
| ` : '<p class="text-sm text-green-600 mb-2">No suspicious activity detected</p>'} | |
| <div class="flex gap-2 overflow-x-auto py-2"> | |
| ${exam.snapshots.map((snapshot, i) => ` | |
| <div class="relative"> | |
| <div class="w-16 h-16 bg-gray-200 rounded-lg flex items-center justify-center text-xs text-gray-500"> | |
| Snapshot ${i + 1} | |
| </div> | |
| ${snapshot.suspicious ? '<div class="suspicious-count">!</div>' : ''} | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| `; | |
| pendingExamsList.appendChild(examElement); | |
| }); | |
| feather.replace(); | |
| } | |
| function editProfile() { | |
| const newName = prompt('Enter your new name:', state.currentUser.name); | |
| if (newName && newName.trim() !== '') { | |
| state.currentUser.name = newName.trim(); | |
| studentNameElement.textContent = state.currentUser.name; | |
| profileName.textContent = state.currentUser.name; | |
| localStorage.setItem('examGuardUser', JSON.stringify(state.currentUser)); | |
| } | |
| } | |
| </script> | |
| <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script> | |
| </body> | |
| </html> |