feat: add table sorting and advanced filters
Some checks are pending
CI / lint (push) Waiting to run
Deploy / deploy (push) Waiting to run

This commit is contained in:
Adrian Miesikowski 2026-02-18 21:49:18 +01:00
parent 6b5d385463
commit 4f93293927

View File

@ -17,6 +17,8 @@
.table-wrap{overflow:auto;-webkit-overflow-scrolling:touch} .table-wrap{overflow:auto;-webkit-overflow-scrolling:touch}
table{width:100%;border-collapse:collapse;font-size:13px;min-width:760px} table{width:100%;border-collapse:collapse;font-size:13px;min-width:760px}
th,td{border-bottom:1px solid #24315f;padding:8px;text-align:left;vertical-align:top} th,td{border-bottom:1px solid #24315f;padding:8px;text-align:left;vertical-align:top}
th.sortable{cursor:pointer;user-select:none}
th.sortable:hover{background:#1a2550}
a{color:#9ec5ff;text-decoration:underline;text-underline-offset:2px} a{color:#9ec5ff;text-decoration:underline;text-underline-offset:2px}
a:hover{color:#cfe2ff} a:hover{color:#cfe2ff}
td b{color:#ffffff;font-weight:700;letter-spacing:0.1px} td b{color:#ffffff;font-weight:700;letter-spacing:0.1px}
@ -53,6 +55,37 @@
<label>TLD <label>TLD
<select id="tld"><option value="">Wszystkie</option><option>pl</option><option>com</option><option>ai</option></select> <select id="tld"><option value="">Wszystkie</option><option>pl</option><option>com</option><option>ai</option></select>
</label> </label>
<label>LLM decyzja
<select id="decisionFilter">
<option value="">Wszystkie</option>
<option value="buy_now">buy_now</option>
<option value="watch">watch</option>
<option value="skip">skip</option>
<option value="none">brak</option>
</select>
</label>
<label>Status
<select id="statusFilter">
<option value="">Wszystkie</option>
<option value="available">available</option>
<option value="unknown">unknown</option>
</select>
</label>
<label>Min score <input id="minScore" type="number" min="0" max="100" step="1" placeholder="np. 70" /></label>
<label>Sortuj po
<select id="sortBy">
<option value="score">score</option>
<option value="llm_score">llm_score</option>
<option value="domain">domena</option>
<option value="scanned_at">czas skanu</option>
</select>
</label>
<label>Kierunek
<select id="sortDir">
<option value="desc">malejąco</option>
<option value="asc">rosnąco</option>
</select>
</label>
<label class="small"><input type="checkbox" id="onlyNew" /> Tylko nowe domeny</label> <label class="small"><input type="checkbox" id="onlyNew" /> Tylko nowe domeny</label>
<label class="small"><input type="checkbox" id="autoRefresh" checked /> Auto-refresh 60s</label> <label class="small"><input type="checkbox" id="autoRefresh" checked /> Auto-refresh 60s</label>
<button id="reload">Odśwież</button> <button id="reload">Odśwież</button>
@ -64,7 +97,7 @@
<h2>TOP domen + porównanie cen (rejestracja / odnowienie)</h2> <h2>TOP domen + porównanie cen (rejestracja / odnowienie)</h2>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead><tr><th>Domena</th><th>TLD</th><th>Score</th><th>LLM</th><th>Status</th><th>Run</th><th>Skan</th><th>Oferty (top 3)</th></tr></thead> <thead><tr><th class="sortable" data-sort="domain">Domena</th><th class="sortable" data-sort="tld">TLD</th><th class="sortable" data-sort="score">Score</th><th class="sortable" data-sort="llm_score">LLM</th><th class="sortable" data-sort="status">Status</th><th>Run</th><th class="sortable" data-sort="scanned_at">Skan</th><th>Oferty (top 3)</th></tr></thead>
<tbody id="rows"></tbody> <tbody id="rows"></tbody>
</table> </table>
</div> </div>
@ -73,8 +106,10 @@
<script> <script>
let domains = []; let domains = [];
let visibleDomains = [];
let livePriceByTld = {}; let livePriceByTld = {};
let viewMode = 'current'; let viewMode = 'current';
let metaBase = '';
function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)} zł`; } function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)} zł`; }
@ -146,16 +181,61 @@ async function loadDomains(){
const d = await (await fetch(u)).json(); const d = await (await fetch(u)).json();
domains = d.domains || []; domains = d.domains || [];
if(viewMode === 'all'){ if(viewMode === 'all'){
document.getElementById('meta').textContent = `widok: wszystkie rekordy | limit: ${d.meta?.limit||'—'} | widoczne: ${domains.length}`; metaBase = `widok: wszystkie rekordy | limit: ${d.meta?.limit||'—'} | widoczne: ${domains.length}`;
} else { } else {
document.getElementById('meta').textContent = `runId: ${d.meta?.runId||'—'} | prev: ${d.meta?.prevRunId||'—'} | scannedAt: ${d.meta?.scannedAt||'—'} | nowe: ${d.meta?.newCount||0} | widoczne: ${domains.length}`; metaBase = `runId: ${d.meta?.runId||'—'} | prev: ${d.meta?.prevRunId||'—'} | scannedAt: ${d.meta?.scannedAt||'—'} | nowe: ${d.meta?.newCount||0} | widoczne: ${domains.length}`;
} }
document.getElementById('meta').textContent = metaBase;
}
function applyFiltersAndSort(){
const decision = document.getElementById('decisionFilter').value;
const status = document.getElementById('statusFilter').value;
const minScoreRaw = document.getElementById('minScore').value;
const minScore = minScoreRaw === '' ? null : Number(minScoreRaw);
const sortBy = document.getElementById('sortBy').value;
const sortDir = document.getElementById('sortDir').value;
let arr = (domains || []).filter(d => {
if (decision === 'none' && d.decision) return false;
if (decision && decision !== 'none' && (d.decision || '') !== decision) return false;
if (status && (d.status || 'available') !== status) return false;
if (minScore !== null && Number(d.score || 0) < minScore) return false;
return true;
});
const dir = sortDir === 'asc' ? 1 : -1;
arr.sort((a,b)=>{
let av = a?.[sortBy];
let bv = b?.[sortBy];
if (sortBy === 'llm_score') {
av = av === null || av === undefined ? -1 : Number(av);
bv = bv === null || bv === undefined ? -1 : Number(bv);
} else if (sortBy === 'score') {
av = Number(av || 0);
bv = Number(bv || 0);
} else if (sortBy === 'scanned_at') {
av = av ? Date.parse(av) : 0;
bv = bv ? Date.parse(bv) : 0;
} else {
av = (av || '').toString().toLowerCase();
bv = (bv || '').toString().toLowerCase();
}
if (av < bv) return -1 * dir;
if (av > bv) return 1 * dir;
return 0;
});
visibleDomains = arr;
} }
function renderRows(){ function renderRows(){
applyFiltersAndSort();
const rows = document.getElementById('rows'); const rows = document.getElementById('rows');
rows.innerHTML=''; rows.innerHTML='';
for(const d of domains.slice(0,300)){ for(const d of visibleDomains.slice(0,300)){
const tr = document.createElement('tr'); const tr = document.createElement('tr');
const checkUrl = domainCheckUrl(d.domain); const checkUrl = domainCheckUrl(d.domain);
const offers = topOffersByTld(d.tld); const offers = topOffersByTld(d.tld);
@ -181,6 +261,10 @@ function renderRows(){
<td>${offersHtml}</td>`; <td>${offersHtml}</td>`;
rows.appendChild(tr); rows.appendChild(tr);
} }
const meta = document.getElementById('meta');
if(meta){
meta.textContent = `${metaBase} | po filtrach: ${visibleDomains.length}`;
}
} }
async function refreshAll(){ async function refreshAll(){
@ -223,6 +307,25 @@ document.getElementById('q').addEventListener('input', async()=>{
}); });
document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();}); document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();}); document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); await loadLivePricesForDomains(domains); renderRows();});
document.getElementById('decisionFilter').addEventListener('change', renderRows);
document.getElementById('statusFilter').addEventListener('change', renderRows);
document.getElementById('minScore').addEventListener('input', renderRows);
document.getElementById('sortBy').addEventListener('change', renderRows);
document.getElementById('sortDir').addEventListener('change', renderRows);
document.querySelectorAll('th.sortable').forEach(th=>{
th.addEventListener('click', ()=>{
const key = th.dataset.sort;
const sortByEl = document.getElementById('sortBy');
const sortDirEl = document.getElementById('sortDir');
if(sortByEl.value === key){
sortDirEl.value = sortDirEl.value === 'asc' ? 'desc' : 'asc';
} else {
sortByEl.value = key;
sortDirEl.value = (key === 'domain' || key === 'tld' || key === 'status') ? 'asc' : 'desc';
}
renderRows();
});
});
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh); document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
refreshAll(); refreshAll();