feat: /api/v2/analiza/* endpoints - sport analytics backend
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<!doctype html>
|
||||
<html lang="hr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sportaš profil — PGŽ Sport</title>
|
||||
<style>
|
||||
:root{--bg:#0a0a0f;--card:#0c0c14;--border:#1a1a24;--text:#e8e8f0;--muted:#8888a0;--subtle:#6a6a80;--accent:#6366f1;--green:#10b981}
|
||||
*{box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Inter","Segoe UI",sans-serif;background:var(--bg);color:var(--text);margin:0;line-height:1.55}
|
||||
.topbar{border-bottom:1px solid var(--border);padding:12px 20px;background:rgba(10,10,15,.85);position:sticky;top:0;z-index:50;display:flex;justify-content:space-between;align-items:center}
|
||||
.brand{font-weight:700;font-size:15px;display:flex;align-items:center;gap:9px;color:inherit;text-decoration:none}
|
||||
.brand-icon{width:30px;height:30px;background:linear-gradient(135deg,#6366f1,#4f46e5);border-radius:7px;display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;font-size:14px}
|
||||
main{max-width:1080px;margin:0 auto;padding:24px 20px}
|
||||
.hero{display:grid;grid-template-columns:140px 1fr;gap:24px;margin-bottom:24px;align-items:start}
|
||||
@media (max-width:700px){.hero{grid-template-columns:96px 1fr}}
|
||||
.photo{width:140px;height:140px;background:linear-gradient(135deg,#1a1a26,#0c0c14);border:1px solid var(--border);border-radius:12px;display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:48px;font-weight:700}
|
||||
@media (max-width:700px){.photo{width:96px;height:96px;font-size:34px}}
|
||||
.head h1{margin:0 0 8px;font-size:30px;letter-spacing:-.02em}
|
||||
.head .sub{color:var(--muted);font-size:14px;margin:0 0 12px}
|
||||
.pills{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
|
||||
.pill{background:#1a1a26;color:var(--text);padding:4px 12px;border-radius:99px;font-size:11.5px;letter-spacing:.02em}
|
||||
.pill.fed{background:rgba(99,102,241,.15);color:#a5b4fc;border:1px solid rgba(99,102,241,.35)}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:24px}
|
||||
.stat{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:14px 16px}
|
||||
.stat .label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
|
||||
.stat .val{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.1}
|
||||
.section{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:18px;margin-bottom:18px}
|
||||
.section h2{margin:0 0 14px;font-size:15px;font-weight:600;letter-spacing:.02em;color:var(--text)}
|
||||
.section table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
.section th,.section td{text-align:left;padding:7px 8px;border-bottom:1px solid var(--border)}
|
||||
.section th{color:var(--muted);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
||||
.section td.num{text-align:right;font-variant-numeric:tabular-nums}
|
||||
.empty{color:var(--subtle);font-size:13px;padding:8px 0;font-style:italic}
|
||||
.row-flex{display:flex;flex-wrap:wrap;gap:8px 24px;font-size:13px;color:var(--muted)}
|
||||
.row-flex b{color:var(--text);font-weight:600}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
.err{background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.35);color:#fca5a5;padding:14px 16px;border-radius:9px}
|
||||
.banner{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);border-radius:9px;padding:10px 14px;font-size:12px;color:#fcd34d;margin-bottom:18px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/sport/dashboard"><span class="brand-icon">S</span> PGŽ Sport · Sportaš</a>
|
||||
<div><a href="javascript:history.back()" style="font-size:13px;color:var(--muted)">← natrag</a></div>
|
||||
</header>
|
||||
<main id="root">
|
||||
<p style="color:var(--muted)">Učitavam...</p>
|
||||
</main>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
const fmt = n => (n == null ? '—' : Number(n).toLocaleString('hr-HR'));
|
||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||
|
||||
function sid() {
|
||||
const m = location.pathname.match(/\/sport\/sportas\/(\d+)/);
|
||||
return m ? parseInt(m[1]) : null;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const id = sid();
|
||||
if (!id) {
|
||||
document.getElementById('root').innerHTML = '<div class="err">Nevažeći URL — očekivano /sport/sportas/{id}.</div>';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const r = await fetch('/api/v2/sport/sportas/' + id);
|
||||
if (!r.ok) {
|
||||
document.getElementById('root').innerHTML = '<div class="err">HTTP ' + r.status + ': sportas ' + id + ' nije pronađen.</div>';
|
||||
return;
|
||||
}
|
||||
render(await r.json());
|
||||
} catch (e) {
|
||||
document.getElementById('root').innerHTML = '<div class="err">Greška: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function initials(ime, prez) {
|
||||
return ((ime||'?')[0] + (prez||'?')[0]).toUpperCase();
|
||||
}
|
||||
|
||||
function render(d) {
|
||||
const s = d.sportas || {};
|
||||
const k = d.klub || {};
|
||||
const f = d.federation || {};
|
||||
const stats = d.stats || {};
|
||||
const sezone = d.sezone || [];
|
||||
const nagrade = d.nagrade || [];
|
||||
const utakmice = d.utakmice || [];
|
||||
const socials = d.socials || {};
|
||||
|
||||
const pills = [];
|
||||
if (s.pozicija) pills.push('<span class="pill">' + esc(s.pozicija) + '</span>');
|
||||
if (s.rod_godina) pills.push('<span class="pill">' + s.rod_godina + '</span>');
|
||||
if (s.visina_cm) pills.push('<span class="pill">' + s.visina_cm + ' cm</span>');
|
||||
if (s.tezina_kg) pills.push('<span class="pill">' + s.tezina_kg + ' kg</span>');
|
||||
if (s.ekipa) pills.push('<span class="pill">' + esc(s.ekipa) + '</span>');
|
||||
if (f.code) pills.push('<span class="pill fed">' + esc(f.code) + '</span>');
|
||||
|
||||
const socHtml = Object.entries(socials).map(([k,v]) =>
|
||||
`<a href="${esc(v)}" target="_blank" rel="noopener">${esc(k)}</a>`
|
||||
).join(' · ') || '';
|
||||
|
||||
const sezTable = sezone.length ? `
|
||||
<table>
|
||||
<thead><tr><th>Sezona</th><th>Klub</th><th>Natjecanje</th>
|
||||
<th class="num">Nastupi</th><th class="num">Pogoci</th><th class="num">Asistencije</th></tr></thead>
|
||||
<tbody>${sezone.map(r => `<tr>
|
||||
<td>${esc(r.sezona)}</td><td>${esc(r.klub_naziv||'—')}</td><td>${esc(r.natjecanje||'—')}</td>
|
||||
<td class="num">${fmt(r.nastupi)}</td><td class="num">${fmt(r.pogoci)}</td><td class="num">${fmt(r.asistencije)}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>` : '<div class="empty">Nema podataka o sezonama (sportašov ID nije povezan s legacy clan_* tablicom).</div>';
|
||||
|
||||
const nagTable = nagrade.length ? `
|
||||
<table>
|
||||
<thead><tr><th>Godina</th><th>Sezona</th><th>Natjecanje</th><th>Razina</th><th class="num">Plasman</th></tr></thead>
|
||||
<tbody>${nagrade.map(r => `<tr>
|
||||
<td>${esc(r.godina)}</td><td>${esc(r.sezona||'—')}</td><td>${esc(r.natjecanje||'—')}</td>
|
||||
<td>${esc(r.razina_natjecanja||'—')}</td><td class="num">${fmt(r.plasman)}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>` : '<div class="empty">Nema upisanih postignuća.</div>';
|
||||
|
||||
const utTable = utakmice.length ? `
|
||||
<table>
|
||||
<thead><tr><th>Datum</th><th>Domaćin</th><th>Gost</th><th>Rezultat</th><th>Natjecanje</th>
|
||||
<th class="num">Pogoci</th><th class="num">🟨</th><th class="num">🟥</th></tr></thead>
|
||||
<tbody>${utakmice.map(r => `<tr>
|
||||
<td>${r.datum ? new Date(r.datum).toLocaleDateString('hr-HR') : '—'}</td>
|
||||
<td>${esc(r.domacin||'—')}</td><td>${esc(r.gost||'—')}</td>
|
||||
<td>${esc(r.rezultat||'—')}</td><td>${esc(r.natjecanje||'—')}</td>
|
||||
<td class="num">${fmt(r.pogoci)}</td>
|
||||
<td class="num">${fmt(r.zuti)}</td><td class="num">${fmt(r.crveni)}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>` : '<div class="empty">Nema upisanih utakmica.</div>';
|
||||
|
||||
let banner = '';
|
||||
if (!d.linked_legacy_clan_id) {
|
||||
banner = '<div class="banner">⚠ Ovaj sportaš (iz nove <code>pgz_sport.sportasi</code> tablice) <b>nije povezan</b> s legacy <code>pgz_sport.clanovi</code> zapisom — povijest sezona/utakmica/nagrada nije dostupna dok ne uspostavimo cross-link po OIB-u ili imenu.</div>';
|
||||
}
|
||||
|
||||
document.title = `${s.ime||''} ${s.prezime||''} — Sportaš profil`;
|
||||
document.getElementById('root').innerHTML = `
|
||||
${banner}
|
||||
<div class="hero">
|
||||
<div class="photo">${initials(s.ime, s.prezime)}</div>
|
||||
<div class="head">
|
||||
<h1>${esc(s.ime||'?')} ${esc(s.prezime||'?')}</h1>
|
||||
<div class="sub">
|
||||
${s.sport ? esc(s.sport) : ''} ${k.naziv ? '· <a href="/klubovi/'+(k.id||'')+'">'+esc(k.naziv)+'</a>' : ''}
|
||||
${k.grad ? '· '+esc(k.grad) : ''}
|
||||
</div>
|
||||
<div class="pills">${pills.join('')}</div>
|
||||
${s.oib_redacted ? '<div class="row-flex" style="margin-top:12px"><span>OIB (delkated): <b>'+esc(s.oib_redacted)+'</b></span></div>' : ''}
|
||||
${socHtml ? '<div class="row-flex" style="margin-top:8px"><span>Social: '+socHtml+'</span></div>' : ''}
|
||||
${f.web ? '<div class="row-flex" style="margin-top:8px"><span>Federacija: <a href="'+esc(f.web)+'" target="_blank" rel="noopener">'+esc(f.name||f.code)+'</a></span></div>' : ''}
|
||||
${s.federation_url ? '<div class="row-flex" style="margin-top:4px"><span><a href="'+esc(s.federation_url)+'" target="_blank" rel="noopener">Profil na ' + esc(f.code || 'savezu') + ' →</a></span></div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="stat"><div class="label">Sezone</div><div class="val">${fmt(stats.sezone_count) || 0}</div></div>
|
||||
<div class="stat"><div class="label">Ukupno nastupa</div><div class="val">${fmt(stats.nastupi_total) || 0}</div></div>
|
||||
<div class="stat"><div class="label">Ukupno pogodaka</div><div class="val">${fmt(stats.pogoci_total) || 0}</div></div>
|
||||
<div class="stat"><div class="label">Asistencija</div><div class="val">${fmt(stats.asistencije_total) || 0}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Klubska povijest po sezonama</h2>
|
||||
${sezTable}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Postignuća</h2>
|
||||
${nagTable}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Aktualne utakmice (zadnjih 20)</h2>
|
||||
${utTable}
|
||||
</div>
|
||||
|
||||
<div class="section" style="opacity:.7">
|
||||
<h2>Tehnički detalji</h2>
|
||||
<div class="row-flex">
|
||||
<span>ID: <b>${s.id}</b></span>
|
||||
<span>Izvor: <b>${esc(s.source||'—')}</b></span>
|
||||
<span>Sezona scraper: <b>${esc(s.sezona||'—')}</b></span>
|
||||
<span>Federation ID: <b>${esc(s.federation_player_id||'—')}</b></span>
|
||||
<span>Scraped: <b>${s.scraped_at ? new Date(s.scraped_at).toLocaleString('hr-HR') : '—'}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
window.addEventListener('load', load);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user