threadr.lostcave.ddnss.de/static/app.js

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);
}
});
});
});