feat: /api/v2/analiza/* endpoints - sport analytics backend

This commit is contained in:
Damir Radulic
2026-05-16 00:28:12 +02:00
parent 7ca5d7d94e
commit aca5051418
1355 changed files with 321891 additions and 4128 deletions
+146 -55
View File
@@ -23,19 +23,34 @@
background:#1a1a1e;border:1px solid #2a2a2e;color:#fff;padding:8px 12px;border-radius:5px;font-size:13px
}
.filters input{min-width:240px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
.card{background:#0c1016;border:1px solid #1a1a1e;border-radius:8px;padding:16px;cursor:pointer;transition:all .2s}
.card:hover{border-color:#5fb6ff;transform:translateY(-2px);box-shadow:0 6px 20px rgba(95,182,255,.2)}
.card-year{font-size:32px;font-weight:800;color:#5fb6ff;margin-bottom:4px}
.card-title{font-size:14px;font-weight:600;margin-bottom:8px;color:#fff;line-height:1.3}
.card-org{font-size:11px;color:#888;margin-bottom:6px}
.card-meta{font-size:10px;color:#666;display:flex;gap:8px;flex-wrap:wrap}
.card-meta span{background:#1a1a1e;padding:2px 8px;border-radius:3px}
.empty{text-align:center;padding:40px;color:#666}
.stats{font-size:13px;color:#888;margin-bottom:16px}
.toolbar{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;gap:12px;flex-wrap:wrap}
.stats{font-size:13px;color:#888}
.stats b{color:#5fb6ff;font-weight:600}
.view-toggle{display:inline-flex;background:#0c1016;border:1px solid #2a2a2e;border-radius:6px;overflow:hidden}
.view-toggle button{background:transparent;border:0;color:#888;padding:6px 12px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;font-family:inherit}
.view-toggle button.active{background:#1e3a5f;color:#5fb6ff}
.view-toggle button:hover:not(.active){color:#fff}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
.card{background:#0c1016;border:1px solid #1a1a1e;border-radius:8px;padding:14px;cursor:pointer;transition:all .2s;display:flex;gap:12px;align-items:flex-start}
.card:hover{border-color:#5fb6ff;transform:translateY(-2px);box-shadow:0 6px 20px rgba(95,182,255,.2)}
.card-thumb{flex:0 0 56px;width:56px;height:72px;border-radius:4px;background:linear-gradient(160deg,#1e3a5f 0%,#0c1016 100%);border:1px solid #2a2a2e;display:flex;align-items:center;justify-content:center;font-size:24px}
.card-body{flex:1;min-width:0}
.card-title{font-size:13px;font-weight:600;margin-bottom:6px;color:#fff;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.card-org{font-size:11px;color:#888;margin-bottom:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card-meta{font-size:10px;color:#666;display:flex;gap:6px;flex-wrap:wrap}
.card-meta span{background:#1a1a1e;padding:2px 6px;border-radius:3px}
table.docs{width:100%;border-collapse:collapse;background:#0c1016;border:1px solid #1a1a1e;border-radius:8px;overflow:hidden}
table.docs th, table.docs td{padding:8px 12px;text-align:left;font-size:12px;border-bottom:1px solid #1a1a1e}
table.docs th{background:#0a0e15;color:#888;font-weight:600;text-transform:uppercase;font-size:10px;letter-spacing:.5px;white-space:nowrap}
table.docs tr{cursor:pointer;transition:background .15s}
table.docs tbody tr:hover{background:#11161f}
table.docs td.t-title{color:#fff;font-weight:500;max-width:480px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
table.docs td.t-num{font-variant-numeric:tabular-nums;color:#aaa;text-align:right;white-space:nowrap}
table.docs td.t-tag{color:#888}
.empty{text-align:center;padding:40px;color:#666;background:#0c1016;border:1px solid #1a1a1e;border-radius:8px}
.badge-godisnjak{background:#1e3a5f;color:#5fb6ff;padding:2px 6px;border-radius:3px;font-size:10px;font-weight:600}
</style>
<script src="/static/shared/sortable.js" defer></script>
</head>
<body>
<header>
@@ -83,6 +98,29 @@
<option value="vaterpolo">Vaterpolo</option>
</select>
</div>
<div>
<label>Godina</label><br>
<select id="f-godina" onchange="loadDocs()">
<option value="">Sve godine</option>
<option value="2026">2026</option>
<option value="2025">2025</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
<option value="2022">2022</option>
<option value="2021">2021</option>
<option value="2020">2020</option>
<option value="2019">2019</option>
<option value="2018">2018</option>
<option value="2017">2017</option>
<option value="2016">2016</option>
<option value="2015">2015</option>
<option value="2014">2014</option>
<option value="2013">2013</option>
<option value="2012">2012</option>
<option value="2011">2011</option>
<option value="2010">2010</option>
</select>
</div>
<div>
<label>Pretraga</label><br>
<input type="text" id="f-q" placeholder="Naziv, opis…" onkeyup="if(event.key==='Enter') loadDocs()">
@@ -90,64 +128,117 @@
<button onclick="loadDocs()" style="background:#5fb6ff;color:#000;border:none;padding:10px 16px;border-radius:5px;font-weight:600;cursor:pointer">🔍 Pretraži</button>
</div>
<div class="stats" id="stats">Učitavanje…</div>
<div class="grid" id="docs-grid">
<div class="toolbar">
<div class="stats" id="stats">Učitavanje…</div>
<div class="view-toggle" role="tablist" aria-label="Pogled">
<button id="view-card" type="button" onclick="setView('card')" title="Kartice"><span aria-hidden="true"></span> Cards</button>
<button id="view-table" type="button" onclick="setView('table')" title="Tablica"><span aria-hidden="true"></span> Table</button>
</div>
</div>
<div id="docs-out">
<div class="empty">Učitavanje dokumenata…</div>
</div>
</div>
<script>
const VIEW_KEY = 'pgz_dok_view';
let _docsCache = [];
let _view = (function(){
const saved = localStorage.getItem(VIEW_KEY);
if (saved === 'card' || saved === 'table') return saved;
return (window.matchMedia && window.matchMedia('(max-width: 760px)').matches) ? 'card' : 'table';
})();
function fmtSize(chars){
if (!chars) return '—';
const kb = chars / 1024;
return kb < 1024 ? Math.round(kb)+' KB' : (kb/1024).toFixed(1)+' MB';
}
function fmtDate(iso){
if (!iso) return '—';
try { return new Date(iso).toLocaleDateString('hr-HR'); } catch(_) { return '—'; }
}
function rowDate(r){ return r.izdano_datum || r.scraped_at || null; }
function rowUrl(r){ return r.izvor_url || ('/sport/api/v2/dokumenti/'+r.id); }
function esc(s){ return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function renderCards(rows){
return '<div class="grid">' + rows.map(r => {
const isGod = r.vrsta === 'godisnjak';
return `<div class="card" onclick="window.open('${esc(rowUrl(r))}', '_blank', 'noopener')">
<div class="card-thumb" aria-hidden="true">${isGod ? '📖' : '📄'}</div>
<div class="card-body">
<div class="card-title">${esc(r.title || 'Dokument')}</div>
<div class="card-org">${esc(r.organizacija || '—')}</div>
<div class="card-meta">
${isGod ? '<span class="badge-godisnjak">GODIŠNJAK</span>' : ''}
${r.vrsta ? '<span>'+esc(r.vrsta)+'</span>' : ''}
${r.godina ? '<span>'+esc(r.godina)+'</span>' : ''}
<span>${fmtSize(r.chars)}</span>
<span>${fmtDate(rowDate(r))}</span>
</div>
</div>
</div>`;
}).join('') + '</div>';
}
function renderTable(rows){
return `<table class="docs">
<thead><tr>
<th>Title</th><th>Type</th><th>Year</th><th style="text-align:right">Size</th><th>Date</th>
</tr></thead>
<tbody>${rows.map(r => `
<tr onclick="window.open('${esc(rowUrl(r))}', '_blank', 'noopener')">
<td class="t-title">${esc(r.title || 'Dokument')}${r.vrsta==='godisnjak' ? ' <span class="badge-godisnjak">G</span>' : ''}</td>
<td class="t-tag">${esc(r.vrsta || '—')}</td>
<td class="t-num">${esc(r.godina || '—')}</td>
<td class="t-num">${fmtSize(r.chars)}</td>
<td class="t-tag">${fmtDate(rowDate(r))}</td>
</tr>`).join('')}</tbody>
</table>`;
}
function paint(){
const out = document.getElementById('docs-out');
document.getElementById('view-card').classList.toggle('active', _view === 'card');
document.getElementById('view-table').classList.toggle('active', _view === 'table');
if (!_docsCache.length){ out.innerHTML = '<div class="empty">Nema dokumenata po filtru</div>'; return; }
out.innerHTML = _view === 'card' ? renderCards(_docsCache) : renderTable(_docsCache);
}
function setView(v){
if (v !== 'card' && v !== 'table') return;
_view = v;
try { localStorage.setItem(VIEW_KEY, v); } catch(_) {}
paint();
}
async function loadDocs(){
const vrsta = document.getElementById('f-vrsta').value;
const org = document.getElementById('f-org').value;
const sport = document.getElementById('f-sport').value;
const q = document.getElementById('f-q').value;
const vrsta = document.getElementById('f-vrsta').value;
const org = document.getElementById('f-org').value;
const sport = document.getElementById('f-sport').value;
const godina = document.getElementById('f-godina').value;
const q = document.getElementById('f-q').value;
const params = new URLSearchParams();
if(vrsta) params.set('vrsta', vrsta);
if(sport) params.set('sport', sport);
if(q) params.set('q', q);
if(org) params.set('organizacija', org);
if(vrsta) params.set('vrsta', vrsta);
if(sport) params.set('sport', sport);
if(godina) params.set('godina', godina);
if(q) params.set('q', q);
if(org) params.set('organizacija', org);
params.set('limit', '500');
document.getElementById('docs-grid').innerHTML = '<div class="empty">Učitavanje…</div>';
try{
document.getElementById('docs-out').innerHTML = '<div class="empty">Učitavanje…</div>';
try {
const r = await fetch('/sport/api/v2/dokumenti?'+params.toString());
const d = await r.json();
let rows = d.rows || d.dokumenti || [];
document.getElementById('stats').innerHTML = `<b>${rows.length}</b> dokumenata po filtru (filter: ${[vrsta,sport,org,q].filter(Boolean).join(', ') || 'bez filtera'})`;
if(rows.length === 0){
document.getElementById('docs-grid').innerHTML = '<div class="empty">Nema dokumenata po filtru</div>';
return;
}
document.getElementById('docs-grid').innerHTML = rows.map(r => {
const year = r.godina || '—';
const url = r.izvor_url || ('/sport/api/v2/dokumenti/'+r.id);
const isGod = r.vrsta === 'godisnjak';
return `
<div class="card" onclick="window.open('${url}', '_blank')">
<div class="card-year">${year}</div>
<div class="card-title">${r.title || r.fname || 'Dokument'}</div>
<div class="card-org">${r.organizacija || '—'}</div>
<div class="card-meta">
${isGod ? '<span class="badge-godisnjak">📖 GODIŠNJAK</span>' : ''}
<span>${r.vrsta || ''}</span>
${r.sport ? '<span>'+r.sport+'</span>' : ''}
${r.sadrzaj_size ? '<span>'+(Math.round(r.sadrzaj_size/1000))+'KB</span>' : ''}
<span>📄 PDF</span>
</div>
</div>
`;
}).join('');
}catch(e){
document.getElementById('docs-grid').innerHTML = '<div class="empty">Greška: '+e.message+'</div>';
_docsCache = d.rows || d.dokumenti || [];
document.getElementById('stats').innerHTML =
`<b>${_docsCache.length}</b> dokumenata po filtru (filter: ${[vrsta,sport,godina,org,q].filter(Boolean).join(', ') || 'bez filtera'})`;
paint();
} catch(e) {
document.getElementById('docs-out').innerHTML = '<div class="empty">Greška: '+esc(e.message)+'</div>';
}
}
loadDocs();
</script>
<script src="/static/_ai_widget.js" defer></script>
</body>
</html>