diff --git a/login.html b/login.html index 7c4f0e4..dca0515 100644 --- a/login.html +++ b/login.html @@ -17,6 +17,7 @@

Login

+
@@ -28,6 +29,7 @@

Login

+
diff --git a/scripts/auth.js b/scripts/auth.js index dc3b505..e0558fe 100644 --- a/scripts/auth.js +++ b/scripts/auth.js @@ -57,11 +57,240 @@ function initializePasswordToggle() { // Initialize password toggle when DOM is loaded document.addEventListener('DOMContentLoaded', function () { initializePasswordToggle(); + // Initialize form validation for both login and signup pages + initializeFormValidation(); }); +/* =============================================== + FORM VALIDATION SYSTEM - START + =============================================== */ + +// Validation configuration for reusability +const VALIDATION_RULES = { + email: { pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, minLength: 5, maxLength: 100 }, + username: { pattern: /^[a-zA-Z0-9_-]+$/, minLength: 3, maxLength: 20 }, + password: { login: { minLength: 6 }, signup: { minLength: 8, strengthCheck: true } } +}; + +/** + * Initialize form validation for both login and signup pages + */ +function initializeFormValidation() { + const formConfigs = { + 'login-form': [ + { id: 'email', type: 'email' }, + { id: 'password', type: 'password', mode: 'login' } + ], + 'signup-form': [ + { id: 'username', type: 'username' }, + { id: 'email', type: 'email' }, + { id: 'password', type: 'password', mode: 'signup' } + ] + }; + + // Setup validation for each form + Object.entries(formConfigs).forEach(([formId, fields]) => { + if (document.getElementById(formId)) { + fields.forEach(field => setupFieldValidation(field)); + } + }); +} + +/** + * Setup validation for a single field - removes duplicate event listener code + */ +function setupFieldValidation(field) { + const input = document.getElementById(field.id); + if (!input) return; + + const validator = () => validateField(input, field.type, field.mode || 'login'); + ['input', 'blur'].forEach(event => input.addEventListener(event, validator)); +} + +/** + * Universal field validation function - handles all types with common logic + */ +function validateField(input, type, mode) { + if (!input) return false; + + const value = input.value.trim(); + const messageId = `${input.id}-validation`; + + if (!value) { + hideValidationMessage(messageId, input); + if (type === 'password' && mode === 'signup') hidePasswordStrengthIndicator(messageId); + return false; + } + + switch (type) { + case 'email': return validateWithRules(input, value, messageId, 'email', '✓ Email format is valid'); + case 'username': return validateWithRules(input, value, messageId, 'username', '✓ Username is valid'); + case 'password': return validatePasswordField(input, value, messageId, mode); + default: return false; + } +} + +/** + * Unified validation with rules - eliminates duplicate code + */ +function validateWithRules(input, value, messageId, type, successMsg) { + const rules = VALIDATION_RULES[type]; + const errors = [ + [type === 'email' && !rules.pattern.test(value), 'Please enter a valid email address'], + [type === 'email' && value.includes('..'), 'Email cannot contain consecutive dots'], + [type === 'email' && (value.startsWith('.') || value.endsWith('.')), 'Email cannot start or end with a dot'], + [type === 'username' && value.length < rules.minLength, `Username must be at least ${rules.minLength} characters (${value.length}/${rules.minLength})`], + [type === 'username' && value.length > rules.maxLength, `Username cannot exceed ${rules.maxLength} characters`], + [type === 'username' && !rules.pattern.test(value), 'Username can only contain letters, numbers, underscore, and hyphen'], + [type === 'username' && !/^[a-zA-Z0-9]/.test(value), 'Username must start with a letter or number'] + ].filter(([condition]) => condition); + + if (errors.length) { + showValidationMessage(messageId, errors[0][1], 'error', input); + return false; + } + + showValidationMessage(messageId, successMsg, 'success', input); + return true; +} + +/** + * Concise password validation with mode support + */ +function validatePasswordField(input, password, messageId, mode) { + const rules = VALIDATION_RULES.password[mode]; + if (!rules) return false; + + if (password.length < rules.minLength) { + showValidationMessage(messageId, `Password must be at least ${rules.minLength} characters (${password.length}/${rules.minLength})`, 'error', input); + if (mode === 'signup') hidePasswordStrengthIndicator(messageId); + return false; + } + + if (mode === 'signup' && rules.strengthCheck) { + const strength = calculatePasswordStrength(password); + showPasswordStrengthIndicator(messageId, strength); + const messages = { + weak: ['Password is weak. ' + strength.suggestions[0], 'warning', false], + fair: ['Password is fair. ' + (strength.suggestions[0] || 'Consider adding more variety'), 'warning', true], + good: ['✓ Good password strength', 'success', true], + strong: ['✓ Strong password!', 'success', true] + }; + const [message, type, isValid] = messages[strength.level]; + showValidationMessage(messageId, message, type, input); + return isValid; + } + + showValidationMessage(messageId, '✓ Password entered', 'success', input); + return true; +} + +/** + * Efficient validation message utilities + */ +function showValidationMessage(messageId, message, type, inputElement) { + const messageElement = document.getElementById(messageId); + if (!messageElement || !inputElement) return; + + messageElement.className = `validation-message show ${type}`; + + // Remove all validation-* classes using classList methods + Array.from(inputElement.classList) + .filter(cls => /^validation-\w+$/.test(cls)) + .forEach(cls => inputElement.classList.remove(cls)); + + // Add new validation class + inputElement.classList.add(`validation-${type}`); + messageElement.textContent = message; +} + +function hideValidationMessage(messageId, inputElement) { + const messageElement = document.getElementById(messageId); + if (messageElement) messageElement.className = 'validation-message'; + + if (inputElement) { + // Remove all validation-* classes using classList methods + Array.from(inputElement.classList) + .filter(cls => /^validation-\w+$/.test(cls)) + .forEach(cls => inputElement.classList.remove(cls)); + } +} + +/** + * Concise password strength calculation + */ +function calculatePasswordStrength(password) { + const checks = [ + [password.length >= 8, 'Add more characters'], + [password.length >= 12, ''], + [/[a-z]/.test(password), 'Add lowercase letters'], + [/[A-Z]/.test(password), 'Add uppercase letters'], + [/[0-9]/.test(password), 'Add numbers'], + [/[^a-zA-Z0-9]/.test(password), 'Add special characters (!@#$%)'] + ]; + + const score = checks.filter(([test]) => test).length; + const suggestions = checks.filter(([test, msg]) => !test && msg).map(([, msg]) => msg); + const levels = ['weak', 'weak', 'weak', 'fair', 'fair', 'good', 'strong']; + + return { level: levels[score] || 'weak', score, suggestions }; +} + +/** + * Efficient password strength indicator + */ +function showPasswordStrengthIndicator(messageId, strength) { + const messageElement = document.getElementById(messageId); + if (!messageElement) return; + + // Clean up existing indicator + const existing = messageElement.parentNode.querySelector('.password-strength-indicator'); + if (existing) existing.remove(); + + const indicator = document.createElement('div'); + indicator.innerHTML = ` +
+
+
+
+
Password strength: ${strength.level.charAt(0).toUpperCase() + strength.level.slice(1)}
+
+ `; + + messageElement.parentNode.insertBefore(indicator.firstElementChild, messageElement.nextSibling); +} + +function hidePasswordStrengthIndicator(messageId) { + const messageElement = document.getElementById(messageId); + const indicator = messageElement?.parentNode?.querySelector('.password-strength-indicator'); + if (indicator) indicator.remove(); +} + +/** + * Efficient form validation for submission + */ +function validateForm(formType) { + const fieldConfigs = { + login: [['email', 'email'], ['password', 'password', 'login']], + signup: [['username', 'username'], ['email', 'email'], ['password', 'password', 'signup']] + }; + + const fields = fieldConfigs[formType]; + return fields ? fields.every(([id, type, mode]) => { + const input = document.getElementById(id); + return input && validateField(input, type, mode || 'login'); + }) : false; +} + if (document.getElementById('signup-form')) { document.getElementById('signup-form').addEventListener('submit', async (e) => { e.preventDefault() + + // Validate form before submission + if (!validateForm('signup')) { + return; // Stop submission if validation fails + } + const username = document.getElementById('username').value const email = document.getElementById('email').value const password = document.getElementById('password').value @@ -79,6 +308,12 @@ if (document.getElementById('signup-form')) { if (document.getElementById('login-form')) { document.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault() + + // Validate form before submission + if (!validateForm('login')) { + return; // Stop submission if validation fails + } + const email = document.getElementById('email').value const password = document.getElementById('password').value const res = await API.login({ email, password }) diff --git a/signup.html b/signup.html index 7655631..f576ed6 100644 --- a/signup.html +++ b/signup.html @@ -17,10 +17,12 @@

Sign up

+
+
@@ -32,6 +34,7 @@

Sign up

+
diff --git a/styles/login.css b/styles/login.css index dafe6d2..3afc41d 100644 --- a/styles/login.css +++ b/styles/login.css @@ -218,4 +218,128 @@ input[type="password"]::-webkit-credentials-auto-fill-button { font-size: 1rem; /* Smaller font size for button text */ } -} \ No newline at end of file +} +/* =============================================== + FORM VALIDATION STYLES - START + =============================================== */ + + +.validation-message { + font-size: 0.85rem; + margin-top: 0; + margin-bottom: 0; + transition: all 0.3s ease; + opacity: 0; + transform: translateY(-10px); + visibility: hidden; + height: 0; + overflow: hidden; + font-weight: 500; +} + + +.validation-message.show { + opacity: 1; + transform: translateY(0); + visibility: visible; + height: auto; + margin-top: 5px; + margin-bottom: 5px; +} + + +.validation-message.error { + color: #780000; /* Keep red for errors */ +} + +.validation-message.success { + color: #28a745; /* Green for success */ +} + + +.validation-message.warning { + color: #ffc107; /* Yellow for warnings */ +} + +.input-group input.validation-error { + border-color: #780000; + background-color: rgba(120, 0, 0, 0.05); + box-shadow: 0 0 0 2px rgba(120, 0, 0, 0.1); +} + +.input-group input.validation-success { + border-color: #28a745; /* Green border for success */ + background-color: rgba(40, 167, 69, 0.05); /* Light green background */ + box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.1); /* Green box-shadow */ +} + +.input-group input.validation-warning { + border-color: #ffc107; /* Yellow border for warnings */ + background-color: rgba(255, 193, 7, 0.05); /* Light yellow background */ + box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.1); /* Yellow box-shadow */ +} + + +.password-strength-indicator { + margin-top: 8px; + margin-bottom: 5px; +} + +.password-strength-bar { + width: 100%; + height: 4px; + background-color: #e9ecef; + border-radius: 2px; + overflow: hidden; + margin-bottom: 5px; +} + +.password-strength-fill { + height: 100%; + transition: all 0.3s ease; + border-radius: 2px; +} + +.password-strength-fill.weak { + width: 25%; + background-color: #780000; +} + +.password-strength-fill.fair { + width: 50%; + background-color: #a00000; +} + +.password-strength-fill.good { + width: 75%; + background-color: #780000; +} + +.password-strength-fill.strong { + width: 100%; + background-color: #780000; +} + +.password-strength-text { + font-size: 0.8rem; + font-weight: 500; + text-align: center; + color: #780000; + /* Using theme color */ +} + + +@media (max-width: 600px) { + .validation-message { + font-size: 0.8rem; + padding: 6px 10px; + } + + .password-strength-text { + font-size: 0.75rem; + } +} + +/* =============================================== + FORM VALIDATION STYLES - END + =============================================== */ \ No newline at end of file