From 2c7634da43cb43c44ca3912991a2358fd4083cbe Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Fri, 20 Feb 2026 13:37:46 -0300 Subject: [PATCH] split stuff so i can read better --- static/app.js | 442 ++----------------------------------------- static/drafts.js | 78 ++++++++ static/forms.js | 212 +++++++++++++++++++++ static/likes.js | 11 ++ static/shortcuts.js | 25 +++ static/validation.js | 51 +++++ templates/base.html | 5 + 7 files changed, 401 insertions(+), 423 deletions(-) create mode 100644 static/drafts.js create mode 100644 static/forms.js create mode 100644 static/likes.js create mode 100644 static/shortcuts.js create mode 100644 static/validation.js diff --git a/static/app.js b/static/app.js index 5c56db0..bcdf04e 100644 --- a/static/app.js +++ b/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 = ` -
- 📝 - Draft from ${formatTimeAgo(timestamp)} - - -
- `; - - 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(); +} diff --git a/static/drafts.js b/static/drafts.js new file mode 100644 index 0000000..23c831a --- /dev/null +++ b/static/drafts.js @@ -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 = ` +
+ 📝 + Draft from ${formatTimeAgo(timestamp)} + + +
+ `; + + 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; +} diff --git a/static/forms.js b/static/forms.js new file mode 100644 index 0000000..cb21ff4 --- /dev/null +++ b/static/forms.js @@ -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); + } + }); + }); +} diff --git a/static/likes.js b/static/likes.js new file mode 100644 index 0000000..eff8075 --- /dev/null +++ b/static/likes.js @@ -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 + ' ✓'; + } + }); + }); +} diff --git a/static/shortcuts.js b/static/shortcuts.js new file mode 100644 index 0000000..9b7cd0e --- /dev/null +++ b/static/shortcuts.js @@ -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); + }); + } + }); +} diff --git a/static/validation.js b/static/validation.js new file mode 100644 index 0000000..add236e --- /dev/null +++ b/static/validation.js @@ -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(); + } +} diff --git a/templates/base.html b/templates/base.html index 10b8efa..822f64d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,6 +4,11 @@ {{.Title}} + + + + +