split stuff so i can read better
parent
56416b78ec
commit
2c7634da43
442
static/app.js
442
static/app.js
|
|
@ -1,429 +1,25 @@
|
||||||
// ThreadR UI Enhancement 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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optimistic UI for like/dislike buttons
|
|
||||||
document.querySelectorAll('form[action*="/like/"]').forEach(form => {
|
|
||||||
form.addEventListener('submit', (e) => {
|
|
||||||
const button = form.querySelector('button[type="submit"]');
|
|
||||||
if (button) {
|
|
||||||
button.style.opacity = '0.5';
|
|
||||||
button.textContent = button.textContent + ' ✓';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize on DOM ready
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', initRelativeTimestamps);
|
|
||||||
} else {
|
|
||||||
initRelativeTimestamps();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
|
||||||
function initKeyboardShortcuts() {
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
// Ctrl+Enter or Cmd+Enter to submit forms
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
||||||
const activeElement = document.activeElement;
|
|
||||||
if (activeElement && activeElement.tagName === 'TEXTAREA') {
|
|
||||||
const form = activeElement.closest('form');
|
|
||||||
if (form) {
|
|
||||||
e.preventDefault();
|
|
||||||
form.requestSubmit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape to cancel/close things
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
// Clear any active focus from textareas
|
|
||||||
if (document.activeElement && document.activeElement.tagName === 'TEXTAREA') {
|
|
||||||
document.activeElement.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide notifications
|
|
||||||
document.querySelectorAll('.notification').forEach(notif => {
|
|
||||||
notif.classList.add('hiding');
|
|
||||||
setTimeout(() => notif.remove(), 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add form submission handlers
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Initialize keyboard shortcuts
|
|
||||||
initKeyboardShortcuts();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Draft Auto-Save Functions
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// Save draft to localStorage
|
|
||||||
function saveDraft(key, content) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(key, content);
|
|
||||||
localStorage.setItem(key + '_timestamp', Date.now().toString());
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to save draft:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load draft from localStorage
|
|
||||||
function loadDraft(key) {
|
|
||||||
try {
|
|
||||||
return localStorage.getItem(key);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load draft:', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear draft from localStorage
|
|
||||||
function clearDraft(key) {
|
|
||||||
try {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
localStorage.removeItem(key + '_timestamp');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to clear draft:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get draft timestamp
|
|
||||||
function getDraftTimestamp(key) {
|
|
||||||
try {
|
|
||||||
const timestamp = localStorage.getItem(key + '_timestamp');
|
|
||||||
return timestamp ? parseInt(timestamp) : null;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to get draft timestamp:', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format time ago
|
|
||||||
function formatTimeAgo(timestamp) {
|
|
||||||
const now = Date.now();
|
|
||||||
const diff = now - timestamp;
|
|
||||||
const seconds = Math.floor(diff / 1000);
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
|
|
||||||
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
||||||
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
||||||
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
||||||
return 'just now';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show draft restoration indicator
|
|
||||||
function showDraftIndicator(content, timestamp, onRestore, onDiscard) {
|
|
||||||
const indicator = document.createElement('div');
|
|
||||||
indicator.className = 'draft-indicator';
|
|
||||||
indicator.innerHTML = `
|
|
||||||
<div class="draft-indicator-content">
|
|
||||||
<span class="draft-indicator-icon">📝</span>
|
|
||||||
<span class="draft-indicator-text">Draft from ${formatTimeAgo(timestamp)}</span>
|
|
||||||
<button type="button" class="draft-indicator-button restore">Restore</button>
|
|
||||||
<button type="button" class="draft-indicator-button discard">Discard</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const restoreBtn = indicator.querySelector('.restore');
|
|
||||||
const discardBtn = indicator.querySelector('.discard');
|
|
||||||
|
|
||||||
restoreBtn.addEventListener('click', () => {
|
|
||||||
onRestore(content);
|
|
||||||
indicator.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
discardBtn.addEventListener('click', () => {
|
|
||||||
onDiscard();
|
|
||||||
indicator.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
return indicator;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Validation Functions
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
function validateRequired(value, fieldName) {
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
return `${fieldName} is required`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUsername(username) {
|
|
||||||
if (!username || username.trim() === '') {
|
|
||||||
return 'Username is required';
|
|
||||||
}
|
|
||||||
if (username.length < 3) {
|
|
||||||
return 'Username must be at least 3 characters';
|
|
||||||
}
|
|
||||||
if (username.length > 50) {
|
|
||||||
return 'Username must be less than 50 characters';
|
|
||||||
}
|
|
||||||
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
|
||||||
return 'Username can only contain letters, numbers, and underscores';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validatePassword(password) {
|
|
||||||
if (!password || password.trim() === '') {
|
|
||||||
return 'Password is required';
|
|
||||||
}
|
|
||||||
if (password.length < 6) {
|
|
||||||
return 'Password must be at least 6 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFieldError(field, error) {
|
|
||||||
field.classList.add('error');
|
|
||||||
let errorDiv = field.nextElementSibling;
|
|
||||||
if (!errorDiv || !errorDiv.classList.contains('field-error')) {
|
|
||||||
errorDiv = document.createElement('div');
|
|
||||||
errorDiv.className = 'field-error';
|
|
||||||
field.parentNode.insertBefore(errorDiv, field.nextSibling);
|
|
||||||
}
|
|
||||||
errorDiv.textContent = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFieldError(field) {
|
|
||||||
field.classList.remove('error');
|
|
||||||
const errorDiv = field.nextElementSibling;
|
|
||||||
if (errorDiv && errorDiv.classList.contains('field-error')) {
|
|
||||||
errorDiv.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoResizeTextarea(textarea) {
|
|
||||||
textarea.style.height = 'auto';
|
|
||||||
textarea.style.height = textarea.scrollHeight + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCharacterCounter(textarea, maxLength) {
|
|
||||||
const counter = document.createElement('div');
|
|
||||||
counter.className = 'char-counter';
|
|
||||||
textarea.parentNode.insertBefore(counter, textarea.nextSibling);
|
|
||||||
|
|
||||||
const updateCounter = () => {
|
|
||||||
const length = textarea.value.length;
|
|
||||||
counter.textContent = `${length}/${maxLength}`;
|
|
||||||
if (length > maxLength * 0.9) {
|
|
||||||
counter.classList.add('warning');
|
|
||||||
} else {
|
|
||||||
counter.classList.remove('warning');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
textarea.addEventListener('input', updateCounter);
|
|
||||||
updateCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initRelativeTimestamps() {
|
function initRelativeTimestamps() {
|
||||||
// This function can be used to show relative timestamps
|
// This function can be used to show relative timestamps
|
||||||
// For now, it's just a placeholder
|
// For now, it's just a placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initApp() {
|
||||||
|
initRelativeTimestamps();
|
||||||
|
if (typeof initKeyboardShortcuts === 'function') {
|
||||||
|
initKeyboardShortcuts();
|
||||||
|
}
|
||||||
|
if (typeof initFormHandling === 'function') {
|
||||||
|
initFormHandling();
|
||||||
|
}
|
||||||
|
if (typeof initLikeButtons === 'function') {
|
||||||
|
initLikeButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initApp);
|
||||||
|
} else {
|
||||||
|
initApp();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
function saveDraft(key, content) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, content);
|
||||||
|
localStorage.setItem(key + '_timestamp', Date.now().toString());
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save draft:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDraft(key) {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load draft:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDraft(key) {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
localStorage.removeItem(key + '_timestamp');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to clear draft:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDraftTimestamp(key) {
|
||||||
|
try {
|
||||||
|
const timestamp = localStorage.getItem(key + '_timestamp');
|
||||||
|
return timestamp ? parseInt(timestamp, 10) : null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get draft timestamp:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimeAgo(timestamp) {
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - timestamp;
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||||
|
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||||
|
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||||
|
return 'just now';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDraftIndicator(content, timestamp, onRestore, onDiscard) {
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.className = 'draft-indicator';
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<div class="draft-indicator-content">
|
||||||
|
<span class="draft-indicator-icon">📝</span>
|
||||||
|
<span class="draft-indicator-text">Draft from ${formatTimeAgo(timestamp)}</span>
|
||||||
|
<button type="button" class="draft-indicator-button restore">Restore</button>
|
||||||
|
<button type="button" class="draft-indicator-button discard">Discard</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const restoreBtn = indicator.querySelector('.restore');
|
||||||
|
const discardBtn = indicator.querySelector('.discard');
|
||||||
|
|
||||||
|
restoreBtn.addEventListener('click', () => {
|
||||||
|
onRestore(content);
|
||||||
|
indicator.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
discardBtn.addEventListener('click', () => {
|
||||||
|
onDiscard();
|
||||||
|
indicator.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
return indicator;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormSubmit(form, button) {
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.classList.add('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitButtons = form.querySelectorAll('input[type="submit"], button[type="submit"]');
|
||||||
|
submitButtons.forEach(btn => {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.classList.add('loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeLoadingState(form) {
|
||||||
|
const submitButtons = form.querySelectorAll('input[type="submit"], button[type="submit"]');
|
||||||
|
submitButtons.forEach(btn => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.classList.remove('loading');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableEnterToSubmit(input, form) {
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoResizeTextarea(textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCharacterCounter(textarea, maxLength) {
|
||||||
|
const counter = document.createElement('div');
|
||||||
|
counter.className = 'char-counter';
|
||||||
|
textarea.parentNode.insertBefore(counter, textarea.nextSibling);
|
||||||
|
|
||||||
|
const updateCounter = () => {
|
||||||
|
const length = textarea.value.length;
|
||||||
|
counter.textContent = `${length}/${maxLength}`;
|
||||||
|
if (length > maxLength * 0.9) {
|
||||||
|
counter.classList.add('warning');
|
||||||
|
} else {
|
||||||
|
counter.classList.remove('warning');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
textarea.addEventListener('input', updateCounter);
|
||||||
|
updateCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFormHandling() {
|
||||||
|
document.querySelectorAll('form').forEach(form => {
|
||||||
|
form.addEventListener('submit', () => {
|
||||||
|
const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
|
||||||
|
handleFormSubmit(form, submitButton);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('textarea').forEach(textarea => {
|
||||||
|
textarea.addEventListener('input', () => autoResizeTextarea(textarea));
|
||||||
|
|
||||||
|
if (textarea.id === 'content' || textarea.name === 'content') {
|
||||||
|
addCharacterCounter(textarea, 10000);
|
||||||
|
}
|
||||||
|
if (textarea.id === 'bio' || textarea.name === 'bio') {
|
||||||
|
addCharacterCounter(textarea, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginForm = document.querySelector('form[action*="login"]');
|
||||||
|
if (loginForm) {
|
||||||
|
const passwordInput = loginForm.querySelector('input[type="password"]');
|
||||||
|
if (passwordInput) {
|
||||||
|
enableEnterToSubmit(passwordInput, loginForm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
function initLikeButtons() {
|
||||||
|
document.querySelectorAll('form[action*="/like/"]').forEach(form => {
|
||||||
|
form.addEventListener('submit', () => {
|
||||||
|
const button = form.querySelector('button[type="submit"]');
|
||||||
|
if (button) {
|
||||||
|
button.style.opacity = '0.5';
|
||||||
|
button.textContent = button.textContent + ' ✓';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
function initKeyboardShortcuts() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (activeElement && activeElement.tagName === 'TEXTAREA') {
|
||||||
|
const form = activeElement.closest('form');
|
||||||
|
if (form) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.requestSubmit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (document.activeElement && document.activeElement.tagName === 'TEXTAREA') {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.notification').forEach(notif => {
|
||||||
|
notif.classList.add('hiding');
|
||||||
|
setTimeout(() => notif.remove(), 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
function validateRequired(value, fieldName) {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return `${fieldName} is required`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateUsername(username) {
|
||||||
|
if (!username || username.trim() === '') {
|
||||||
|
return 'Username is required';
|
||||||
|
}
|
||||||
|
if (username.length < 3) {
|
||||||
|
return 'Username must be at least 3 characters';
|
||||||
|
}
|
||||||
|
if (username.length > 50) {
|
||||||
|
return 'Username must be less than 50 characters';
|
||||||
|
}
|
||||||
|
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||||
|
return 'Username can only contain letters, numbers, and underscores';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePassword(password) {
|
||||||
|
if (!password || password.trim() === '') {
|
||||||
|
return 'Password is required';
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
return 'Password must be at least 6 characters';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFieldError(field, error) {
|
||||||
|
field.classList.add('error');
|
||||||
|
let errorDiv = field.nextElementSibling;
|
||||||
|
if (!errorDiv || !errorDiv.classList.contains('field-error')) {
|
||||||
|
errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'field-error';
|
||||||
|
field.parentNode.insertBefore(errorDiv, field.nextSibling);
|
||||||
|
}
|
||||||
|
errorDiv.textContent = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFieldError(field) {
|
||||||
|
field.classList.remove('error');
|
||||||
|
const errorDiv = field.nextElementSibling;
|
||||||
|
if (errorDiv && errorDiv.classList.contains('field-error')) {
|
||||||
|
errorDiv.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
<head>
|
<head>
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
<script src="{{.StaticPath}}/validation.js" defer></script>
|
||||||
|
<script src="{{.StaticPath}}/drafts.js" defer></script>
|
||||||
|
<script src="{{.StaticPath}}/forms.js" defer></script>
|
||||||
|
<script src="{{.StaticPath}}/shortcuts.js" defer></script>
|
||||||
|
<script src="{{.StaticPath}}/likes.js" defer></script>
|
||||||
<script src="{{.StaticPath}}/app.js" defer></script>
|
<script src="{{.StaticPath}}/app.js" defer></script>
|
||||||
<script src="{{.StaticPath}}/chat.js" defer></script>
|
<script src="{{.StaticPath}}/chat.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue