Spaces:
Running
Running
| // App State | |
| const state = { | |
| isLoggedIn: false, | |
| currentStudent: null, | |
| exams: [ | |
| { | |
| id: 1, | |
| subject: "Mathematics", | |
| date: "2023-06-15", | |
| duration: 30, // minutes | |
| questions: [ | |
| { | |
| id: 1, | |
| type: "multiple", | |
| text: "What is the derivative of x²?", | |
| options: ["x", "2x", "x²", "2x²"], | |
| answer: 1 | |
| }, | |
| { | |
| id: 2, | |
| type: "multiple", | |
| text: "What is the value of π (pi) to two decimal places?", | |
| options: ["3.14", "3.16", "3.12", "3.18"], | |
| answer: 0 | |
| }, | |
| { | |
| id: 3, | |
| type: "short", | |
| text: "Solve for x: 2x + 5 = 15", | |
| answer: "5" | |
| }, | |
| { | |
| id: 4, | |
| type: "multiple", | |
| text: "Which of these is a prime number?", | |
| options: ["4", "9", "11", "15"], | |
| answer: 2 | |
| }, | |
| { | |
| id: 5, | |
| type: "short", | |
| text: "What is the area of a circle with radius 3? (Use π ≈ 3.14)", | |
| answer: "28.26" | |
| } | |
| ] | |
| }, | |
| { | |
| id: 2, | |
| subject: "Physics", | |
| date: "2023-06-20", | |
| duration: 45, | |
| questions: [ | |
| { | |
| id: 1, | |
| type: "multiple", | |
| text: "What is the SI unit of force?", | |
| options: ["Joule", "Watt", "Newton", "Pascal"], | |
| answer: 2 | |
| }, | |
| { | |
| id: 2, | |
| type: "short", | |
| text: "What is the acceleration due to gravity on Earth (in m/s²)?", | |
| answer: "9.8" | |
| } | |
| ] | |
| } | |
| ], | |
| currentExam: null, | |
| examStartTime: null, | |
| examTimer: null, | |
| suspiciousActivities: 0, | |
| submissions: [] | |
| }; | |
| // DOM Elements | |
| const loginScreen = document.getElementById('login-screen'); | |
| const appScreens = document.getElementById('app-screens'); | |
| const loginForm = document.getElementById('login-form'); | |
| const forgotPasswordBtn = document.getElementById('forgot-password'); | |
| const forgotModal = document.getElementById('forgot-modal'); | |
| const resetConfirmation = document.getElementById('reset-confirmation'); | |
| const studentName = document.getElementById('student-name'); | |
| const studentAvatar = document.getElementById('student-avatar'); | |
| const statusBadge = document.getElementById('status-badge'); | |
| const examList = document.getElementById('exam-list'); | |
| const examScreen = document.getElementById('exam-screen'); | |
| const examQuestions = document.getElementById('exam-questions'); | |
| const examTitle = document.getElementById('exam-title'); | |
| const examTimer = document.getElementById('exam-timer'); | |
| const examProgress = document.getElementById('exam-progress'); | |
| const submitExamBtn = document.getElementById('submit-exam'); | |
| const submissionList = document.getElementById('submission-list'); | |
| const logoutBtn = document.getElementById('logout-btn'); | |
| const navButtons = document.querySelectorAll('.nav-btn'); | |
| // Initialize the app | |
| function init() { | |
| checkAuth(); | |
| setupEventListeners(); | |
| renderExamList(); | |
| loadSubmissions(); | |
| } | |
| // Check if user is logged in | |
| function checkAuth() { | |
| const student = localStorage.getItem('examGuardStudent'); | |
| if (student) { | |
| state.isLoggedIn = true; | |
| state.currentStudent = JSON.parse(student); | |
| showApp(); | |
| } else { | |
| showLogin(); | |
| } | |
| } | |
| // Show login screen | |
| function showLogin() { | |
| loginScreen.classList.remove('hidden'); | |
| appScreens.classList.add('hidden'); | |
| state.isLoggedIn = false; | |
| } | |
| // Show main app | |
| function showApp() { | |
| loginScreen.classList.add('hidden'); | |
| appScreens.classList.remove('hidden'); | |
| // Update student info | |
| studentName.textContent = state.currentStudent?.name || 'Student'; | |
| studentAvatar.src = `https://static.photos/people/120x120/${state.currentStudent?.id || 1}`; | |
| // Show dashboard by default | |
| showScreen('dashboard'); | |
| updateStatusBadge(); | |
| } | |
| // Show a specific screen | |
| function showScreen(screenId) { | |
| // Hide all screens | |
| document.querySelectorAll('[id$="-screen"]').forEach(screen => { | |
| screen.classList.add('hidden'); | |
| }); | |
| // Show requested screen | |
| document.getElementById(`${screenId}-screen`)?.classList.remove('hidden'); | |
| // Update active nav button | |
| navButtons.forEach(btn => { | |
| if (btn.dataset.screen === screenId) { | |
| btn.classList.add('text-indigo-600'); | |
| btn.classList.remove('text-gray-500'); | |
| } else { | |
| btn.classList.remove('text-indigo-600'); | |
| btn.classList.add('text-gray-500'); | |
| } | |
| }); | |
| } | |
| // Update status badge | |
| function updateStatusBadge() { | |
| const pendingExams = state.exams.filter(exam => | |
| !state.submissions.some(sub => sub.examId === exam.id) | |
| ); | |
| if (pendingExams.length > 0) { | |
| statusBadge.textContent = `${pendingExams.length} pending exam${pendingExams.length > 1 ? 's' : ''}`; | |
| statusBadge.className = 'text-xs px-2 py-1 bg-amber-100 text-amber-800 rounded-full'; | |
| } else { | |
| statusBadge.textContent = 'No active exams'; | |
| statusBadge.className = 'text-xs px-2 py-1 bg-emerald-100 text-emerald-800 rounded-full'; | |
| } | |
| } | |
| // Render exam list | |
| function renderExamList() { | |
| examList.innerHTML = ''; | |
| state.exams.forEach(exam => { | |
| const isSubmitted = state.submissions.some(sub => sub.examId === exam.id); | |
| const examCard = document.createElement('div'); | |
| examCard.className = 'bg-white rounded-xl shadow-sm p-4'; | |
| examCard.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <h3 class="font-medium text-gray-800">${exam.subject}</h3> | |
| <p class="text-sm text-gray-500">${new Date(exam.date).toLocaleDateString()} • ${exam.duration} min</p> | |
| </div> | |
| ${isSubmitted ? | |
| '<span class="text-xs px-2 py-1 bg-green-100 text-green-800 rounded-full">Submitted</span>' : | |
| `<button data-exam-id="${exam.id}" class="take-exam-btn text-sm bg-indigo-600 text-white px-3 py-1 rounded-lg hover:bg-indigo-700"> | |
| Take Exam | |
| </button>` | |
| } | |
| </div> | |
| `; | |
| examList.appendChild(examCard); | |
| }); | |
| // Add event listeners to take exam buttons | |
| document.querySelectorAll('.take-exam-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const examId = parseInt(btn.dataset.examId); | |
| startExam(examId); | |
| }); | |
| }); | |
| } | |
| // Start an exam | |
| function startExam(examId) { | |
| const exam = state.exams.find(e => e.id === examId); | |
| if (!exam) return; | |
| state.currentExam = exam; | |
| state.currentQuestionIndex = 0; | |
| state.examStartTime = new Date(); | |
| state.suspiciousActivities = 0; | |
| // Setup exam UI | |
| examTitle.textContent = exam.subject; | |
| examQuestions.innerHTML = ''; | |
| // Render first question | |
| renderQuestion(0); | |
| const questionCard = document.createElement('div'); | |
| questionCard.className = 'question-card bg-white rounded-xl shadow-sm p-4'; | |
| if (q.type === 'multiple') { | |
| questionCard.innerHTML = ` | |
| <h4 class="font-medium mb-3">${index + 1}. ${q.text}</h4> | |
| <div class="space-y-2"> | |
| ${q.options.map((opt, i) => ` | |
| <label class="flex items-center space-x-3 cursor-pointer"> | |
| <input type="radio" name="q${q.id}" value="${i}" class="w-4 h-4 text-indigo-600"> | |
| <span>${opt}</span> | |
| </label> | |
| `).join('')} | |
| </div> | |
| `; | |
| } else { | |
| questionCard.innerHTML = ` | |
| <h4 class="font-medium mb-3">${index + 1}. ${q.text}</h4> | |
| <input type="text" name="q${q.id}" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg"> | |
| `; | |
| } | |
| // Add event listeners for navigation | |
| document.getElementById('prev-question').addEventListener('click', () => { | |
| if (state.currentQuestionIndex > 0) { | |
| state.currentQuestionIndex--; | |
| renderQuestion(state.currentQuestionIndex); | |
| } | |
| }); | |
| document.getElementById('next-question').addEventListener('click', () => { | |
| if (state.currentQuestionIndex < state.currentExam.questions.length - 1) { | |
| state.currentQuestionIndex++; | |
| renderQuestion(state.currentQuestionIndex); | |
| } else { | |
| submitExam(); | |
| } | |
| }); | |
| // Render a single question | |
| function renderQuestion(index) { | |
| const question = state.currentExam.questions[index]; | |
| examQuestions.innerHTML = ''; | |
| const questionCard = document.createElement('div'); | |
| questionCard.className = 'question-card'; | |
| if (question.type === 'multiple') { | |
| questionCard.innerHTML = ` | |
| <h4 class="font-medium mb-3">${index + 1}. ${question.text}</h4> | |
| <div class="space-y-2"> | |
| ${question.options.map((opt, i) => ` | |
| <label class="flex items-center space-x-3 cursor-pointer"> | |
| <input type="radio" name="q${question.id}" value="${i}" class="w-4 h-4 text-indigo-600"> | |
| <span>${opt}</span> | |
| </label> | |
| `).join('')} | |
| </div> | |
| `; | |
| } else { | |
| questionCard.innerHTML = ` | |
| <h4 class="font-medium mb-3">${index + 1}. ${question.text}</h4> | |
| <input type="text" name="q${question.id}" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg"> | |
| `; | |
| } | |
| examQuestions.appendChild(questionCard); | |
| // Update navigation buttons | |
| const prevBtn = document.getElementById('prev-question'); | |
| const nextBtn = document.getElementById('next-question'); | |
| prevBtn.classList.toggle('hidden', index === 0); | |
| nextBtn.textContent = index === state.currentExam.questions.length - 1 ? 'Submit Exam' : 'Next Question'; | |
| } | |
| // Start timer | |
| const durationInSeconds = exam.duration * 60; | |
| let remainingTime = durationInSeconds; | |
| updateTimerDisplay(remainingTime); | |
| state.examTimer = setInterval(() => { | |
| remainingTime--; | |
| updateTimerDisplay(remainingTime); | |
| if (remainingTime <= 0) { | |
| clearInterval(state.examTimer); | |
| submitExam(); | |
| } | |
| }, 1000); | |
| // Setup anti-cheat | |
| setupAntiCheat(); | |
| // Show exam screen | |
| showScreen('exam'); | |
| } | |
| // Update timer display | |
| function updateTimerDisplay(seconds) { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| examTimer.textContent = `${mins}:${secs < 10 ? '0' + secs : secs}`; | |
| if (seconds < 60) { | |
| examTimer.classList.add('timer-urgent'); | |
| } else { | |
| examTimer.classList.remove('timer-urgent'); | |
| } | |
| // Update progress bar | |
| const examDuration = state.currentExam.duration * 60; | |
| const percent = (seconds / examDuration) * 100; | |
| examProgress.style.width = `${percent}%`; | |
| } | |
| // Setup anti-cheat measures | |
| function setupAntiCheat() { | |
| // Add exam layout class | |
| document.getElementById('app').classList.add('exam-layout'); | |
| // Detect tab switching | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.visibilityState !== 'visible') { | |
| handleSuspiciousActivity('Tab switched'); | |
| } | |
| }); | |
| // Disable right click | |
| document.addEventListener('contextmenu', (e) => { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Right click attempt'); | |
| }); | |
| // Disable text selection | |
| document.addEventListener('selectstart', (e) => { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Text selection attempt'); | |
| }); | |
| // Disable copy/paste | |
| document.addEventListener('copy', (e) => { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Copy attempt'); | |
| }); | |
| document.addEventListener('paste', (e) => { | |
| e.preventDefault(); | |
| handleSuspiciousActivity('Paste attempt'); | |
| }); | |
| // Try to access camera | |
| if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { | |
| navigator.mediaDevices.getUserMedia({ video: true }) | |
| .then(stream => { | |
| // Camera access granted | |
| console.log('Camera access granted for monitoring'); | |
| }) | |
| .catch(err => { | |
| console.log('Camera access denied or not available'); | |
| }); | |
| } | |
| } | |
| // Handle suspicious activity | |
| function handleSuspiciousActivity(reason) { | |
| state.suspiciousActivities++; | |
| // Show warning | |
| const warning = document.createElement('div'); | |
| warning.className = 'warning-flash fixed top-4 left-1/2 transform -translate-x-1/2 bg-white shadow-md rounded-lg px-4 py-2 z-50'; | |
| warning.textContent = `⚠️ Suspicious activity detected: ${reason}`; | |
| document.body.appendChild(warning); | |
| setTimeout(() => { | |
| warning.remove(); | |
| }, 3000); | |
| } | |
| // Submit exam | |
| function submitExam() { | |
| // Remove exam layout class | |
| document.getElementById('app').classList.remove('exam-layout'); | |
| if (!state.currentExam) return; | |
| clearInterval(state.examTimer); | |
| // Create submission | |
| const submission = { | |
| examId: state.currentExam.id, | |
| subject: state.currentExam.subject, | |
| submittedAt: new Date().toISOString(), | |
| suspiciousCount: state.suspiciousActivities | |
| }; | |
| // Save to state and localStorage | |
| state.submissions.push(submission); | |
| localStorage.setItem('examGuardSubmissions', JSON.stringify(state.submissions)); | |
| // Update UI | |
| updateStatusBadge(); | |
| renderExamList(); | |
| loadSubmissions(); | |
| // Show pending screen | |
| showScreen('pending'); | |
| } | |
| // Load submissions from localStorage | |
| function loadSubmissions() { | |
| const savedSubmissions = localStorage.getItem('examGuardSubmissions'); | |
| if (savedSubmissions) { | |
| state.submissions = JSON.parse(savedSubmissions); | |
| } | |
| renderSubmissions(); | |
| } | |
| // Render submissions list | |
| function renderSubmissions() { | |
| submissionList.innerHTML = ''; | |
| if (state.submissions.length === 0) { | |
| submissionList.innerHTML = '<p class="text-gray-500 text-center py-4">No submissions yet</p>'; | |
| return; | |
| } | |
| state.submissions.forEach(sub => { | |
| const subCard = document.createElement('div'); | |
| subCard.className = 'bg-white rounded-xl shadow-sm p-4'; | |
| subCard.innerHTML = ` | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <h3 class="font-medium text-gray-800">${sub.subject}</h3> | |
| <p class="text-sm text-gray-500">Submitted: ${new Date(sub.submittedAt).toLocaleString()}</p> | |
| </div> | |
| <div class="text-right"> | |
| <span class="block text-xs px-2 py-1 bg-blue-100 text-blue-800 rounded-full mb-1">Pending Review</span> | |
| ${sub.suspiciousCount > 0 ? | |
| `<span class="text-xs px-2 py-1 bg-red-100 text-red-800 rounded-full"> | |
| ${sub.suspiciousCount} suspicious event${sub.suspiciousCount > 1 ? 's' : ''} | |
| </span>` : | |
| `<span class="text-xs px-2 py-1 bg-green-100 text-green-800 rounded-full"> | |
| No issues detected | |
| </span>` | |
| } | |
| </div> | |
| </div> | |
| `; | |
| submissionList.appendChild(subCard); | |
| }); | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| // Login form | |
| loginForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const studentId = document.getElementById('student-id').value; | |
| const password = document.getElementById('password').value; | |
| // Mock authentication | |
| state.currentStudent = { | |
| id: studentId, | |
| name: `Student ${studentId}`, | |
| email: `student${studentId}@example.com` | |
| }; | |
| // Save to localStorage | |
| localStorage.setItem('examGuardStudent', JSON.stringify(state.currentStudent)); | |
| // Show app | |
| showApp(); | |
| }); | |
| // Forgot password | |
| forgotPasswordBtn.addEventListener('click', () => { | |
| forgotModal.classList.remove('hidden'); | |
| }); | |
| // Cancel reset | |
| document.getElementById('cancel-reset').addEventListener('click', () => { | |
| forgotModal.classList.add('hidden'); | |
| }); | |
| // Submit reset | |
| document.getElementById('submit-reset').addEventListener('click', () => { | |
| const email = document.getElementById('reset-email').value; | |
| if (email) { | |
| forgotModal.classList.add('hidden'); | |
| resetConfirmation.classList.remove('hidden'); | |
| } | |
| }); | |
| // Close confirmation | |
| document.getElementById('close-confirmation').addEventListener('click', () => { | |
| resetConfirmation.classList.add('hidden'); | |
| }); | |
| // Navigation buttons | |
| navButtons.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| showScreen(btn.dataset.screen); | |
| }); | |
| }); | |
| // Submit exam button | |
| submitExamBtn.addEventListener('click', submitExam); | |
| // Logout button | |
| logoutBtn.addEventListener('click', () => { | |
| localStorage.removeItem('examGuardStudent'); | |
| showLogin(); | |
| }); | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', init); |