diff --git a/static/app.js b/static/app.js index 0116868..5c56db0 100644 --- a/static/app.js +++ b/static/app.js @@ -45,22 +45,21 @@ function enableEnterToSubmit(input, form) { 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 + ' ✓'; - } - }); + } }); } +// 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); @@ -343,3 +342,88 @@ function showDraftIndicator(content, timestamp, onRestore, onDiscard) { 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 +} diff --git a/static/style.css b/static/style.css index 7ccb3d7..3d1cf89 100644 --- a/static/style.css +++ b/static/style.css @@ -343,6 +343,13 @@ p.thread-info { background-color: #ffe0f0; /* Light pink background */ animation: highlight-fade 2s ease-out; } +/* Grouped messages (hide header for consecutive messages from same user) */ +.chat-message.grouped { + margin-top: -4px; +} +.chat-message.grouped .chat-message-header { + display: none; +} @keyframes highlight-fade { from { background-color: #f582ae; diff --git a/templates/pages/chat.html b/templates/pages/chat.html index 2adba73..4d168db 100644 --- a/templates/pages/chat.html +++ b/templates/pages/chat.html @@ -437,7 +437,7 @@ ... {{range .Messages}} -
+
{{if .PfpFileID.Valid}} PFP @@ -627,6 +627,8 @@ } msgDiv.className = 'chat-message' + highlightClass; msgDiv.id = 'msg-' + msg.id; + msgDiv.dataset.user = msg.username; + msgDiv.dataset.reply = msg.replyTo || 0; let pfpHTML = `
`; if (msg.pfpFileId && msg.pfpFileId.Valid) { pfpHTML = `PFP`; @@ -647,6 +649,9 @@ `; messages.appendChild(msgDiv); + // Apply grouping + applyGrouping(); + // Scroll handling if (wasAtBottom) { messages.scrollTop = messages.scrollHeight; @@ -834,6 +839,9 @@ const messagesContainer = document.getElementById('chat-messages'); messagesContainer.scrollTop = messagesContainer.scrollHeight; + // Apply message grouping on load + applyGrouping(); + // Add scroll event listener messagesContainer.addEventListener('scroll', checkScrollPosition); @@ -909,6 +917,30 @@ // Initial check for scroll position checkScrollPosition(); }; + + function applyGrouping() { + const container = document.getElementById('chat-messages'); + const msgs = Array.from(container.querySelectorAll('.chat-message')).filter(el => el.id.startsWith('msg-')); + + for (let i = 0; i < msgs.length; i++) { + const curr = msgs[i]; + + // Remove grouped class first + curr.classList.remove('grouped'); + + if (i === 0) continue; // First message is never grouped + + const prev = msgs[i - 1]; + const currUser = curr.dataset.user; + const prevUser = prev.dataset.user; + const currReply = parseInt(curr.dataset.reply) || -1; + + // Group if same user and not a reply (reply is -1 or 0 when there's no reply) + if (currUser === prevUser && (currReply <= 0)) { + curr.classList.add('grouped'); + } + } + }