Files
pgz-sport/static/objekti.html
T
damir ae9c4e2bfd Sportski objekti: API + Leaflet map page + address enrichment
DB: pgz_sport.sportski_objekti (103 objekti, 103 s geo, 60 s adresom, 31 tip)

API:
- /api/v2/sportski-objekti (filter: tip, grad, sport, q)
- /api/v2/sportski-objekti/meta (tipovi, gradovi, sportovi, ukupno)

Frontend:
- /static/objekti.html — Leaflet (OpenStreetMap) interactive map
- 3 dropdown filter (tip, grad, sport) + search
- Side panel s listom + map markers s ikonama (🏟️🏊🎿🎳⛸️🎯🥌🏃)
- Popup: naziv, tip, kapacitet, adresa, upravitelj, izgradeno, sportovi, web link, Google Maps link
- /objekti, /sport/objekti, /sport/api/v2/sportski-objekti routes

Sidebar app.html: +Sportski objekti link
Background: scripts/objekti_enrich_address.py (Nominatim reverse-geocode 60 objekata bez adrese)
2026-05-05 18:35:04 +02:00

197 lines
7.8 KiB
HTML

<!DOCTYPE html>
<!--
objekti.html — Sportski objekti PGŽ (Google Maps + filter)
Author: Damir Radulić | v1.0 | 05.05.2026
-->
<html lang="hr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>🏟️ Sportski objekti PGŽ</title>
<link rel="icon" href="/favicon.ico">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font:14px system-ui;background:#06080d;color:#e0e0e0}
header{background:#0a0e15;padding:12px 20px;border-bottom:1px solid #2a2a2e;display:flex;justify-content:space-between;align-items:center}
header h1{font-size:18px;color:#5fb6ff}
header a{color:#888;text-decoration:none;margin-left:14px;font-size:13px}
header a:hover{color:#fff}
.container{display:grid;grid-template-columns:380px 1fr;height:calc(100vh - 50px)}
.sidebar{background:#0c1016;border-right:1px solid #1a1a1e;overflow-y:auto;padding:14px}
.filters{display:flex;flex-direction:column;gap:8px;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #1a1a1e}
.filters label{font-size:11px;color:#888;text-transform:uppercase}
.filters select, .filters input{
background:#1a1a1e;border:1px solid #2a2a2e;color:#fff;padding:8px 10px;border-radius:5px;font-size:13px;width:100%
}
.stats{font-size:12px;color:#888;padding:8px 0;border-bottom:1px solid #1a1a1e;margin-bottom:8px}
.stats b{color:#5fb6ff}
.obj-list{display:flex;flex-direction:column;gap:6px}
.obj-item{background:#0c1016;border:1px solid #1a1a1e;border-radius:5px;padding:10px;cursor:pointer;transition:all .15s}
.obj-item:hover{border-color:#5fb6ff;background:#0f1620}
.obj-item.active{border-color:#fbbf24;background:#1a1610}
.obj-name{font-weight:600;color:#fff;margin-bottom:3px;font-size:13px}
.obj-meta{font-size:10px;color:#888;display:flex;gap:6px;flex-wrap:wrap}
.obj-meta span{background:#1a1a1e;padding:1px 6px;border-radius:3px}
#map{flex:1;background:#000}
.leaflet-container{background:#1a1a1e}
.popup-title{font-weight:700;font-size:14px;margin-bottom:4px;color:#000}
.popup-meta{font-size:11px;color:#666;margin-bottom:4px}
.popup-link{display:inline-block;margin-top:6px;padding:4px 8px;background:#1a73e8;color:#fff;text-decoration:none;border-radius:3px;font-size:11px}
</style>
</head>
<body>
<header>
<h1><a href="/" style="color:#5fb6ff;text-decoration:none">🏟️ Sportski objekti PGŽ</a></h1>
<div>
<a href="/">🏠 Home</a>
<a href="/static/sport2.html#dashboard">📊 Dashboard</a>
<a href="/sport/dokumenti">📚 Dokumenti</a>
<a href="/admin/users">👥 Admin</a>
</div>
</header>
<div class="container">
<div class="sidebar">
<div class="filters">
<div>
<label>Tip objekta</label>
<select id="f-tip" onchange="loadObjekti()">
<option value="">Svi tipovi</option>
</select>
</div>
<div>
<label>Grad</label>
<select id="f-grad" onchange="loadObjekti()">
<option value="">Svi gradovi</option>
</select>
</div>
<div>
<label>Sport</label>
<select id="f-sport" onchange="loadObjekti()">
<option value="">Svi sportovi</option>
</select>
</div>
<div>
<label>Pretraga</label>
<input type="search" id="f-q" placeholder="Naziv, adresa…" onkeyup="if(event.key==='Enter') loadObjekti()">
</div>
</div>
<div class="stats" id="stats">Učitavanje…</div>
<div class="obj-list" id="obj-list"></div>
</div>
<div id="map"></div>
</div>
<script>
const map = L.map('map').setView([45.3271, 14.4422], 10); // Rijeka centar
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, attribution: '© OpenStreetMap'
}).addTo(map);
let markers = L.layerGroup().addTo(map);
const tipIcons = {
'dvorana': '🏟️', 'stadion': '⚽', 'bazen': '🏊', 'kompleks': '🏛️',
'marina': '⛵', 'skijalište': '🎿', 'kuglana': '🎳', 'tenis kompleks': '🎾',
'klizalište': '⛸️', 'strelište': '🎯', 'boćalište': '🥌', 'atletska staza': '🏃',
'centar': '🏟️', 'sanjkalište': '🛷', 'hipodrom': '🐎'
};
async function loadMeta(){
const r = await fetch('/sport/api/v2/sportski-objekti/meta');
const m = await r.json();
const tipSel = document.getElementById('f-tip');
m.tipovi.forEach(t => tipSel.innerHTML += `<option value="${t.tip}">${tipIcons[t.tip]||'•'} ${t.tip} (${t.broj})</option>`);
const gradSel = document.getElementById('f-grad');
m.gradovi.forEach(g => gradSel.innerHTML += `<option value="${g.grad}">${g.grad} (${g.broj})</option>`);
const sportSel = document.getElementById('f-sport');
m.sportovi.forEach(s => sportSel.innerHTML += `<option value="${s.sport}">${s.sport} (${s.broj})</option>`);
}
async function loadObjekti(){
const params = new URLSearchParams();
const tip = document.getElementById('f-tip').value;
const grad = document.getElementById('f-grad').value;
const sport = document.getElementById('f-sport').value;
const q = document.getElementById('f-q').value;
if(tip) params.set('tip', tip);
if(grad) params.set('grad', grad);
if(sport) params.set('sport', sport);
if(q) params.set('q', q);
params.set('limit', '500');
const r = await fetch('/sport/api/v2/sportski-objekti?'+params.toString());
const d = await r.json();
document.getElementById('stats').innerHTML = `<b>${d.count}</b> objekata po filtru`;
// Markers
markers.clearLayers();
const list = document.getElementById('obj-list');
list.innerHTML = '';
const bounds = [];
d.rows.forEach((o, i) => {
if(o.lat && o.lng){
const icon = tipIcons[o.tip] || '📍';
const m = L.marker([o.lat, o.lng], {
title: o.naziv,
icon: L.divIcon({
html: `<div style="background:#1a73e8;color:#fff;padding:2px 5px;border-radius:50%;border:2px solid #fff;box-shadow:0 2px 4px rgba(0,0,0,.5);font-size:14px;width:30px;height:30px;display:flex;align-items:center;justify-content:center">${icon}</div>`,
className: '', iconSize: [30, 30], iconAnchor: [15, 15]
})
});
m.bindPopup(`
<div class="popup-title">${o.naziv}</div>
<div class="popup-meta">
${icon} ${o.tip} · ${o.grad || ''}
${o.kapacitet ? ' · ' + o.kapacitet + ' mjesta' : ''}
</div>
${o.adresa ? '<div class="popup-meta">📍 ' + o.adresa + '</div>' : ''}
${o.upravitelj ? '<div class="popup-meta">👤 ' + o.upravitelj + '</div>' : ''}
${o.izgradeno ? '<div class="popup-meta">🏗 Izgrađeno: ' + o.izgradeno + '</div>' : ''}
${o.sportovi && o.sportovi.length ? '<div class="popup-meta">⚽ ' + o.sportovi.join(', ') + '</div>' : ''}
${o.web ? '<a href="' + o.web + '" target="_blank" class="popup-link">🌐 Web</a>' : ''}
<a href="https://www.google.com/maps?q=${o.lat},${o.lng}" target="_blank" class="popup-link">🗺️ Google Maps</a>
`, {maxWidth: 320});
markers.addLayer(m);
bounds.push([o.lat, o.lng]);
}
list.innerHTML += `
<div class="obj-item" onclick="zoomTo(${o.lat||0}, ${o.lng||0})">
<div class="obj-name">${tipIcons[o.tip]||'•'} ${o.naziv}</div>
<div class="obj-meta">
<span>${o.tip}</span>
${o.grad ? '<span>'+o.grad+'</span>' : ''}
${o.kapacitet ? '<span>'+o.kapacitet+' mj</span>' : ''}
</div>
</div>
`;
});
if(bounds.length > 0){
map.fitBounds(bounds, {padding: [40, 40], maxZoom: 13});
}
}
function zoomTo(lat, lng){
if(lat && lng){
map.setView([lat, lng], 16);
markers.eachLayer(m => {
if(m.getLatLng().lat === lat) m.openPopup();
});
}
}
loadMeta().then(() => loadObjekti());
</script>
</body>
</html>