diff --git a/UI_UPDATE.MD b/UI_UPDATE.MD index 8d58f45..efb55ca 100644 --- a/UI_UPDATE.MD +++ b/UI_UPDATE.MD @@ -465,10 +465,18 @@ All utilities follow the established pattern of using CSS custom properties from - [x] Implement focus indicators meeting WCAG standards - [x] Add active and pressed state styling -#### 4.2.2: Form Validation -- [ ] Implement visual form validation feedback -- [ ] Add error state styling -- [ ] Create success confirmation states +#### 4.2.2: Form Validation - COMPLETED 9/29/2025 +- [x] Implement visual form validation feedback +- [x] Add error state styling +- [x] Create success confirmation states + +**Notes:** Comprehensive form validation system implemented for chat inputs including: +- HTML structure updates: Added form-group wrappers with error message elements for nickname and message inputs +- Enhanced JavaScript validation: Added client-side validation with detailed error messages for nickname (1-20 chars, alphanumeric/places) and message (1-1000 chars) requirements +- CSS integration: Utilizes existing .form-group--error and .form-group--success classes for visual feedback +- User experience: Error messages display below inputs, success states provide green styling and toast confirmation, focus management ensures good UX flow +- Validation coverage: Empty inputs, character length limits, nickname character restrictions, and server error handling +- Accessibility: Proper error messaging and focus management for screen readers and keyboard navigation ### Sub-task 4.3: Enhanced User Feedback Systems diff --git a/assets/js/chat.js b/assets/js/chat.js index ec289b4..0991fb8 100644 --- a/assets/js/chat.js +++ b/assets/js/chat.js @@ -76,44 +76,137 @@ }); } + // Validation helper functions + function validateNickname(nickname) { + if (!nickname) { + return { valid: false, message: 'Nickname is required' }; + } + + const trimmed = nickname.trim(); + if (trimmed.length === 0) { + return { valid: false, message: 'Nickname cannot be empty' }; + } + + if (trimmed.length > 20) { + return { valid: false, message: 'Nickname must be 20 characters or less' }; + } + + if (trimmed.length < 1) { + return { valid: false, message: 'Nickname must be at least 1 character' }; + } + + // Allow alphanumeric characters and spaces, apostrophes, hyphens + const nicknameRegex = /^[a-zA-Z0-9\s'-]+$/; + if (!nicknameRegex.test(trimmed)) { + return { valid: false, message: 'Nickname can only contain letters, numbers, spaces, hyphens, and apostrophes' }; + } + + return { valid: true }; + } + + function validateMessage(message) { + if (!message) { + return { valid: false, message: 'Message is required' }; + } + + const trimmed = message.trim(); + if (trimmed.length === 0) { + return { valid: false, message: 'Message cannot be empty' }; + } + + if (trimmed.length > 1000) { + return { valid: false, message: 'Message must be 1000 characters or less' }; + } + + return { valid: true }; + } + + function updateValidationUI(field, errorElement, isValid, errorMessage) { + const formGroup = errorElement.closest('.form-group'); + const input = formGroup.querySelector('input'); + + formGroup.classList.remove('form-group--error', 'form-group--success'); + + if (isValid) { + formGroup.classList.add('form-group--success'); + errorElement.style.display = 'none'; + } else { + formGroup.classList.add('form-group--error'); + errorElement.textContent = errorMessage; + errorElement.style.display = 'block'; + } + } + + function clearValidationState(field) { + const formGroup = document.querySelector(`#${field}`).closest('.form-group'); + if (formGroup) { + formGroup.classList.remove('form-group--error', 'form-group--success'); + const errorElement = document.getElementById(`${field}Error`); + if (errorElement) { + errorElement.style.display = 'none'; + } + } + } + // Send message functionality function sendMessage() { const messageInput = document.getElementById('messageInput'); const nicknameInput = document.getElementById('nickname'); + const nicknameErrorElement = document.getElementById('nicknameError'); + const messageErrorElement = document.getElementById('messageError'); + const message = messageInput?.value?.trim() || ''; const nickname = nicknameInput?.value?.trim() || ''; - if (!nickname) { - if (nicknameInput) { - nicknameInput.style.borderColor = 'var(--dodgers-red)'; - nicknameInput.focus(); - } - UIControls.showToast('Please enter a nickname'); - setTimeout(() => { - if (nicknameInput) nicknameInput.style.borderColor = '#e0e0e0'; - }, 2000); + // Validate nickname + const nicknameValidation = validateNickname(nickname); + updateValidationUI('nickname', nicknameErrorElement, nicknameValidation.valid, nicknameValidation.message); + + // Validate message + const messageValidation = validateMessage(message); + updateValidationUI('messageInput', messageErrorElement, messageValidation.valid, messageValidation.message); + + // If either validation failed, focus the appropriate field and stop + if (!nicknameValidation.valid) { + nicknameInput.focus(); return; } - if (!message) { - return; // Just return quietly for empty message + if (!messageValidation.valid) { + messageInput.focus(); + return; } - // Send message via API + // All validation passed - send message via API API.sendMessage(nickname, message) .then(data => { if (data.success) { + // Clear the form and show success if (messageInput) messageInput.value = ''; AppState.nickname = nickname; - // Message will be added automatically by polling + + // Show success confirmation + updateValidationUI('nickname', nicknameErrorElement, true); + updateValidationUI('messageInput', messageErrorElement, true); + + UIControls.showToast('Message sent successfully!'); + + // Clear success state after a delay + setTimeout(() => { + clearValidationState('nickname'); + clearValidationState('messageInput'); + }, 3000); + + AppLogger.log('Message sent successfully'); } else if (data.error) { - UIControls.showToast(data.error); + // Show server error + updateValidationUI('messageInput', messageErrorElement, false, 'Failed to send message: ' + data.error); UIControls.updateConnectionStatus(false); } }) .catch(error => { AppLogger.error('Error sending message:', error); - UIControls.showToast('Failed to send message'); + updateValidationUI('messageInput', messageErrorElement, false, 'Failed to send message. Please try again.'); UIControls.updateConnectionStatus(false); }); } diff --git a/index.php b/index.php index 20f2a52..df0c5f8 100644 --- a/index.php +++ b/index.php @@ -461,8 +461,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {