536 lines
23 KiB
HTML
536 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Nerdhalla — Valheim Server</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚔️</text></svg>">
|
||
<style>
|
||
:root {
|
||
--bg: #0a0a0f;
|
||
--surface: #14141f;
|
||
--surface2: #1a1a2e;
|
||
--accent: #c8a84e;
|
||
--accent2: #8b6f3a;
|
||
--text: #d4d4dc;
|
||
--text2: #8888a0;
|
||
--green: #4ade80;
|
||
--red: #f87171;
|
||
--border: #2a2a3e;
|
||
}
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
}
|
||
/* Header */
|
||
.header {
|
||
background: linear-gradient(135deg, var(--surface) 0%, #0d0d1a 100%);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 1.5rem 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
}
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
.header-icon { font-size: 2rem; }
|
||
.header h1 {
|
||
font-size: 1.5rem;
|
||
color: var(--accent);
|
||
font-weight: 700;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
.header .subtitle {
|
||
font-size: 0.85rem;
|
||
color: var(--text2);
|
||
margin-top: 0.15rem;
|
||
}
|
||
.server-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 1rem;
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
font-size: 0.9rem;
|
||
}
|
||
.status-dot {
|
||
width: 10px; height: 10px;
|
||
border-radius: 50%;
|
||
display: inline-block;
|
||
}
|
||
.status-dot.online { background: var(--green); box-shadow: 0 0 8px var(--green); }
|
||
.status-dot.offline { background: var(--red); box-shadow: 0 0 8px var(--red); }
|
||
.status-dot.unknown { background: var(--text2); }
|
||
|
||
/* Nav */
|
||
.nav {
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
gap: 0;
|
||
overflow-x: auto;
|
||
}
|
||
.nav a {
|
||
padding: 0.75rem 1.5rem;
|
||
color: var(--text2);
|
||
text-decoration: none;
|
||
font-size: 0.9rem;
|
||
border-bottom: 2px solid transparent;
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
.nav a:hover, .nav a.active {
|
||
color: var(--accent);
|
||
border-bottom-color: var(--accent);
|
||
background: rgba(200, 168, 78, 0.05);
|
||
}
|
||
|
||
/* Content */
|
||
.content { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
||
.page { display: none; }
|
||
.page.active { display: block; }
|
||
|
||
/* Cards */
|
||
.card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.card h2 {
|
||
font-size: 1.1rem;
|
||
color: var(--accent);
|
||
margin-bottom: 1rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
.card-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
.stat-card {
|
||
background: var(--surface2);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 1rem;
|
||
text-align: center;
|
||
}
|
||
.stat-card .value {
|
||
font-size: 1.8rem;
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
}
|
||
.stat-card .label {
|
||
font-size: 0.8rem;
|
||
color: var(--text2);
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
/* Map */
|
||
#map {
|
||
height: 600px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
/* Info table */
|
||
.info-table { width: 100%; border-collapse: collapse; }
|
||
.info-table td {
|
||
padding: 0.6rem 0.8rem;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 0.9rem;
|
||
}
|
||
.info-table td:first-child {
|
||
color: var(--text2);
|
||
width: 200px;
|
||
font-weight: 500;
|
||
}
|
||
.info-table tr:last-child td { border-bottom: none; }
|
||
|
||
/* Footer */
|
||
.footer {
|
||
text-align: center;
|
||
padding: 2rem;
|
||
color: var(--text2);
|
||
font-size: 0.8rem;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
.footer a { color: var(--accent); text-decoration: none; }
|
||
|
||
/* Responsive */
|
||
@media (max-width: 768px) {
|
||
.header { flex-direction: column; align-items: flex-start; }
|
||
.header-left { width: 100%; }
|
||
.server-status { width: 100%; justify-content: center; }
|
||
#map { height: 400px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Header -->
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<div class="header-icon">⚔️</div>
|
||
<div>
|
||
<h1>Nerdhalla</h1>
|
||
<div class="subtitle">Valheim Community Server</div>
|
||
</div>
|
||
</div>
|
||
<div class="server-status" id="serverStatus">
|
||
<span class="status-dot unknown" id="statusDot"></span>
|
||
<span id="statusText">Checking server...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Navigation -->
|
||
<nav class="nav">
|
||
<a href="#" class="active" data-page="home">🏠 Home</a>
|
||
<a href="#" data-page="map">🗺️ World Map</a>
|
||
<a href="#" data-page="world">🌍 World Info</a>
|
||
<a href="#" data-page="pois">📍 Points of Interest</a>
|
||
</nav>
|
||
|
||
<!-- Content -->
|
||
<div class="content">
|
||
<!-- Home Page -->
|
||
<div class="page active" id="page-home">
|
||
<div class="card">
|
||
<h2>🏰 Welcome to Nerdhalla</h2>
|
||
<p style="line-height:1.7; color: var(--text2);">
|
||
A vanilla Valheim community server — no mods, no BepInEx, just pure Viking survival.
|
||
Explore a hand-mapped world, find the best base locations, and conquer the tenth world together.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card-grid">
|
||
<div class="stat-card">
|
||
<div class="value" id="statSeed">yzZ5fr2tGa</div>
|
||
<div class="label">World Seed</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value" id="statBiomes">7</div>
|
||
<div class="label">Biomes</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value" id="statPOIs">11,309</div>
|
||
<div class="label">Points of Interest</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="value" id="statMapSize">4,096</div>
|
||
<div class="label">Map Size (px)</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>🎯 Best Tar Pit Location</h2>
|
||
<p style="color: var(--text2); margin-bottom: 0.5rem;">
|
||
<strong style="color: var(--accent);">(1210, -4159)</strong> — 5 tar pits within 200m radius.
|
||
Prime spot for a base with easy tar access.
|
||
</p>
|
||
<p style="color: var(--text2); font-size: 0.85rem;">
|
||
Use the <a href="#" onclick="switchPage('map')" style="color: var(--accent);">World Map</a> to explore more locations.
|
||
</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>📋 Quick Links</h2>
|
||
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||
<a href="#" onclick="switchPage('map')" style="background: var(--accent2); color: #fff; padding: 0.6rem 1.2rem; border-radius: 6px; text-decoration: none; font-size: 0.9rem;">🗺️ Open World Map</a>
|
||
<a href="#" onclick="switchPage('world')" style="background: var(--surface2); color: var(--text); padding: 0.6rem 1.2rem; border-radius: 6px; text-decoration: none; font-size: 0.9rem; border: 1px solid var(--border);">🌍 World Details</a>
|
||
<a href="#" onclick="switchPage('pois')" style="background: var(--surface2); color: var(--text); padding: 0.6rem 1.2rem; border-radius: 6px; text-decoration: none; font-size: 0.9rem; border: 1px solid var(--border);">📍 Browse POIs</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Map Page -->
|
||
<div class="page" id="page-map">
|
||
<div class="card" style="padding: 0; overflow: hidden;">
|
||
<div id="map"></div>
|
||
</div>
|
||
<div class="card" style="margin-top: 1rem;">
|
||
<h2>🗺️ Map Controls</h2>
|
||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.85rem; color: var(--text2);">
|
||
<span>🖱️ Click POI markers for details</span>
|
||
<span>🔍 Scroll to zoom</span>
|
||
<span>📦 Layer selector in top-right</span>
|
||
<span>🔎 Search box for locations</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- World Info Page -->
|
||
<div class="page" id="page-world">
|
||
<div class="card">
|
||
<h2>🌍 World Settings</h2>
|
||
<table class="info-table">
|
||
<tr><td>Seed</td><td><code>yzZ5fr2tGa</code></td></tr>
|
||
<tr><td>World Name</td><td>Nerdhalla</td></tr>
|
||
<tr><td>Difficulty</td><td>Normal (combat, raids, resources)</td></tr>
|
||
<tr><td>Map Size</td><td>~10 km × 10 km (4,096 × 4,096 px tiles)</td></tr>
|
||
<tr><td>Biomes</td><td>Meadows, Black Forest, Swamp, Mountain, Plains, Ocean, Mistlands</td></tr>
|
||
<tr><td>Total POIs</td><td>11,309 locations catalogued</td></tr>
|
||
</table>
|
||
</div>
|
||
<div class="card">
|
||
<h2>📊 Biome Distribution</h2>
|
||
<div id="biomeChart" style="height: 200px; display: flex; align-items: flex-end; gap: 0.5rem; padding: 1rem 0;">
|
||
<div style="flex:1; text-align:center;">
|
||
<div style="background:#7ec850; height:120px; border-radius:4px 4px 0 0;"></div>
|
||
<div style="font-size:0.75rem; color:var(--text2); margin-top:0.3rem;">Meadows</div>
|
||
</div>
|
||
<div style="flex:1; text-align:center;">
|
||
<div style="background:#2d5a1e; height:160px; border-radius:4px 4px 0 0;"></div>
|
||
<div style="font-size:0.75rem; color:var(--text2); margin-top:0.3rem;">Black Forest</div>
|
||
</div>
|
||
<div style="flex:1; text-align:center;">
|
||
<div style="background:#3a3a1a; height:90px; border-radius:4px 4px 0 0;"></div>
|
||
<div style="font-size:0.75rem; color:var(--text2); margin-top:0.3rem;">Swamp</div>
|
||
</div>
|
||
<div style="flex:1; text-align:center;">
|
||
<div style="background:#8b8b8b; height:140px; border-radius:4px 4px 0 0;"></div>
|
||
<div style="font-size:0.75rem; color:var(--text2); margin-top:0.3rem;">Mountain</div>
|
||
</div>
|
||
<div style="flex:1; text-align:center;">
|
||
<div style="background:#c8a84e; height:100px; border-radius:4px 4px 0 0;"></div>
|
||
<div style="font-size:0.75rem; color:var(--text2); margin-top:0.3rem;">Plains</div>
|
||
</div>
|
||
<div style="flex:1; text-align:center;">
|
||
<div style="background:#4a6fa5; height:180px; border-radius:4px 4px 0 0;"></div>
|
||
<div style="font-size:0.75rem; color:var(--text2); margin-top:0.3rem;">Ocean</div>
|
||
</div>
|
||
<div style="flex:1; text-align:center;">
|
||
<div style="background:#5a2d7a; height:70px; border-radius:4px 4px 0 0;"></div>
|
||
<div style="font-size:0.75rem; color:var(--text2); margin-top:0.3rem;">Mistlands</div>
|
||
</div>
|
||
</div>
|
||
<p style="font-size:0.8rem; color:var(--text2); text-align:center;">Approximate — based on tile analysis</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- POIs Page -->
|
||
<div class="page" id="page-pois">
|
||
<div class="card">
|
||
<h2>📍 Points of Interest</h2>
|
||
<p style="color: var(--text2); margin-bottom: 1rem;">
|
||
All 11,309 locations from the Nerdhalla world, searchable and categorized.
|
||
</p>
|
||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||
<input type="text" id="poiSearch" placeholder="Search POIs..." style="flex:1; min-width:200px; padding:0.6rem; background:var(--surface2); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:0.9rem;">
|
||
<select id="poiCategory" style="padding:0.6rem; background:var(--surface2); border:1px solid var(--border); border-radius:6px; color:var(--text); font-size:0.9rem;">
|
||
<option value="">All Categories</option>
|
||
</select>
|
||
</div>
|
||
<div id="poiResults" style="max-height: 500px; overflow-y: auto;">
|
||
<p style="color: var(--text2); text-align: center; padding: 2rem;">
|
||
Loading POI data from database...
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="footer">
|
||
<p>Nerdhalla — Vanilla Valheim Community Server</p>
|
||
<p style="margin-top: 0.3rem;">
|
||
<a href="http://git.sweeney.fyi:3000/aelith/valheim-map" target="_blank">Source on Forgejo</a>
|
||
· Powered by <a href="https://caddyserver.com" target="_blank">Caddy</a>
|
||
· Map by <a href="https://leafletjs.com" target="_blank">Leaflet</a>
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Leaflet -->
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script src="https://unpkg.com/leaflet-search@3.0.10/dist/leaflet-search.src.js"></script>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet-search@3.0.10/dist/leaflet-search.src.css" />
|
||
|
||
<script>
|
||
// ====== Page Navigation ======
|
||
function switchPage(name) {
|
||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
document.querySelectorAll('.nav a').forEach(a => a.classList.remove('active'));
|
||
document.getElementById('page-' + name).classList.add('active');
|
||
document.querySelector(`.nav a[data-page="${name}"]`).classList.add('active');
|
||
if (name === 'map' && map) setTimeout(() => map.invalidateSize(), 100);
|
||
if (name === 'pois') loadPOIs();
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
document.querySelectorAll('.nav a').forEach(a => {
|
||
a.addEventListener('click', e => {
|
||
e.preventDefault();
|
||
switchPage(a.dataset.page);
|
||
});
|
||
});
|
||
|
||
// ====== Server Status ======
|
||
async function checkServer() {
|
||
const dot = document.getElementById('statusDot');
|
||
const text = document.getElementById('statusText');
|
||
try {
|
||
const resp = await fetch('/api/status');
|
||
const data = await resp.json();
|
||
if (data.online) {
|
||
dot.className = 'status-dot online';
|
||
text.textContent = `🟢 Online — ${data.players || 0} player(s)`;
|
||
} else {
|
||
dot.className = 'status-dot offline';
|
||
text.textContent = '🔴 Offline';
|
||
}
|
||
} catch {
|
||
dot.className = 'status-dot unknown';
|
||
text.textContent = '❓ Status unavailable';
|
||
}
|
||
}
|
||
checkServer();
|
||
setInterval(checkServer, 60000);
|
||
|
||
// ====== Map ======
|
||
let map = null;
|
||
function initMap() {
|
||
if (map) return;
|
||
map = L.map('map', {
|
||
center: [0, 0],
|
||
zoom: 1,
|
||
maxZoom: 5,
|
||
minZoom: 0,
|
||
crs: L.CRS.Simple,
|
||
zoomControl: true
|
||
});
|
||
|
||
const bounds = [[0, 0], [4096, 4096]];
|
||
|
||
// Tile layers
|
||
const composite = L.imageOverlay('world_composite.png', bounds, { opacity: 1 });
|
||
const biome = L.imageOverlay('world_biome.png', bounds, { opacity: 0.7 });
|
||
const height = L.imageOverlay('world_height.png', bounds, { opacity: 0.5 });
|
||
|
||
const baseMaps = {
|
||
"Composite": composite,
|
||
"Biome": biome,
|
||
"Height": height
|
||
};
|
||
|
||
composite.addTo(map);
|
||
map.fitBounds(bounds);
|
||
|
||
L.control.layers(baseMaps, null, { collapsed: false }).addTo(map);
|
||
|
||
// Coordinate display
|
||
const coordDisplay = L.control({ position: 'bottomleft' });
|
||
coordDisplay.onAdd = function() {
|
||
const div = L.DomUtil.create('div', 'coord-display');
|
||
div.style.cssText = 'background:rgba(20,20,31,0.9); color:#d4d4dc; padding:4px 10px; border-radius:4px; font-size:12px; border:1px solid #2a2a3e;';
|
||
div.innerHTML = 'Move cursor for coordinates';
|
||
return div;
|
||
};
|
||
coordDisplay.addTo(map);
|
||
|
||
map.on('mousemove', function(e) {
|
||
const x = Math.round(e.latlng.lng);
|
||
const y = Math.round(e.latlng.lat);
|
||
document.querySelector('.coord-display').innerHTML = `📍 (${x}, ${y})`;
|
||
});
|
||
|
||
// POI markers from database
|
||
fetch('/api/pois?limit=500')
|
||
.then(r => r.json())
|
||
.then(pois => {
|
||
const poiIcons = {
|
||
'point_of_interest': L.divIcon({ html: '📍', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'boss': L.divIcon({ html: '👑', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'dungeon': L.divIcon({ html: '🏚️', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'resource': L.divIcon({ html: '⛏️', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'structure': L.divIcon({ html: '🏠', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'spawner': L.divIcon({ html: '👾', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'altar': L.divIcon({ html: '🪦', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'vendor': L.divIcon({ html: '🏪', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'other': L.divIcon({ html: '❓', className: 'poi-icon', iconSize: [20, 20] }),
|
||
'default': L.divIcon({ html: '📍', className: 'poi-icon', iconSize: [20, 20] })
|
||
};
|
||
const markerLayer = L.layerGroup();
|
||
pois.forEach(p => {
|
||
const icon = poiIcons[p.category] || poiIcons['default'];
|
||
const m = L.marker([p.y, p.x], { icon }).bindPopup(
|
||
`<b>${p.prefab || 'Unknown'}</b><br>📍 (${Math.round(p.x)}, ${Math.round(p.y)})<br>📂 ${p.category || 'Unknown'}`
|
||
);
|
||
markerLayer.addLayer(m);
|
||
});
|
||
markerLayer.addTo(map);
|
||
L.control.layers(null, { "POIs (11k+)": markerLayer }, { collapsed: true }).addTo(map);
|
||
});
|
||
}
|
||
|
||
// Init map when page loads or switches to map
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Defer map init until tab is clicked
|
||
});
|
||
document.querySelector('.nav a[data-page="map"]').addEventListener('click', () => {
|
||
setTimeout(initMap, 200);
|
||
});
|
||
|
||
// ====== POI Browser ======
|
||
let allPOIs = [];
|
||
async function loadPOIs() {
|
||
const results = document.getElementById('poiResults');
|
||
if (allPOIs.length > 0) return renderPOIs();
|
||
try {
|
||
const resp = await fetch('/api/pois?limit=11309');
|
||
allPOIs = await resp.json();
|
||
// Populate categories
|
||
const cats = new Set(allPOIs.map(p => p.category).filter(Boolean));
|
||
const sel = document.getElementById('poiCategory');
|
||
cats.forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = c;
|
||
opt.textContent = c.replace(/_/g, ' ');
|
||
sel.appendChild(opt);
|
||
});
|
||
renderPOIs();
|
||
} catch {
|
||
results.innerHTML = '<p style="color: var(--red); text-align: center; padding: 2rem;">Failed to load POIs</p>';
|
||
}
|
||
}
|
||
|
||
function renderPOIs() {
|
||
const search = document.getElementById('poiSearch').value.toLowerCase();
|
||
const cat = document.getElementById('poiCategory').value;
|
||
const filtered = allPOIs.filter(p => {
|
||
const name = (p.prefab || '').toLowerCase();
|
||
const catVal = (p.category || '');
|
||
return name.includes(search) && (!cat || catVal === cat);
|
||
});
|
||
const results = document.getElementById('poiResults');
|
||
if (filtered.length === 0) {
|
||
results.innerHTML = '<p style="color: var(--text2); text-align: center; padding: 2rem;">No matching POIs</p>';
|
||
return;
|
||
}
|
||
results.innerHTML = filtered.slice(0, 200).map(p => `
|
||
<div style="padding:0.5rem; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center;">
|
||
<span><strong>${p.prefab || 'Unknown'}</strong> <span style="color:var(--text2);font-size:0.8rem;">(${Math.round(p.x)}, ${Math.round(p.y)})</span></span>
|
||
<span style="color:var(--text2);font-size:0.8rem;">${(p.category || '').replace(/_/g, ' ')}</span>
|
||
</div>
|
||
`).join('');
|
||
if (filtered.length > 200) {
|
||
results.innerHTML += `<p style="color:var(--text2);text-align:center;padding:0.5rem;font-size:0.8rem;">Showing 200 of ${filtered.length} results</p>`;
|
||
}
|
||
}
|
||
|
||
document.getElementById('poiSearch').addEventListener('input', renderPOIs);
|
||
document.getElementById('poiCategory').addEventListener('change', renderPOIs);
|
||
</script>
|
||
</body>
|
||
</html>
|