feat: add option sort dropdown
This commit is contained in:
@@ -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 @@
|
||||
|
||||
<!-- Main -->
|
||||
<main>
|
||||
<div class="sort-bar" id="sortBar">
|
||||
<label for="sortModeSelect">Sort by</label>
|
||||
<select id="sortModeSelect" class="sort-select" onchange="setSortMode(this.value)">
|
||||
<option value="vote-desc">Votes: High to Low</option>
|
||||
<option value="vote-asc">Votes: Low to High</option>
|
||||
<option value="price-asc">Price: Low to High</option>
|
||||
<option value="price-desc">Price: High to Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="status-bar">
|
||||
<div><span class="status-dot" id="wsDot"></span><span id="wsStatus">Connecting…</span></div>
|
||||
<div><span class="polls-badge open" id="pollsBadge">POLLS OPEN</span></div>
|
||||
@@ -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() : '';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user