(() => { const API_BASE_URL = `https://${window.location.hostname}`; // Elements const registerForm = document.getElementById('registerForm'); const registerBtn = document.getElementById('registerBtn'); // Validation state let validationErrors = {}; let isSubmitting = false; // API helper async function api(path, opts) { const url = API_BASE_URL + path; const res = await fetch(url, opts); if (!res.ok) { let errorData; try { errorData = await res.json(); } catch (e) { const txt = await res.text(); errorData = { error: txt || res.statusText }; } throw new Error(JSON.stringify(errorData)); } return res.json(); } // Show message function showMessage(text, type = 'error') { const existing = document.querySelector('.error-message, .success-message'); if (existing) existing.remove(); const message = document.createElement('div'); message.className = type === 'error' ? 'error-message' : 'success-message'; message.textContent = text; registerForm.insertBefore(message, registerForm.firstChild); setTimeout(() => message.remove(), 5000); } // Show field error function showFieldError(fieldId, message) { const errorElement = document.getElementById(fieldId + 'Error'); if (errorElement) { errorElement.textContent = message; errorElement.classList.add('show'); } const inputElement = document.getElementById(fieldId); const formGroup = inputElement ? inputElement.closest('.form-group') : null; if (formGroup) { formGroup.classList.add('error'); formGroup.classList.remove('success'); } } // Show server validation errors for specific fields function showServerFieldErrors(serverErrors) { const fieldMapping = { 'username': 'regUsername', 'email': 'regEmail', 'fullname': 'regFullname', 'telephone': 'regTelephone', 'province': 'regProvince', 'district': 'regDistrict', 'password': 'regPassword', 'confirmPassword': 'regConfirmPassword' }; Object.keys(serverErrors).forEach(field => { const fieldId = fieldMapping[field] || 'reg' + field.charAt(0).toUpperCase() + field.slice(1); showFieldError(fieldId, serverErrors[field]); }); } // Clear field error function clearFieldError(fieldId) { const errorElement = document.getElementById(fieldId + 'Error'); if (errorElement) { errorElement.textContent = ''; errorElement.classList.remove('show'); } const formGroup = document.getElementById(fieldId).closest('.form-group'); if (formGroup) { formGroup.classList.remove('error'); formGroup.classList.add('success'); } } // Clear all field errors function clearAllFieldErrors() { const fieldIds = ['regUsername', 'regEmail', 'regFullname', 'regTelephone', 'regProvince', 'regDistrict', 'regPassword', 'regConfirmPassword', 'agreeTerms']; fieldIds.forEach(fieldId => clearFieldError(fieldId)); } // Clear all generic error messages function clearAllGenericMessages() { const existing = document.querySelector('.error-message, .success-message'); if (existing) existing.remove(); } // Validate username function validateUsername(username) { if (!username || username.trim() === '') { return 'Username is required'; } if (username.length < 3) { return 'Username must be at least 3 characters'; } if (username.length > 50) { return 'Username must be less than 50 characters'; } if (!/^[a-zA-Z0-9_]+$/.test(username)) { return 'Username can only contain letters, numbers, and underscores'; } // Check for reserved usernames const reservedUsernames = ['admin', 'administrator', 'root', 'system', 'api', 'test', 'user', 'guest', 'null', 'undefined']; if (reservedUsernames.includes(username.toLowerCase())) { return 'This username is reserved and cannot be used'; } return null; } // Validate email function validateEmail(email) { if (!email || email.trim() === '') { return 'Email address is required'; } if (email.length > 100) { return 'Email address must be less than 100 characters'; } const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; if (!emailPattern.test(email)) { return 'Please enter a valid email address'; } // Check for common email providers const commonDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'icloud.com']; const domain = email.split('@')[1]?.toLowerCase(); if (domain && !commonDomains.includes(domain) && !domain.includes('.')) { return 'Please enter a valid email address'; } return null; } // Validate full name function validateFullName(fullname) { if (!fullname || fullname.trim() === '') { return 'Full name is required'; } if (fullname.length < 2) { return 'Full name must be at least 2 characters'; } if (fullname.length > 100) { return 'Full name must be less than 100 characters'; } if (!/^[a-zA-Z\s\-'\.]+$/.test(fullname)) { return 'Full name can only contain letters, spaces, hyphens, apostrophes, and periods'; } // Check for minimum words const words = fullname.trim().split(/\s+/); if (words.length < 2) { return 'Please enter your complete name (first and last name)'; } return null; } // Validate phone number function validatePhone(telephone) { if (!telephone || telephone.trim() === '') { return 'Phone number is required'; } // Remove all spaces and special characters except + and digits const cleanPhone = telephone.replace(/[^\d+]/g, ''); // Check Rwanda phone number format const phonePattern = /^(\+250|0)[0-9]{9}$/; if (!phonePattern.test(cleanPhone)) { return 'Please enter a valid Rwanda phone number (+250XXXXXXXXX or 07XXXXXXXX)'; } // Additional validation for specific prefixes if (cleanPhone.startsWith('0')) { const prefix = cleanPhone.substring(0, 3); const validPrefixes = ['078', '079', '072', '073', '074', '075', '076', '077']; if (!validPrefixes.includes(prefix)) { return 'Please enter a valid Rwanda mobile number'; } } return null; } // Validate password function validatePassword(password) { if (!password || password === '') { return 'Password is required'; } if (password.length < 8) { return 'Password must be at least 8 characters long'; } if (password.length > 128) { return 'Password must be less than 128 characters'; } // Check for at least one letter and one number if (!/[a-zA-Z]/.test(password)) { return 'Password must contain at least one letter'; } if (!/[0-9]/.test(password)) { return 'Password must contain at least one number'; } // Check for common weak passwords const weakPasswords = ['password', '123456', '12345678', 'qwerty', 'abc123', 'password123', 'admin', 'letmein']; if (weakPasswords.includes(password.toLowerCase())) { return 'This password is too common. Please choose a stronger password'; } return null; } // Validate password confirmation function validatePasswordConfirmation(password, confirmPassword) { if (!confirmPassword || confirmPassword === '') { return 'Please confirm your password'; } if (password !== confirmPassword) { return 'Passwords do not match'; } return null; } // Validate province function validateProvince(province) { if (!province || province === '') { return 'Please select a province'; } const validProvinces = ['Kigali', 'Eastern', 'Northern', 'Southern', 'Western']; if (!validProvinces.includes(province)) { return 'Please select a valid province'; } return null; } // Validate district function validateDistrict(district, province) { if (!district || district === '') { return 'Please select a district'; } if (!province || province === '') { return 'Please select a province first'; } const validDistricts = getDistrictsForProvince(province); if (!validDistricts.includes(district)) { return 'Please select a valid district for the selected province'; } return null; } // Validate terms agreement function validateTerms(agreeTerms) { if (!agreeTerms) { return 'You must agree to the Terms of Service and Privacy Policy'; } return null; } // Get districts for province function getDistrictsForProvince(province) { const provinceDistricts = { 'Kigali': ['Gasabo', 'Kicukiro', 'Nyarugenge'], 'Eastern': ['Bugesera', 'Gatsibo', 'Kayonza', 'Kirehe', 'Ngoma', 'Nyagatare', 'Rwamagana'], 'Northern': ['Burera', 'Gakenke', 'Gicumbi', 'Musanze', 'Rulindo'], 'Southern': ['Gisagara', 'Huye', 'Kamonyi', 'Muhanga', 'Nyamagabe', 'Nyanza', 'Nyaruguru', 'Ruhango'], 'Western': ['Karongi', 'Ngororero', 'Nyabihu', 'Nyamasheke', 'Rubavu', 'Rusizi', 'Rutsiro'] }; return provinceDistricts[province] || []; } // Calculate password strength function calculatePasswordStrength(password) { let score = 0; if (password.length >= 8) score += 1; if (password.length >= 12) score += 1; if (/[a-z]/.test(password)) score += 1; if (/[A-Z]/.test(password)) score += 1; if (/[0-9]/.test(password)) score += 1; if (/[^a-zA-Z0-9]/.test(password)) score += 1; if (score <= 2) return 'weak'; if (score <= 4) return 'medium'; return 'strong'; } // Update password strength indicator function updatePasswordStrength(password) { const strength = calculatePasswordStrength(password); const strengthElement = document.querySelector('.password-strength'); const strengthBar = document.querySelector('.password-strength-bar'); if (strengthElement && strengthBar) { strengthElement.textContent = `Password strength: ${strength}`; strengthElement.className = `password-strength ${strength}`; strengthBar.className = `password-strength-bar ${strength}`; } } // Real-time validation function setupRealTimeValidation() { // Username validation document.getElementById('regUsername').addEventListener('blur', function() { const error = validateUsername(this.value); if (error) { showFieldError('regUsername', error); } else { clearFieldError('regUsername'); } }); // Email validation document.getElementById('regEmail').addEventListener('blur', function() { const error = validateEmail(this.value); if (error) { showFieldError('regEmail', error); } else { clearFieldError('regEmail'); } }); // Full name validation document.getElementById('regFullname').addEventListener('blur', function() { const error = validateFullName(this.value); if (error) { showFieldError('regFullname', error); } else { clearFieldError('regFullname'); } }); // Phone validation document.getElementById('regTelephone').addEventListener('blur', function() { const error = validatePhone(this.value); if (error) { showFieldError('regTelephone', error); } else { clearFieldError('regTelephone'); } }); // Province validation document.getElementById('regProvince').addEventListener('change', function() { const error = validateProvince(this.value); if (error) { showFieldError('regProvince', error); } else { clearFieldError('regProvince'); } }); // District validation document.getElementById('regDistrict').addEventListener('change', function() { const province = document.getElementById('regProvince').value; const error = validateDistrict(this.value, province); if (error) { showFieldError('regDistrict', error); } else { clearFieldError('regDistrict'); } }); // Password validation document.getElementById('regPassword').addEventListener('input', function() { updatePasswordStrength(this.value); const error = validatePassword(this.value); if (error) { showFieldError('regPassword', error); } else { clearFieldError('regPassword'); } }); // Password confirmation validation document.getElementById('regConfirmPassword').addEventListener('blur', function() { const password = document.getElementById('regPassword').value; const error = validatePasswordConfirmation(password, this.value); if (error) { showFieldError('regConfirmPassword', error); } else { clearFieldError('regConfirmPassword'); } }); // Terms validation document.getElementById('agreeTerms').addEventListener('change', function() { const error = validateTerms(this.checked); if (error) { showFieldError('agreeTerms', error); } else { clearFieldError('agreeTerms'); } }); } // Validate all fields function validateAllFields() { const username = document.getElementById('regUsername').value.trim(); const email = document.getElementById('regEmail').value.trim(); const fullname = document.getElementById('regFullname').value.trim(); const telephone = document.getElementById('regTelephone').value.trim(); const province = document.getElementById('regProvince').value; const district = document.getElementById('regDistrict').value; const password = document.getElementById('regPassword').value; const confirmPassword = document.getElementById('regConfirmPassword').value; const agreeTerms = document.getElementById('agreeTerms').checked; validationErrors = {}; // Validate each field const usernameError = validateUsername(username); if (usernameError) validationErrors.username = usernameError; const emailError = validateEmail(email); if (emailError) validationErrors.email = emailError; const fullnameError = validateFullName(fullname); if (fullnameError) validationErrors.fullname = fullnameError; const telephoneError = validatePhone(telephone); if (telephoneError) validationErrors.telephone = telephoneError; const provinceError = validateProvince(province); if (provinceError) validationErrors.province = provinceError; const districtError = validateDistrict(district, province); if (districtError) validationErrors.district = districtError; const passwordError = validatePassword(password); if (passwordError) validationErrors.password = passwordError; const confirmPasswordError = validatePasswordConfirmation(password, confirmPassword); if (confirmPasswordError) validationErrors.confirmPassword = confirmPasswordError; const termsError = validateTerms(agreeTerms); if (termsError) validationErrors.terms = termsError; // Show all errors Object.keys(validationErrors).forEach(field => { showFieldError('reg' + field.charAt(0).toUpperCase() + field.slice(1), validationErrors[field]); }); return Object.keys(validationErrors).length === 0; } // Redirect to main app function redirectToApp(account = null) { if (account) { localStorage.setItem('aimhsa_account', account); } window.location.href = '/index.html'; } // Registration form submission registerForm.addEventListener('submit', async (e) => { e.preventDefault(); if (isSubmitting) return; // Clear previous errors clearAllFieldErrors(); clearAllGenericMessages(); // Validate all fields if (!validateAllFields()) { showMessage('Please correct the errors below'); return; } const username = document.getElementById('regUsername').value.trim(); const email = document.getElementById('regEmail').value.trim(); const fullname = document.getElementById('regFullname').value.trim(); const telephone = document.getElementById('regTelephone').value.trim(); const province = document.getElementById('regProvince').value; const district = document.getElementById('regDistrict').value; const password = document.getElementById('regPassword').value; isSubmitting = true; registerBtn.disabled = true; registerBtn.textContent = 'Creating account...'; try { const response = await api('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, fullname, telephone, province, district, password }) }); showMessage('Account created successfully! Redirecting...', 'success'); setTimeout(() => redirectToApp(username), 1500); } catch (err) { console.log('Registration error:', err); // Parse server error response for specific field errors let serverErrors = {}; let genericError = 'Registration failed. Please check the errors below.'; try { // Try to parse JSON error response const errorData = JSON.parse(err.message); console.log('Parsed error data:', errorData); if (errorData.errors) { serverErrors = errorData.errors; console.log('Server errors:', serverErrors); } else if (errorData.error) { genericError = errorData.error; } } catch (parseError) { console.log('Could not parse error as JSON:', parseError); // If not JSON, check for specific error patterns const errorText = err.message.toLowerCase(); if (errorText.includes('username')) { if (errorText.includes('already exists') || errorText.includes('taken')) { serverErrors.username = 'This username is already taken. Please choose another.'; } else if (errorText.includes('invalid')) { serverErrors.username = 'Invalid username format.'; } } else if (errorText.includes('email')) { if (errorText.includes('already exists') || errorText.includes('taken')) { serverErrors.email = 'This email is already registered. Please use a different email.'; } else if (errorText.includes('invalid')) { serverErrors.email = 'Invalid email format.'; } } else if (errorText.includes('phone') || errorText.includes('telephone')) { serverErrors.telephone = 'Invalid phone number format.'; } else if (errorText.includes('password')) { serverErrors.password = 'Password does not meet requirements.'; } else if (errorText.includes('province')) { serverErrors.province = 'Please select a valid province.'; } else if (errorText.includes('district')) { serverErrors.district = 'Please select a valid district.'; } } // Show specific field errors if (Object.keys(serverErrors).length > 0) { console.log('Showing field errors:', serverErrors); // Clear any existing generic error messages clearAllGenericMessages(); // Show server validation errors for each field showServerFieldErrors(serverErrors); // Show generic message if there are field errors showMessage('Please correct the errors below'); return; // Exit after showing field errors } // Only show generic message if no specific field errors console.log('Showing generic error:', genericError); showMessage(genericError); } finally { isSubmitting = false; registerBtn.disabled = false; registerBtn.textContent = 'Create Account'; } }); // Province/District mapping for Rwanda const provinceDistricts = { 'Kigali': ['Gasabo', 'Kicukiro', 'Nyarugenge'], 'Eastern': ['Bugesera', 'Gatsibo', 'Kayonza', 'Kirehe', 'Ngoma', 'Nyagatare', 'Rwamagana'], 'Northern': ['Burera', 'Gakenke', 'Gicumbi', 'Musanze', 'Rulindo'], 'Southern': ['Gisagara', 'Huye', 'Kamonyi', 'Muhanga', 'Nyamagabe', 'Nyanza', 'Nyaruguru', 'Ruhango'], 'Western': ['Karongi', 'Ngororero', 'Nyabihu', 'Nyamasheke', 'Rubavu', 'Rusizi', 'Rutsiro'] }; // Handle province change to filter districts document.getElementById('regProvince').addEventListener('change', function() { const province = this.value; const districtSelect = document.getElementById('regDistrict'); // Clear existing options except the first one districtSelect.innerHTML = ''; if (province && provinceDistricts[province]) { provinceDistricts[province].forEach(district => { const option = document.createElement('option'); option.value = district; option.textContent = district; districtSelect.appendChild(option); }); } // Clear district error when province changes clearFieldError('regDistrict'); }); // Initialize real-time validation setupRealTimeValidation(); // Check if already logged in const account = localStorage.getItem('aimhsa_account'); if (account && account !== 'null') { redirectToApp(account); } })();