| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Register - Attendr</title> |
| <link rel="stylesheet" href="style.css"> |
| <link |
| href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@700;800&display=swap" |
| rel="stylesheet"> |
| </head> |
|
|
| <body> |
| |
| |
| {% include '_navbar.html' %} |
|
|
| |
| <section class="section"> |
| <div class="container" style="max-width: 800px;"> |
| <h1 class="text-center mb-3">Register Account</h1> |
| <p class="text-center text-muted mb-3"> |
| Create your student or lecturer account |
| </p> |
|
|
| |
| <div id="globalStatusMessage" class="mb-2"></div> |
|
|
| |
| <div id="step1Card" class="glass-card"> |
| <h2 class="mb-2">Step 1: Account Information</h2> |
|
|
| <div class="form-group"> |
| <label class="form-label">I am a...</label> |
| <select id="roleSelect" class="form-select"> |
| <option value="">Select your role</option> |
| <option value="student">Student</option> |
| <option value="lecturer">Lecturer</option> |
| </select> |
| </div> |
|
|
| <div class="form-group"> |
| <label class="form-label">Matric Number</label> |
| <input type="text" id="matricNo" class="form-input" placeholder="e.g., A20EC0001 or L001" |
| style="text-transform: uppercase;"> |
| <small class="text-muted" id="matricHelp" style="font-size: 0.875rem;"> |
| Select your role first |
| </small> |
| </div> |
|
|
| <div class="form-group"> |
| <label class="form-label">Full Name</label> |
| <input type="text" id="fullName" class="form-input" placeholder="Your full name"> |
| </div> |
|
|
| <div class="form-group"> |
| <label class="form-label">Email (Optional)</label> |
| <input type="email" id="email" class="form-input" placeholder="your.email@graduate.utm.my"> |
| </div> |
|
|
| <div class="form-group"> |
| <label class="form-label">Password</label> |
| <input type="password" id="password" class="form-input" placeholder="Create a strong password"> |
| </div> |
|
|
| <div class="form-group"> |
| <label class="form-label">Confirm Password</label> |
| <input type="password" id="confirmPassword" class="form-input" placeholder="Re-enter your password"> |
| </div> |
|
|
| <div id="statusMessage" class="mt-2" style="display:none;"></div> |
|
|
| <button id="nextBtn" class="btn btn-primary mt-2" style="width: 100%;"> |
| Next: Face Registration → |
| </button> |
|
|
| <div class="text-center mt-2"> |
| <p class="text-muted"> |
| Already have an account? <a href="/login" style="color: var(--color-primary);">Login here</a> |
| </p> |
| </div> |
| </div> |
|
|
| |
| <div id="step2Card" class="glass-card mt-2 hidden"> |
| <h2 class="mb-2">Step 2: Face Registration</h2> |
| <p class="text-muted mb-2">Capture your face for biometric verification</p> |
|
|
| <div class="text-center"> |
| <video id="video" width="400" height="300" autoplay |
| style="border-radius: 12px; border: 2px solid var(--color-primary);"></video> |
| <canvas id="canvas" width="400" height="300" style="display: none;"></canvas> |
|
|
| <div id="preview" class="mt-2 hidden"> |
| <img id="capturedImage" |
| style="max-width: 400px; border-radius: 12px; border: 2px solid var(--color-success);"> |
| </div> |
| </div> |
|
|
| <div id="faceStatusMessage" class="mt-2" style="display:none;"></div> |
|
|
| <div class="flex-center gap-2 mt-2"> |
| <button id="startCameraBtn" class="btn btn-secondary"> |
| 📷 Start Camera |
| </button> |
| <button id="captureBtn" class="btn btn-primary hidden"> |
| 📸 Capture Face |
| </button> |
| <button id="retakeBtn" class="btn btn-outline hidden"> |
| 🔄 Retake |
| </button> |
| <button id="registerBtn" class="btn btn-success hidden"> |
| ✅ Complete Registration |
| </button> |
| </div> |
|
|
| <div class="text-center mt-2"> |
| <button id="backBtn" class="btn btn-outline"> |
| ← Back to Account Details |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="successCard" class="glass-card mt-2 hidden" |
| style="background: rgba(34, 197, 94, 0.1); border: 2px solid var(--color-success);"> |
| <div class="text-center"> |
| <div style="font-size: 4rem; margin-bottom: 1rem;">✅</div> |
| <h2 style="color: var(--color-success);">Registration Submitted!</h2> |
| <p class="text-muted mt-2" id="successMessage">Your account has been created successfully. Your application is now pending approval by an Admin. You will be able to login once approved.</p> |
| <div class="flex-center gap-2 mt-4"> |
| <a href="/login" class="btn btn-primary">Go to Login Page</a> |
| </div> |
| </div> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <div id="loadingOverlay" class="loading-overlay hidden"> |
| <div class="text-center"> |
| <div class="spinner"></div> |
| <p class="mt-2" id="loadingText">Processing...</p> |
| </div> |
| </div> |
|
|
| <script> |
| let videoStream = null; |
| let capturedImageData = null; |
| let userRole = null; |
| |
| |
| document.getElementById('roleSelect').addEventListener('change', (e) => { |
| userRole = e.target.value; |
| const helpText = document.getElementById('matricHelp'); |
| |
| if (userRole === 'student') { |
| helpText.textContent = 'Student matric number must start with A or B (e.g., A20EC0001)'; |
| helpText.style.color = 'var(--color-primary)'; |
| } else if (userRole === 'lecturer') { |
| helpText.textContent = 'Lecturer matric number must start with L (e.g., L001)'; |
| helpText.style.color = 'var(--color-primary)'; |
| } else { |
| helpText.textContent = 'Select your role first'; |
| helpText.style.color = 'var(--color-text-muted)'; |
| } |
| }); |
| |
| |
| document.getElementById('nextBtn').addEventListener('click', async () => { |
| const role = document.getElementById('roleSelect').value; |
| const matricNo = document.getElementById('matricNo').value.trim().toUpperCase(); |
| const fullName = document.getElementById('fullName').value.trim(); |
| const password = document.getElementById('password').value; |
| const confirmPassword = document.getElementById('confirmPassword').value; |
| |
| |
| if (!role) { |
| showStatus('Please select your role', 'error'); |
| return; |
| } |
| |
| if (!matricNo || !fullName || !password) { |
| showStatus('Please fill in all required fields', 'error'); |
| return; |
| } |
| |
| if (password !== confirmPassword) { |
| showStatus('Passwords do not match', 'error'); |
| return; |
| } |
| |
| if (password.length < 6) { |
| showStatus('Password must be at least 6 characters long', 'error'); |
| return; |
| } |
| |
| |
| if (role === 'lecturer') { |
| await registerUser(false); |
| } else { |
| |
| document.getElementById('step1Card').classList.add('hidden'); |
| document.getElementById('step2Card').classList.remove('hidden'); |
| } |
| }); |
| |
| |
| document.getElementById('backBtn').addEventListener('click', () => { |
| stopCamera(); |
| document.getElementById('step2Card').classList.add('hidden'); |
| document.getElementById('step1Card').classList.remove('hidden'); |
| }); |
| |
| |
| document.getElementById('startCameraBtn').addEventListener('click', startCamera); |
| document.getElementById('captureBtn').addEventListener('click', captureFace); |
| document.getElementById('retakeBtn').addEventListener('click', retake); |
| document.getElementById('registerBtn').addEventListener('click', () => registerUser(true)); |
| |
| async function startCamera() { |
| try { |
| videoStream = await navigator.mediaDevices.getUserMedia({ video: true }); |
| document.getElementById('video').srcObject = videoStream; |
| document.getElementById('startCameraBtn').classList.add('hidden'); |
| document.getElementById('captureBtn').classList.remove('hidden'); |
| showFaceStatus('Camera ready! Position your face in the frame', 'success'); |
| } catch (error) { |
| showFaceStatus('Camera access denied. Please allow camera access.', 'error'); |
| } |
| } |
| |
| function stopCamera() { |
| if (videoStream) { |
| videoStream.getTracks().forEach(track => track.stop()); |
| videoStream = null; |
| } |
| } |
| |
| function captureFace() { |
| const video = document.getElementById('video'); |
| const canvas = document.getElementById('canvas'); |
| const context = canvas.getContext('2d'); |
| |
| context.drawImage(video, 0, 0, 400, 300); |
| capturedImageData = canvas.toDataURL('image/jpeg'); |
| |
| document.getElementById('capturedImage').src = capturedImageData; |
| document.getElementById('preview').classList.remove('hidden'); |
| document.getElementById('video').style.display = 'none'; |
| document.getElementById('captureBtn').classList.add('hidden'); |
| document.getElementById('retakeBtn').classList.remove('hidden'); |
| document.getElementById('registerBtn').classList.remove('hidden'); |
| |
| stopCamera(); |
| showFaceStatus('Face captured! Click "Complete Registration" to finish', 'success'); |
| } |
| |
| function retake() { |
| document.getElementById('preview').classList.add('hidden'); |
| document.getElementById('video').style.display = 'block'; |
| document.getElementById('retakeBtn').classList.add('hidden'); |
| document.getElementById('registerBtn').classList.add('hidden'); |
| capturedImageData = null; |
| startCamera(); |
| } |
| |
| async function registerUser(includeFace) { |
| const role = document.getElementById('roleSelect').value; |
| const matricNo = document.getElementById('matricNo').value.trim().toUpperCase(); |
| const fullName = document.getElementById('fullName').value.trim(); |
| const email = document.getElementById('email').value.trim(); |
| const password = document.getElementById('password').value; |
| |
| if (includeFace && !capturedImageData) { |
| showFaceStatus('Please capture your face first', 'error'); |
| return; |
| } |
| |
| showLoading('Creating your account...'); |
| |
| try { |
| |
| const userResponse = await fetch('/api/auth/register', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| role: role, |
| matric_no: matricNo, |
| name: fullName, |
| email: email || null, |
| password: password |
| }) |
| }); |
| |
| const userData = await userResponse.json(); |
| |
| if (!userData.success && !userData.error.includes("already registered")) { |
| hideLoading(); |
| showGlobalStatus(userData.error, 'error'); |
| return; |
| } |
| |
| |
| if (includeFace) { |
| showLoading('Registering face data...'); |
| const faceResponse = await fetch('/api/register_face', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| student_id: matricNo, |
| name: fullName, |
| email: email || null, |
| image: capturedImageData |
| }) |
| }); |
| |
| const faceData = await faceResponse.json(); |
| |
| if (!faceData.success) { |
| hideLoading(); |
| showGlobalStatus(`Face recognition failed: ${faceData.error}`, 'error'); |
| return; |
| } |
| } |
| |
| hideLoading(); |
| console.log('Registration complete, showing success card'); |
| |
| |
| document.getElementById('step1Card').classList.add('hidden'); |
| document.getElementById('step2Card').classList.add('hidden'); |
| document.getElementById('successCard').classList.remove('hidden'); |
| document.getElementById('globalStatusMessage').innerHTML = ''; |
| |
| } catch (error) { |
| hideLoading(); |
| showStatus('Registration failed. Please try again.', 'error'); |
| } |
| } |
| |
| function showLoading(text) { |
| document.getElementById('loadingText').textContent = text; |
| document.getElementById('loadingOverlay').classList.remove('hidden'); |
| } |
| |
| function hideLoading() { |
| document.getElementById('loadingOverlay').classList.add('hidden'); |
| } |
| |
| function showStatus(message, type) { |
| showGlobalStatus(message, type); |
| } |
| |
| function showFaceStatus(message, type) { |
| showGlobalStatus(message, type); |
| } |
| |
| function showGlobalStatus(message, type) { |
| const statusDiv = document.getElementById('globalStatusMessage'); |
| const alertClass = type === 'success' ? 'alert-success' : 'alert-error'; |
| statusDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`; |
| |
| |
| window.scrollTo({ top: 0, behavior: 'smooth' }); |
| |
| if (type === 'error') { |
| setTimeout(() => { |
| statusDiv.innerHTML = ''; |
| }, 8000); |
| } |
| } |
| </script> |
| </body> |
|
|
| </html> |