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 h2 { font-size: 1.1rem; color: var(--accent); margin-bottom: 4px; }
|
||||||
.results-header p { font-size: 0.75rem; color: var(--text-muted); }
|
.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 {
|
.results-category {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@@ -1278,6 +1309,16 @@
|
|||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<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 class="status-bar">
|
||||||
<div><span class="status-dot" id="wsDot"></span><span id="wsStatus">Connecting…</span></div>
|
<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>
|
<div><span class="polls-badge open" id="pollsBadge">POLLS OPEN</span></div>
|
||||||
@@ -1369,6 +1410,7 @@
|
|||||||
budgetScenarios: [],
|
budgetScenarios: [],
|
||||||
priceUpdatedAt: '',
|
priceUpdatedAt: '',
|
||||||
priceHistoryRunCount: 0,
|
priceHistoryRunCount: 0,
|
||||||
|
sortMode: localStorage.getItem('cabo_sort_mode') || 'vote-desc',
|
||||||
priceSourceSelections: (() => {
|
priceSourceSelections: (() => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(localStorage.getItem('cabo_price_source_selections') || '{}');
|
return JSON.parse(localStorage.getItem('cabo_price_source_selections') || '{}');
|
||||||
@@ -1648,6 +1690,55 @@
|
|||||||
render();
|
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) {
|
function escapeHtml(value) {
|
||||||
return String(value ?? '')
|
return String(value ?? '')
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -1995,6 +2086,10 @@
|
|||||||
// ── Render options ────────────────────────────────────────
|
// ── Render options ────────────────────────────────────────
|
||||||
function render() {
|
function render() {
|
||||||
const list = document.getElementById('optionsList');
|
const list = document.getElementById('optionsList');
|
||||||
|
const sortModeSelect = document.getElementById('sortModeSelect');
|
||||||
|
if (sortModeSelect && sortModeSelect.value !== state.sortMode) {
|
||||||
|
sortModeSelect.value = state.sortMode;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Results tab ──────────────────────────────────────────
|
// ── Results tab ──────────────────────────────────────────
|
||||||
if (activeTab === 'results') {
|
if (activeTab === 'results') {
|
||||||
@@ -2062,7 +2157,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted = [...opts];
|
const sorted = sortOptionsByMode(opts, getOptionOrderIndexMap());
|
||||||
const maxVotes = sorted[0] ? getVoteEntries(sorted[0]).length : 1;
|
const maxVotes = sorted[0] ? getVoteEntries(sorted[0]).length : 1;
|
||||||
const budgetBoard = activeTab === 'budget' ? renderBudgetBoard() : '';
|
const budgetBoard = activeTab === 'budget' ? renderBudgetBoard() : '';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user