From 06c7485a7d05de86f9898e388161e8d932d5f3e6 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 12 Jan 2026 14:28:50 -1000 Subject: Modernize frontend with tabs, HTMX, and Tailwind build pipeline Complete UI overhaul implementing modern design patterns with HTMX for dynamic updates, proper Tailwind build pipeline, and improved UX. Build Pipeline: - Add npm + PostCSS + Tailwind CSS configuration - Custom design system with brand colors - Compiled CSS: 27KB (vs 3MB CDN), 99% reduction - Makefile for unified build commands - Inter font for improved typography Tab Interface: - Separate Tasks tab from Notes tab using HTMX - Partial page updates without full refreshes - Tab state management with proper refresh handling - New endpoints: /tabs/tasks, /tabs/notes, /tabs/refresh Template Architecture: - Modular partials system (7 reusable components) - Cleaner separation of concerns Empty Board Management: - Active boards in main 3-column grid - Empty boards in collapsible section - Reduces visual clutter Visual Design Enhancements: - Inter font, brand color accents - Improved typography hierarchy and spacing - Enhanced card styling with hover effects Co-Authored-By: Claude Sonnet 4.5 --- web/static/js/app.js | 190 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 146 insertions(+), 44 deletions(-) (limited to 'web/static/js/app.js') diff --git a/web/static/js/app.js b/web/static/js/app.js index a96c05d..b68b12a 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1,77 +1,179 @@ -// Personal Dashboard JavaScript +// Personal Dashboard JavaScript with HTMX Integration -// Auto-refresh every 5 minutes +// Constants const AUTO_REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes in milliseconds -// Initialize auto-refresh on page load +// Track current active tab +let currentTab = 'tasks'; +let autoRefreshTimer = null; + +// Initialize on page load document.addEventListener('DOMContentLoaded', function() { - // Set up auto-refresh - setInterval(autoRefresh, AUTO_REFRESH_INTERVAL); + console.log('Dashboard initialized'); + + // Set up HTMX event listeners + setupHtmxListeners(); + + // Start auto-refresh + startAutoRefresh(); }); -// Auto-refresh function -async function autoRefresh() { - console.log('Auto-refreshing data...'); - try { - const response = await fetch('/api/refresh', { - method: 'POST' - }); +// HTMX Event Listeners +function setupHtmxListeners() { + // Before HTMX request + document.body.addEventListener('htmx:beforeRequest', function(evt) { + const target = evt.detail.target; + if (target.id === 'tab-content') { + // Show loading state + target.classList.add('opacity-50', 'pointer-events-none'); + } + }); - if (response.ok) { - // Reload the page to show updated data - window.location.reload(); + // After HTMX request completes + document.body.addEventListener('htmx:afterRequest', function(evt) { + const target = evt.detail.target; + if (target.id === 'tab-content') { + // Hide loading state + target.classList.remove('opacity-50', 'pointer-events-none'); + + // Update timestamp + updateLastUpdatedTime(); } - } catch (error) { - console.error('Auto-refresh failed:', error); - } + }); + + // Handle HTMX errors + document.body.addEventListener('htmx:responseError', function(evt) { + console.error('HTMX request failed:', evt.detail); + alert('Failed to load content. Please try again.'); + + // Remove loading state + const target = evt.detail.target; + if (target) { + target.classList.remove('opacity-50', 'pointer-events-none'); + } + }); +} + +// Tab Management +function setActiveTab(button) { + // Remove active class from all tabs + document.querySelectorAll('.tab-button').forEach(tab => { + tab.classList.remove('tab-button-active'); + }); + + // Add active class to clicked tab + button.classList.add('tab-button-active'); + + // Extract tab name from hx-get attribute + const endpoint = button.getAttribute('hx-get'); + currentTab = endpoint.split('/').pop(); // "tasks" or "notes" + + console.log('Switched to tab:', currentTab); + + // Reset auto-refresh timer when switching tabs + resetAutoRefresh(); } -// Manual refresh function +// Manual Refresh async function refreshData() { - const button = event.target; - const originalText = button.textContent; + const button = event.target.closest('button'); + const refreshText = document.getElementById('refresh-text'); + const originalText = refreshText.textContent; // Show loading state button.disabled = true; - button.innerHTML = 'Refreshing...'; + refreshText.innerHTML = ` + + + + + Refreshing... + `; try { - const response = await fetch('/api/refresh', { + // Refresh current tab + const response = await fetch(`/tabs/refresh?tab=${currentTab}`, { method: 'POST' }); - if (response.ok) { - // Update last updated time - const now = new Date(); - const timeString = now.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit' - }); - document.getElementById('last-updated').textContent = timeString; - - // Reload the page to show updated data - setTimeout(() => { - window.location.reload(); - }, 500); - } else { - throw new Error('Refresh failed'); - } + if (!response.ok) throw new Error('Refresh failed'); + + // Get HTML response and update tab content + const html = await response.text(); + document.getElementById('tab-content').innerHTML = html; + + // Update timestamp + updateLastUpdatedTime(); + + // Reset auto-refresh timer + resetAutoRefresh(); + + console.log('Manual refresh successful'); + } catch (error) { console.error('Refresh failed:', error); alert('Failed to refresh data. Please try again.'); + } finally { + // Restore button state button.disabled = false; - button.textContent = originalText; + refreshText.textContent = originalText; + } +} + +// Auto-refresh Functions +function startAutoRefresh() { + if (autoRefreshTimer) { + clearInterval(autoRefreshTimer); + } + autoRefreshTimer = setInterval(autoRefresh, AUTO_REFRESH_INTERVAL); + console.log('Auto-refresh started (5 min interval)'); +} + +function resetAutoRefresh() { + clearInterval(autoRefreshTimer); + startAutoRefresh(); +} + +async function autoRefresh() { + console.log(`Auto-refreshing ${currentTab} tab...`); + + try { + const response = await fetch(`/tabs/refresh?tab=${currentTab}`, { + method: 'POST' + }); + + if (response.ok) { + const html = await response.text(); + document.getElementById('tab-content').innerHTML = html; + updateLastUpdatedTime(); + console.log('Auto-refresh successful'); + } + } catch (error) { + console.error('Auto-refresh failed:', error); + } +} + +// Update Last Updated Time +function updateLastUpdatedTime() { + const now = new Date(); + const timeString = now.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit' + }); + const element = document.getElementById('last-updated'); + if (element) { + element.textContent = timeString; } } -// Filter tasks by status +// Filter tasks by status (Phase 2 feature) function filterTasks(status) { - // This will be implemented in Phase 2 console.log('Filter tasks:', status); + // To be implemented in Phase 2 } -// Toggle task completion +// Toggle task completion (Phase 2 feature) function toggleTask(taskId) { - // This will be implemented in Phase 2 console.log('Toggle task:', taskId); + // To be implemented in Phase 2 } -- cgit v1.2.3