Spaces:
Sleeping
Sleeping
| (() => { | |
| 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 = '<option value="">Select District</option>'; | |
| 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); | |
| } | |
| })(); |