diff --git a/templates/register.templ b/templates/register.templ index c98f811..f502c0a 100644 --- a/templates/register.templ +++ b/templates/register.templ @@ -19,11 +19,11 @@ templ Register() {
- +
- +
@@ -33,11 +33,38 @@ templ Register() {
+
+

Password Requirements:

+ +
- +
-
@@ -48,6 +75,14 @@ templ Register() { Already have an account? Login

+ + } \ No newline at end of file diff --git a/web/src/password_validation.ts b/web/src/password_validation.ts new file mode 100644 index 0000000..36d0c6e --- /dev/null +++ b/web/src/password_validation.ts @@ -0,0 +1,177 @@ +// Password validation for registration form +// Procedural style - no OOP, passive event listeners for password manager compatibility + +// Validation check functions +function hasMinimumLength(password: string): boolean { + return password.length >= 8; +} + +function hasUppercase(password: string): boolean { + return /[A-Z]/.test(password); +} + +function hasLowercase(password: string): boolean { + return /[a-z]/.test(password); +} + +function hasNumber(password: string): boolean { + return /[0-9]/.test(password); +} + +function hasSpecialChar(password: string): boolean { + return /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password); +} + +function passwordsMatch(password: string, confirm: string): boolean { + if (!password && !confirm) { + return false; + } + return password === confirm; +} + +function hasUsername(username: string): boolean { + return username.trim().length > 0; +} + +function hasEmail(email: string): boolean { + return email.trim().length > 0; +} + +// UI update functions +function updateRequirementStatus(elementId: string, passed: boolean): void { + const element = document.getElementById(elementId); + if (!element) { + return; + } + + const icon = element.querySelector('.requirement-icon'); + if (!icon) { + return; + } + + if (passed) { + icon.textContent = '✓'; + icon.className = 'requirement-icon text-green-500'; + element.style.color = 'var(--text-primary)'; + } else { + icon.textContent = '○'; + icon.className = 'requirement-icon'; + element.style.color = 'var(--text-secondary)'; + } +} + +function updateSubmitButton(allPassed: boolean): void { + const button = document.getElementById('register-btn') as HTMLButtonElement; + if (!button) { + return; + } + + if (allPassed) { + button.disabled = false; + button.classList.remove('opacity-50', 'cursor-not-allowed'); + } else { + button.disabled = true; + button.classList.add('opacity-50', 'cursor-not-allowed'); + } +} + +// Main validation orchestrator +function validateAll(): void { + const passwordField = document.getElementById('password') as HTMLInputElement; + const confirmField = document.getElementById('confirm-password') as HTMLInputElement; + const usernameField = document.getElementById('username') as HTMLInputElement; + const emailField = document.getElementById('email') as HTMLInputElement; + + if (!passwordField || !confirmField || !usernameField || !emailField) { + return; + } + + const password = passwordField.value; + const confirm = confirmField.value; + const username = usernameField.value; + const email = emailField.value; + + // Check password requirements + const hasLen = hasMinimumLength(password); + const hasUpper = hasUppercase(password); + const hasLower = hasLowercase(password); + const hasNum = hasNumber(password); + const hasSpecial = hasSpecialChar(password); + const doMatch = passwordsMatch(password, confirm); + const hasUser = hasUsername(username); + const hasEmailAddr = hasEmail(email); + + // Update requirement indicators + updateRequirementStatus('req-length', hasLen); + updateRequirementStatus('req-upper', hasUpper); + updateRequirementStatus('req-lower', hasLower); + updateRequirementStatus('req-number', hasNum); + updateRequirementStatus('req-special', hasSpecial); + updateRequirementStatus('req-match', doMatch); + + // Enable/disable submit button + const allPassed = hasLen && hasUpper && hasLower && hasNum && + hasSpecial && doMatch && hasUser && hasEmailAddr; + updateSubmitButton(allPassed); +} + +// Debounce function to avoid excessive validation calls +let debounceTimer: number | null = null; + +function debouncedValidation(): void { + if (debounceTimer !== null) { + clearTimeout(debounceTimer); + } + debounceTimer = window.setTimeout(() => { + validateAll(); + debounceTimer = null; + }, 100); +} + +// Event handlers (PASSIVE - no preventDefault, doesn't block password managers) +function onPasswordInput(): void { + debouncedValidation(); +} + +function onConfirmInput(): void { + debouncedValidation(); +} + +function onUsernameInput(): void { + debouncedValidation(); +} + +function onEmailInput(): void { + debouncedValidation(); +} + +// Initialization +function initPasswordValidation(): void { + const passwordField = document.getElementById('password') as HTMLInputElement; + const confirmField = document.getElementById('confirm-password') as HTMLInputElement; + const usernameField = document.getElementById('username') as HTMLInputElement; + const emailField = document.getElementById('email') as HTMLInputElement; + + if (!passwordField || !confirmField || !usernameField || !emailField) { + return; + } + + // Add passive event listeners - don't prevent default, don't block password managers + passwordField.addEventListener('input', onPasswordInput, { passive: true }); + passwordField.addEventListener('paste', onPasswordInput, { passive: true }); + + confirmField.addEventListener('input', onConfirmInput, { passive: true }); + confirmField.addEventListener('paste', onConfirmInput, { passive: true }); + + usernameField.addEventListener('input', onUsernameInput, { passive: true }); + usernameField.addEventListener('paste', onUsernameInput, { passive: true }); + + emailField.addEventListener('input', onEmailInput, { passive: true }); + emailField.addEventListener('paste', onEmailInput, { passive: true }); + + // Initial validation + validateAll(); +} + +// Export for use in template +(window as any).initPasswordValidation = initPasswordValidation;