// ThreadR UI Enhancement JavaScript // Show notification toast function showNotification(message, type = 'info', duration = 3000) { const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.classList.add('hiding'); setTimeout(() => { document.body.removeChild(notification); }, 300); }, duration); } // Add loading state to form submission function handleFormSubmit(form, button) { if (button) { button.disabled = true; button.classList.add('loading'); } // Find all submit buttons in the form and disable them const submitButtons = form.querySelectorAll('input[type="submit"], button[type="submit"]'); submitButtons.forEach(btn => { btn.disabled = true; btn.classList.add('loading'); }); } // Remove loading state function removeLoadingState(form) { const submitButtons = form.querySelectorAll('input[type="submit"], button[type="submit"]'); submitButtons.forEach(btn => { btn.disabled = false; btn.classList.remove('loading'); }); } // Enable Enter-to-submit for single-line inputs function enableEnterToSubmit(input, form) { input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); form.requestSubmit(); } }); } // Auto-resize textarea as user types function autoResizeTextarea(textarea) { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; } // Add character counter to textarea function addCharacterCounter(textarea, maxLength) { const counter = document.createElement('div'); counter.className = 'char-counter'; textarea.parentNode.insertBefore(counter, textarea.nextSibling); function updateCounter() { const length = textarea.value.length; counter.textContent = `${length}${maxLength ? '/' + maxLength : ''} characters`; if (maxLength && length > maxLength * 0.9) { counter.classList.add('warning'); } else { counter.classList.remove('warning'); } } textarea.addEventListener('input', updateCounter); updateCounter(); } // Client-side validation helpers function validateUsername(username) { if (username.length < 3) { return 'Username must be at least 3 characters'; } if (username.length > 30) { return 'Username must be less than 30 characters'; } if (!/^[a-zA-Z0-9_-]+$/.test(username)) { return 'Username can only contain letters, numbers, underscores, and hyphens'; } return null; } function validatePassword(password) { if (password.length < 8) { return 'Password must be at least 8 characters'; } if (password.length > 128) { return 'Password is too long'; } return null; } function validateRequired(value, fieldName) { if (!value || value.trim() === '') { return `${fieldName} is required`; } return null; } // Show field error function showFieldError(input, message) { input.classList.add('error'); // Remove existing error message const existingError = input.parentNode.querySelector('.field-error'); if (existingError) { existingError.remove(); } if (message) { const errorDiv = document.createElement('div'); errorDiv.className = 'field-error'; errorDiv.textContent = message; input.parentNode.insertBefore(errorDiv, input.nextSibling); } } // Clear field error function clearFieldError(input) { input.classList.remove('error'); const errorDiv = input.parentNode.querySelector('.field-error'); if (errorDiv) { errorDiv.remove(); } } // Relative time formatting function formatRelativeTime(date) { const now = new Date(); const diff = now - date; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); const months = Math.floor(days / 30); const years = Math.floor(days / 365); if (seconds < 60) return 'just now'; if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`; if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''} ago`; if (days < 30) return `${days} day${days !== 1 ? 's' : ''} ago`; if (months < 12) return `${months} month${months !== 1 ? 's' : ''} ago`; return `${years} year${years !== 1 ? 's' : ''} ago`; } // Convert timestamps to relative time function initRelativeTimestamps() { document.querySelectorAll('[data-timestamp]').forEach(element => { const timestamp = element.getAttribute('data-timestamp'); const date = new Date(timestamp); const originalText = element.textContent; element.textContent = formatRelativeTime(date); element.title = originalText; element.style.cursor = 'help'; }); } // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initRelativeTimestamps); } else { initRelativeTimestamps(); } // Add form submission handlers document.addEventListener('DOMContentLoaded', () => { // Handle all form submissions document.querySelectorAll('form').forEach(form => { form.addEventListener('submit', (e) => { const submitButton = form.querySelector('input[type="submit"], button[type="submit"]'); handleFormSubmit(form, submitButton); }); }); // Auto-resize textareas document.querySelectorAll('textarea').forEach(textarea => { textarea.addEventListener('input', () => autoResizeTextarea(textarea)); // Add character counter for content fields if (textarea.id === 'content' || textarea.name === 'content') { addCharacterCounter(textarea, 10000); } if (textarea.id === 'bio' || textarea.name === 'bio') { addCharacterCounter(textarea, 500); } }); // Enable Enter-to-submit for single-line forms (login, etc.) const loginForm = document.querySelector('form[action*="login"]'); if (loginForm) { const passwordInput = loginForm.querySelector('input[type="password"]'); if (passwordInput) { enableEnterToSubmit(passwordInput, loginForm); } } // Add validation to login form const loginUsername = document.querySelector('input[name="username"]'); const loginPassword = document.querySelector('input[name="password"]'); if (loginUsername && loginPassword) { loginUsername.addEventListener('blur', () => { const error = validateRequired(loginUsername.value, 'Username'); if (error) { showFieldError(loginUsername, error); } else { clearFieldError(loginUsername); } }); loginPassword.addEventListener('blur', () => { const error = validateRequired(loginPassword.value, 'Password'); if (error) { showFieldError(loginPassword, error); } else { clearFieldError(loginPassword); } }); } // Add validation to signup form const signupForm = document.querySelector('form[action*="signup"]'); if (signupForm) { const usernameInput = signupForm.querySelector('input[name="username"]'); const passwordInput = signupForm.querySelector('input[name="password"]'); const confirmInput = signupForm.querySelector('input[name="password_confirm"]'); if (usernameInput) { usernameInput.addEventListener('blur', () => { const error = validateUsername(usernameInput.value); if (error) { showFieldError(usernameInput, error); } else { clearFieldError(usernameInput); } }); } if (passwordInput) { passwordInput.addEventListener('blur', () => { const error = validatePassword(passwordInput.value); if (error) { showFieldError(passwordInput, error); } else { clearFieldError(passwordInput); } }); } if (confirmInput && passwordInput) { confirmInput.addEventListener('blur', () => { if (confirmInput.value !== passwordInput.value) { showFieldError(confirmInput, 'Passwords do not match'); } else { clearFieldError(confirmInput); } }); } signupForm.addEventListener('submit', (e) => { let hasError = false; if (usernameInput) { const error = validateUsername(usernameInput.value); if (error) { showFieldError(usernameInput, error); hasError = true; } } if (passwordInput) { const error = validatePassword(passwordInput.value); if (error) { showFieldError(passwordInput, error); hasError = true; } } if (confirmInput && passwordInput && confirmInput.value !== passwordInput.value) { showFieldError(confirmInput, 'Passwords do not match'); hasError = true; } if (hasError) { e.preventDefault(); removeLoadingState(signupForm); showNotification('Please fix the errors before submitting', 'error'); } }); } // Add validation to thread/post forms document.querySelectorAll('input[name="title"]').forEach(input => { input.addEventListener('blur', () => { const error = validateRequired(input.value, 'Title'); if (error) { showFieldError(input, error); } else if (input.value.length > 255) { showFieldError(input, 'Title is too long (max 255 characters)'); } else { clearFieldError(input); } }); }); document.querySelectorAll('textarea[name="content"]').forEach(textarea => { textarea.addEventListener('blur', () => { const error = validateRequired(textarea.value, 'Content'); if (error) { showFieldError(textarea, error); } else { clearFieldError(textarea); } }); }); });