328 lines
11 KiB
JavaScript
328 lines
11 KiB
JavaScript
// 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);
|
|
}
|
|
});
|
|
});
|
|
});
|