domainhunter-panel/index.html
Adrian Miesikowski c6a4bdfe30
Some checks are pending
CI / lint (push) Waiting to run
Deploy / deploy (push) Waiting to run
feat: add auto-refresh and new-domain filter + deploy workflow
2026-02-17 22:59:14 +01:00

230 lines
8.7 KiB
HTML

<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Domain Hunter Panel</title>
<style>
body{font-family:Inter,Arial,sans-serif;background:#0b1020;color:#e8ecff;margin:0;padding:12px}
.wrap{max-width:1200px;margin:0 auto}
.card{background:#121933;border:1px solid #2a376b;border-radius:12px;padding:12px;margin-bottom:12px}
h1{margin:0 0 8px;font-size:24px} h2{margin:0 0 10px;font-size:17px}
.muted{color:#9aa6d1}
input,button,select{background:#0f1530;color:#e8ecff;border:1px solid #3a4a87;border-radius:8px;padding:8px}
.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end}
.controls label{display:flex;flex-direction:column;gap:6px;min-width:150px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.table-wrap{overflow:auto;-webkit-overflow-scrolling:touch}
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}
.pill{display:inline-flex;align-items:center;gap:6px;background:#1f2b56;border:1px solid #3a4a87;border-radius:999px;padding:4px 8px;margin:4px 6px 4px 0;font-size:12px}
.pill input{width:78px;padding:4px 6px}
.ok{color:#63db8f}.bad{color:#ff8b8b}
.chip{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;background:#264a32;color:#9df5bc;border:1px solid #3f8b5b}
.small{font-size:12px}
@media (max-width: 980px){.grid{grid-template-columns:1fr}}
@media (max-width: 700px){
body{padding:10px}
.controls{flex-direction:column;align-items:stretch}
.controls label{width:100%;min-width:0}
button{width:100%}
}
</style>
</head>
<body>
<div class="wrap">
<h1>Domain Hunter Panel</h1>
<p class="muted">Dynamicznie z DB: domeny z Mongo + ceny rejestratorów (zapisywane na serwerze).</p>
<div class="grid">
<div class="card">
<h2>Filtr domen</h2>
<div class="controls">
<label>Szukaj <input id="q" placeholder="np. lead, ai, crm" /></label>
<label>TLD
<select id="tld"><option value="">Wszystkie</option><option>pl</option><option>com</option><option>ai</option></select>
</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>
<button id="reload">Odśwież</button>
</div>
<p class="muted" id="meta"></p>
</div>
<div class="card">
<h2>Cennik rejestratorów</h2>
<p class="muted">Edycja zapisuje się bezpośrednio na serwerze (SQLite), bez localStorage.</p>
<div id="pricing"></div>
</div>
</div>
<div class="card">
<h2>TOP domen + najlepszy koszt</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Domena</th><th>Score</th><th>Najtaniej kupisz</th><th>Najtaniej odnowisz</th><th>2 lata</th></tr></thead>
<tbody id="rows"></tbody>
</table>
</div>
</div>
</div>
<script>
let domains = [];
let registrarRows = [];
let registrars = [];
function money(v){ return (v===null || v===undefined || Number.isNaN(Number(v))) ? '—' : `${Number(v).toFixed(2)}`; }
function groupPrices(items){
const map = new Map();
for(const x of items){
if(!map.has(x.registrar)) map.set(x.registrar, {name:x.registrar, url:x.url, pricing:{}});
const r = map.get(x.registrar);
r.pricing[x.tld] = {register:x.register_price, renew:x.renew_price};
}
return [...map.values()];
}
function priceFor(reg, tld, k){
const p = reg.pricing?.[tld]?.[k];
return (p===null || p===undefined || p==='') ? null : Number(p);
}
function best(regs, tld, k){
let b = null;
for(const r of regs){
const v = priceFor(r,tld,k);
if(v === null || Number.isNaN(v)) continue;
if(!b || v < b.value) b = {name:r.name,url:r.url,value:v};
}
return b;
}
function best2y(regs, tld){
let b = null;
for(const r of regs){
const a = priceFor(r,tld,'register');
const c = priceFor(r,tld,'renew');
if(a===null || c===null || Number.isNaN(a) || Number.isNaN(c)) continue;
const v = a+c;
if(!b || v < b.value) b = {name:r.name,url:r.url,value:v};
}
return b;
}
async function loadDomains(){
const q = document.getElementById('q').value.trim();
const tld = document.getElementById('tld').value;
const onlyNew = document.getElementById('onlyNew').checked;
const u = new URL('./api.php', location.href);
u.searchParams.set('action','domains');
if(q) u.searchParams.set('q', q);
if(tld) u.searchParams.set('tld', tld);
if(onlyNew) u.searchParams.set('only_new', '1');
const d = await (await fetch(u)).json();
domains = d.domains || [];
document.getElementById('meta').textContent = `runId: ${d.meta?.runId||'—'} | prev: ${d.meta?.prevRunId||'—'} | scannedAt: ${d.meta?.scannedAt||'—'} | nowe: ${d.meta?.newCount||0} | widoczne: ${domains.length}`;
}
async function loadPrices(){
const u = new URL('./api.php', location.href); u.searchParams.set('action','prices');
const d = await (await fetch(u)).json();
registrarRows = d.items || [];
registrars = groupPrices(registrarRows);
}
async function savePrice(registrar,tld,registerPrice,renewPrice){
const u = new URL('./api.php', location.href); u.searchParams.set('action','prices');
const r = await fetch(u, {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({registrar,tld,register_price:registerPrice,renew_price:renewPrice})
});
return r.ok;
}
function renderPricing(){
const wrap = document.getElementById('pricing');
wrap.innerHTML = '';
for(const r of registrars){
const row = document.createElement('div');
row.style.marginBottom='8px';
row.innerHTML = `<b>${r.name}</b> <a href="${r.url}" target="_blank">link</a><br>` + ['pl','com','ai'].map(tld=>{
const a = priceFor(r,tld,'register');
const b = priceFor(r,tld,'renew');
return `<span class='pill'>.${tld} reg <input data-r='${r.name}' data-t='${tld}' data-k='register' value='${a??''}' /></span>
<span class='pill'>odn <input data-r='${r.name}' data-t='${tld}' data-k='renew' value='${b??''}' /></span>`;
}).join('');
wrap.appendChild(row);
}
wrap.querySelectorAll('input').forEach(inp=>{
inp.addEventListener('change', async (e)=>{
const {r,t,k} = e.target.dataset;
const reg = registrars.find(x=>x.name===r);
const value = e.target.value.trim()==='' ? null : Number(e.target.value.replace(',','.'));
if(!reg.pricing[t]) reg.pricing[t] = {};
reg.pricing[t][k] = Number.isFinite(value) ? value : null;
const ok = await savePrice(r, t, reg.pricing[t].register ?? null, reg.pricing[t].renew ?? null);
e.target.classList.toggle('ok', ok);
e.target.classList.toggle('bad', !ok);
renderRows();
});
});
}
function renderRows(){
const rows = document.getElementById('rows');
rows.innerHTML='';
for(const d of domains.slice(0,300)){
const buy = best(registrars,d.tld,'register');
const renew = best(registrars,d.tld,'renew');
const two = best2y(registrars,d.tld);
const tr = document.createElement('tr');
tr.innerHTML = `<td><b>${d.domain}</b> ${d.is_new ? `<span class='chip'>NEW</span>` : ''}</td><td>${d.score}</td>
<td>${buy?`<a href='${buy.url}' target='_blank'>${buy.name}</a> (${money(buy.value)})`:'—'}</td>
<td>${renew?`<a href='${renew.url}' target='_blank'>${renew.name}</a> (${money(renew.value)})`:'—'}</td>
<td>${two?`<a href='${two.url}' target='_blank'>${two.name}</a> (${money(two.value)})`:'—'}</td>`;
rows.appendChild(tr);
}
}
async function refreshAll(){
await loadDomains();
await loadPrices();
renderPricing();
renderRows();
}
let qTimer;
let autoTimer;
function setupAutoRefresh(){
if(autoTimer) clearInterval(autoTimer);
const enabled = document.getElementById('autoRefresh').checked;
if(enabled){
autoTimer = setInterval(async()=>{
await loadDomains();
await loadPrices();
renderPricing();
renderRows();
}, 60000);
}
}
document.getElementById('reload').addEventListener('click', refreshAll);
document.getElementById('q').addEventListener('input', async()=>{
clearTimeout(qTimer);
qTimer = setTimeout(async()=>{ await loadDomains(); renderRows(); }, 250);
});
document.getElementById('tld').addEventListener('change', async()=>{await loadDomains(); renderRows();});
document.getElementById('onlyNew').addEventListener('change', async()=>{await loadDomains(); renderRows();});
document.getElementById('autoRefresh').addEventListener('change', setupAutoRefresh);
refreshAll();
setupAutoRefresh();
</script>
</body>
</html>