diff options
| -rw-r--r-- | static/css/style.css | 6 | ||||
| -rw-r--r-- | static/js/poll.js | 75 |
2 files changed, 73 insertions, 8 deletions
diff --git a/static/css/style.css b/static/css/style.css index 7e0c888..c04ef2d 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -209,6 +209,12 @@ body:not(.poll-view) .container { padding-top: 2rem; padding-bottom: 2rem; } .attendees-mode-toggle.active { opacity: 1; font-weight: 600; background: var(--color-primary-light); } .attendees-mode-separator { opacity: 0.3; margin: 0 0.25rem; } +#results-table th[data-sort] { cursor: pointer; user-select: none; } +#results-table th[data-sort]:hover { opacity: 0.7; } + +.sortable-header { display: inline-flex; align-items: center; gap: 0.25rem; } +.sortable-header .sort-arrow { font-size: 0.75em; min-width: 0.75em; color: var(--color-primary); } + .submit-section { margin-top: var(--layout-spacing); background: var(--color-surface); padding: var(--layout-spacing); border-radius: var(--layout-border-radius); box-shadow: var(--shadow-sm); display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; } .guest-info { display: flex; gap: 0.5rem; flex: 1; } .guest-info input { padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--layout-border-radius-sm); flex: 1; background: var(--color-input-bg); color: var(--color-text); } diff --git a/static/js/poll.js b/static/js/poll.js index 8216180..899a00f 100644 --- a/static/js/poll.js +++ b/static/js/poll.js @@ -15,6 +15,9 @@ let isDayWise = false; let hoveredPane = null; // 'calendar' or 'time' let viewMode = false; // true = show all availability, false = show only yours let attendeesMode = 'available'; // 'available' or 'unavailable' +let sortColumn = 'available'; // 'date' or 'available' +let sortDirection = 'desc'; // 'asc' or 'desc' +let hasUserSorted = false; // Track if user has clicked to sort let allAvailability = { dates: new Map(), times: new Map() }; // Maps date/time -> count let myAvailability = { dates: new Set(), times: new Set() }; let maxDateCount = 0; @@ -551,8 +554,34 @@ function processPollData(data) { // Update table header and notes pane for day-wise polls const slotHeader = document.querySelector('#results-table thead th:first-child'); if (slotHeader) { - slotHeader.textContent = isDayWise ? 'Date' : 'Time Slot'; + slotHeader.innerHTML = `<span class="sortable-header" data-sort="date">${isDayWise ? 'Date' : 'Time Slot'} <span class="sort-arrow"></span></span>`; + slotHeader.style.cursor = 'pointer'; + slotHeader.dataset.sort = 'date'; } + const availableHeader = document.querySelector('#results-table thead th:nth-child(2)'); + if (availableHeader) { + availableHeader.innerHTML = '<span class="sortable-header" data-sort="available">Available <span class="sort-arrow"></span></span>'; + availableHeader.style.cursor = 'pointer'; + availableHeader.dataset.sort = 'available'; + } + + // Add click handlers to entire th elements for sortable headers + document.querySelectorAll('#results-table thead th[data-sort]').forEach(th => { + th.onclick = () => { + const newSortColumn = th.dataset.sort; + hasUserSorted = true; + if (sortColumn === newSortColumn) { + // Toggle direction + sortDirection = sortDirection === 'desc' ? 'asc' : 'desc'; + } else { + // New column - default to desc for available, asc for date + sortColumn = newSortColumn; + sortDirection = newSortColumn === 'available' ? 'desc' : 'asc'; + } + updateSortIndicators(); + renderResults(pollData.responses); + }; + }); const notesPane = document.getElementById('notes-pane'); if (notesPane && notesPane.querySelector('.notes-pane-empty')) { notesPane.innerHTML = `<span class="notes-pane-empty">Click a ${isDayWise ? 'date' : 'time slot'} to view notes</span>`; @@ -577,6 +606,22 @@ function processPollData(data) { } +function updateSortIndicators() { + // Only show sort indicators after user has clicked to sort + if (!hasUserSorted) return; + + document.querySelectorAll('.sortable-header').forEach(header => { + const arrow = header.querySelector('.sort-arrow'); + if (header.dataset.sort === sortColumn) { + header.classList.add('active'); + arrow.textContent = sortDirection === 'desc' ? '▼' : '▲'; + } else { + header.classList.remove('active'); + arrow.textContent = ''; + } + }); +} + function renderResults(responses) { const tbody = document.querySelector('#results-table tbody'); const slotMap = {}; @@ -614,14 +659,27 @@ function renderResults(responses) { }); }); - // Sort by available count (descending), then by date+time (ascending) - // Sorting is always based on available attendees, regardless of display mode + // Sort based on user's selected column and direction const sorted = Object.entries(slotMap).sort((a, b) => { - const countDiff = b[1].attendees.length - a[1].attendees.length; - if (countDiff !== 0) return countDiff; - const dateCmp = a[1].date.localeCompare(b[1].date); - if (dateCmp !== 0) return dateCmp; - return a[1].start - b[1].start; + if (sortColumn === 'available') { + // Sort by available count + const countDiff = sortDirection === 'desc' + ? b[1].attendees.length - a[1].attendees.length + : a[1].attendees.length - b[1].attendees.length; + if (countDiff !== 0) return countDiff; + // Secondary sort by date+time + const dateCmp = a[1].date.localeCompare(b[1].date); + if (dateCmp !== 0) return dateCmp; + return a[1].start - b[1].start; + } else { + // Sort by date+time + const dateCmp = a[1].date.localeCompare(b[1].date); + if (dateCmp !== 0) return sortDirection === 'asc' ? dateCmp : -dateCmp; + const timeDiff = a[1].start - b[1].start; + if (timeDiff !== 0) return sortDirection === 'asc' ? timeDiff : -timeDiff; + // Secondary sort by count (descending) + return b[1].attendees.length - a[1].attendees.length; + } }); const totalPeople = allParticipants.length; @@ -707,6 +765,7 @@ function renderResults(responses) { }); applyAttendeeFilter(); + updateSortIndicators(); } function toggleAttendeeFilter(name) { |
