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
|
||||
|
||||
// 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() {
|
||||
// This function can be used to show relative timestamps
|
||||
// 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>
|
||||
<title>{{.Title}}</title>
|
||||
<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}}/chat.js" defer></script>
|
||||
</head>
|
||||
|
|
|
|||
Loading…
Reference in New Issue