ae9c4e2bfd
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)
197 lines
7.8 KiB
HTML
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>
|