diff --git a/public/index.html b/public/index.html index 14c10e7..9000c44 100644 --- a/public/index.html +++ b/public/index.html @@ -1001,6 +1001,37 @@ } .results-header h2 { font-size: 1.1rem; color: var(--accent); margin-bottom: 4px; } .results-header p { font-size: 0.75rem; color: var(--text-muted); } + .sort-bar { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-bottom: 10px; + flex-wrap: wrap; + } + .sort-bar label { + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.9px; + color: var(--text-muted); + font-weight: 700; + } + .sort-select { + min-width: 220px; + background: rgba(19,22,31,0.96); + border: 1px solid rgba(0,212,255,0.18); + color: #e6f7ff; + border-radius: 10px; + padding: 9px 10px; + font-size: 0.78rem; + outline: none; + appearance: none; + cursor: pointer; + } + .sort-select:focus { + border-color: rgba(0,212,255,0.48); + box-shadow: 0 0 0 2px rgba(0,212,255,0.08); + } .results-category { margin-bottom: 20px; background: var(--surface); @@ -1278,6 +1309,16 @@
+
+ + +
+
Connecting…
POLLS OPEN
@@ -1369,6 +1410,7 @@ budgetScenarios: [], priceUpdatedAt: '', priceHistoryRunCount: 0, + sortMode: localStorage.getItem('cabo_sort_mode') || 'vote-desc', priceSourceSelections: (() => { try { return JSON.parse(localStorage.getItem('cabo_price_source_selections') || '{}'); @@ -1648,6 +1690,55 @@ render(); } + function setSortMode(sortMode) { + const allowedModes = new Set(['vote-desc', 'vote-asc', 'price-asc', 'price-desc']); + state.sortMode = allowedModes.has(sortMode) ? sortMode : 'vote-desc'; + localStorage.setItem('cabo_sort_mode', state.sortMode); + render(); + } + + function getOptionOrderIndexMap() { + return new Map(state.options.map((opt, index) => [opt.id, index])); + } + + function getSelectedOptionPrice(opt) { + const selectedSeries = getOptionSourceSeries(opt); + const selectedPoint = selectedSeries.at(-1) || opt.latestPricePoint || null; + return typeof selectedPoint?.price === 'number' ? selectedPoint.price : null; + } + + function sortOptionsByMode(opts, orderIndexMap) { + const mode = state.sortMode || 'vote-desc'; + const ascending = mode.endsWith('asc'); + const isVoteSort = mode.startsWith('vote'); + return [...opts].sort((a, b) => { + const aIndex = orderIndexMap.get(a.id) ?? 0; + const bIndex = orderIndexMap.get(b.id) ?? 0; + + if (isVoteSort) { + const aVotes = getVoteEntries(a).length; + const bVotes = getVoteEntries(b).length; + if (aVotes !== bVotes) { + return ascending ? aVotes - bVotes : bVotes - aVotes; + } + } else { + const aPrice = getSelectedOptionPrice(a); + const bPrice = getSelectedOptionPrice(b); + const aHasPrice = typeof aPrice === 'number'; + const bHasPrice = typeof bPrice === 'number'; + + if (aHasPrice && bHasPrice && aPrice !== bPrice) { + return ascending ? aPrice - bPrice : bPrice - aPrice; + } + if (aHasPrice !== bHasPrice) { + return aHasPrice ? -1 : 1; + } + } + + return aIndex - bIndex; + }); + } + function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') @@ -1995,6 +2086,10 @@ // ── Render options ──────────────────────────────────────── function render() { const list = document.getElementById('optionsList'); + const sortModeSelect = document.getElementById('sortModeSelect'); + if (sortModeSelect && sortModeSelect.value !== state.sortMode) { + sortModeSelect.value = state.sortMode; + } // ── Results tab ────────────────────────────────────────── if (activeTab === 'results') { @@ -2062,7 +2157,7 @@ return; } - const sorted = [...opts]; + const sorted = sortOptionsByMode(opts, getOptionOrderIndexMap()); const maxVotes = sorted[0] ? getVoteEntries(sorted[0]).length : 1; const budgetBoard = activeTab === 'budget' ? renderBudgetBoard() : '';