feat: /api/v2/analiza/* endpoints - sport analytics backend
This commit is contained in:
+146
-55
@@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
|
||||
Reference in New Issue
Block a user