feat: refresh UI and add sidebar navigation

This commit is contained in:
GuavaPopper
2026-06-10 18:16:46 +07:00
parent c4157cec8d
commit d85d066f6c
14 changed files with 1636 additions and 603 deletions
+104 -62
View File
@@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="id" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Peta Interaktif - WebGIS Miskin & Ibadah</title>
<title>Peta Interaktif - WebGIS Pemetaan Kemiskinan</title>
<!-- Leaflet -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
@@ -18,15 +19,46 @@
<!-- Plus Jakarta Sans Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"
rel="stylesheet">
<style>
* { font-family: 'Plus Jakarta Sans', sans-serif; }
body { font-weight: 500; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
h1, h2, h3, h4, h5, h6, .btn, .card-title, .stat-value { font-weight: 700 !important; letter-spacing: -0.01em; }
.map-wrapper { flex: 1; position: relative; min-height: 0; }
#map { width: 100%; height: 100%; }
* {
font-family: 'Plus Jakarta Sans', sans-serif;
}
body {
font-weight: 500;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
h1,
h2,
h3,
h4,
h5,
h6,
.btn,
.card-title,
.stat-value {
font-weight: 700 !important;
letter-spacing: -0.01em;
}
.map-wrapper {
flex: 1;
position: relative;
min-height: 0;
}
#map {
width: 100%;
height: 100%;
}
/* Floating Controls */
.floating-panel {
position: absolute;
@@ -37,7 +69,7 @@
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.glass-card {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
@@ -46,7 +78,8 @@
}
/* Marker dragging hint */
#dragHint, #mapHint {
#dragHint,
#mapHint {
position: absolute;
bottom: 24px;
left: 50%;
@@ -65,6 +98,7 @@
}
</style>
</head>
<body>
<!-- ═══ NAVBAR ═══════════════════════════════════════════════════════════ -->
@@ -72,7 +106,7 @@
<div class="flex-1">
<a href="../index.html" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
<i class="fa-solid fa-house-chimney-crack"></i>
WebGIS Miskin & Ibadah
WebGIS Pemetaan Kemiskinan
</a>
</div>
<div class="flex-none">
@@ -104,13 +138,16 @@
Mode Operasi
</h3>
<div class="grid grid-cols-1 gap-2">
<button class="btn btn-sm btn-outline btn-primary justify-start gap-2 active" id="btnModeView" onclick="setMode('view')">
<button class="btn btn-sm btn-outline btn-primary justify-start gap-2 active" id="btnModeView"
onclick="setMode('view')">
<i class="fa-solid fa-eye"></i> View & Analisis
</button>
<button class="btn btn-sm btn-outline btn-success justify-start gap-2" id="btnModeIbadah" onclick="setMode('ibadah')">
<button class="btn btn-sm btn-outline btn-success justify-start gap-2" id="btnModeIbadah"
onclick="setMode('ibadah')">
<i class="fa-solid fa-plus"></i> Tambah Rumah Ibadah
</button>
<button class="btn btn-sm btn-outline btn-error justify-start gap-2" id="btnModeMiskin" onclick="setMode('miskin')">
<button class="btn btn-sm btn-outline btn-error justify-start gap-2" id="btnModeMiskin"
onclick="setMode('miskin')">
<i class="fa-solid fa-plus"></i> Tambah Rumah Miskin
</button>
</div>
@@ -130,12 +167,13 @@
<div class="text-xs font-bold text-primary truncate" id="activeName">Nama Ibadah</div>
<div class="text-[10px] text-base-content/40" id="activeAddr">Alamat</div>
</div>
<div class="form-control mb-4">
<label class="label py-1">
<span class="label-text-alt font-bold">Radius: <span id="radiusVal">500</span>m</span>
</label>
<input type="range" min="50" max="2000" value="500" step="50" class="range range-xs range-primary" id="radiusSlider">
<input type="range" min="50" max="2000" value="500" step="50"
class="range range-xs range-primary" id="radiusSlider">
</div>
<div class="grid grid-cols-3 gap-2">
@@ -168,7 +206,7 @@
</div>
<!-- ═══ MODALS ═══════════════════════════════════════════════════════════ -->
<!-- Modal: Input Ibadah -->
<dialog id="modalIbadah" class="modal">
<div class="modal-box">
@@ -179,11 +217,13 @@
<div class="bg-base-200 p-2 rounded text-xs font-mono" id="ib_coords">Lat: -, Lng: -</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Nama Rumah Ibadah</span></div>
<input type="text" id="ib_nama" placeholder="Contoh: Masjid Nurul Iman" class="input input-bordered w-full" />
<input type="text" id="ib_nama" placeholder="Contoh: Masjid Nurul Iman"
class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Alamat</span></div>
<textarea id="ib_alamat" class="textarea textarea-bordered h-20" placeholder="Alamat lengkap..."></textarea>
<textarea id="ib_alamat" class="textarea textarea-bordered h-20"
placeholder="Alamat lengkap..."></textarea>
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Radius Default Analysis (Meter)</span></div>
@@ -207,7 +247,8 @@
<div class="bg-base-200 p-2 rounded text-xs font-mono" id="rm_coords">Lat: -, Lng: -</div>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">Alamat</span></div>
<textarea id="rm_alamat" class="textarea textarea-bordered h-20" placeholder="Alamat lengkap..."></textarea>
<textarea id="rm_alamat" class="textarea textarea-bordered h-20"
placeholder="Alamat lengkap..."></textarea>
</label>
<label class="form-control w-full">
<div class="label"><span class="label-text font-bold">ID Rumah / Nama Pemilik</span></div>
@@ -255,7 +296,7 @@
let activeCircle = null, activeIbadah = null;
let ibadahData = [], miskinData = [];
let tempMarker = null;
let ibadahMarkers = new Map(); // id -> marker
let miskinMarkers = new Map(); // id_rumah -> marker
@@ -286,7 +327,7 @@
function initMap() {
map = L.map('map', { zoomControl: false }).setView([-0.0553, 109.3463], 17);
const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 });
const satellite = L.layerGroup([
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 }),
@@ -296,9 +337,9 @@
satellite.addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.control.layers({ "Satelit": satellite, "Street": osm },
{ "Rumah Ibadah": layerIbadah, "Rumah Miskin": layerMiskin },
{ position: 'topright' }).addTo(map);
L.control.layers({ "Satelit": satellite, "Street": osm },
{ "Rumah Ibadah": layerIbadah, "Rumah Miskin": layerMiskin },
{ position: 'topright' }).addTo(map);
layerIbadah.addTo(map);
layerMiskin.addTo(map);
@@ -315,7 +356,7 @@
fetch('../api/get_ibadah.php').then(r => r.json()),
fetch('../api/get_miskin.php').then(r => r.json())
]);
// Validate data is array
if (!Array.isArray(ri) || !Array.isArray(rm)) {
console.error("Invalid API response", { ri, rm });
@@ -324,7 +365,7 @@
ibadahData = ri;
miskinData = rm;
// Sync activeIbadah to point to the new object in the fresh data
if (activeIbadah) {
const matched = ibadahData.find(i => i.id == activeIbadah.id);
@@ -332,9 +373,9 @@
}
renderMarkers();
} catch (e) {
} catch (e) {
console.error(e);
showToast('Gagal memuat data', 'error');
showToast('Gagal memuat data', 'error');
}
}
@@ -348,16 +389,16 @@
const m = L.marker([item.latitude, item.longitude], { icon: icons.ibadah(), draggable: true })
.bindPopup(buildPopupIbadah(item))
.on('click', () => handleIbadahClick(item));
m.on('dragend', e => updateLocation('ibadah', item.id, e.target.getLatLng(), item));
layerIbadah.addLayer(m);
ibadahMarkers.set(item.id, m);
});
miskinData.forEach(item => {
const marker = L.marker([item.latitude, item.longitude], {
const marker = L.marker([item.latitude, item.longitude], {
icon: icons.miskin('#ef4444'),
draggable: true
draggable: true
}).bindPopup(buildPopupMiskin(item));
marker.on('dragend', e => updateLocation('miskin', item.id_rumah, e.target.getLatLng(), item));
@@ -376,10 +417,10 @@
currentMode = mode;
document.querySelectorAll('.btn-outline').forEach(b => b.classList.remove('active'));
document.getElementById('btnMode' + mode.charAt(0).toUpperCase() + mode.slice(1)).classList.add('active');
document.getElementById('mapHint').classList.toggle('hidden', mode === 'view');
document.getElementById('analysisPanel').classList.toggle('hidden', mode !== 'view');
if (mode !== 'view') {
if (activeCircle) { map.removeLayer(activeCircle); activeCircle = null; }
document.getElementById('statsContent').classList.add('hidden');
@@ -391,13 +432,13 @@
function handleMapClick(latlng) {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker(latlng, { icon: icons.temp() }).addTo(map);
if (currentMode === 'ibadah') {
document.getElementById('ib_coords').innerText = `Lat: ${latlng.lat.toFixed(6)}, Lng: ${latlng.lng.toFixed(6)}`;
document.getElementById('ib_nama').value = '';
document.getElementById('ib_alamat').value = 'Mencari alamat...';
modalIbadah.showModal();
// Auto fetch address
reverseGeocode(latlng.lat, latlng.lng, 'ib_alamat');
} else {
@@ -432,7 +473,7 @@
document.getElementById('statsContent').classList.remove('hidden');
document.getElementById('activeName').innerText = item.nama;
document.getElementById('activeAddr').innerText = item.alamat;
const radius = document.getElementById('radiusSlider').value = item.radius || 500;
updateRadius(item.latitude, item.longitude, radius);
}
@@ -441,25 +482,25 @@
function updateRadius(lat, lng, radius) {
if (activeCircle) map.removeLayer(activeCircle);
activeCircle = L.circle([lat, lng], {
radius: parseInt(radius),
color: '#2563eb',
fillColor: '#3b82f6',
fillOpacity: 0.2,
radius: parseInt(radius),
color: '#2563eb',
fillColor: '#3b82f6',
fillOpacity: 0.2,
weight: 2,
dashArray: '5, 10'
}).addTo(map);
document.getElementById('radiusVal').innerText = radius;
updateAnalysisUI([lat, lng], radius);
}
function updateAnalysisUI(center, radius) {
let houses = 0, kk = 0, jiwa = 0;
miskinData.forEach(h => {
const houseMarker = miskinMarkers.get(h.id_rumah);
const isH = isInside(h, center, radius);
if (isH) {
houses++;
kk += parseInt(h.jumlah_kk);
@@ -484,7 +525,7 @@
// center can be [lat, lng] or {lat, lng}
const cLat = center.lat !== undefined ? center.lat : center[0];
const cLng = center.lng !== undefined ? center.lng : center[1];
const p1 = turf.point([Number(point.longitude), Number(point.latitude)]);
const p2 = turf.point([Number(cLng), Number(cLat)]);
return turf.distance(p1, p2, { units: 'meters' }) <= radius;
@@ -499,7 +540,7 @@
// Save radius to DB when user finishes sliding
document.getElementById('radiusSlider').addEventListener('change', async e => {
if (!activeIbadah) return;
const newRadius = e.target.value;
try {
const res = await fetch('../api/update_ibadah.php', {
@@ -559,11 +600,11 @@
};
const btn = document.getElementById('btnSaveIbadah');
btn.disabled = true;
const res = await fetch('../api/save_ibadah.php', {
method: 'POST',
const res = await fetch('../api/save_ibadah.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
@@ -586,10 +627,10 @@
const btn = document.getElementById('btnSaveMiskin');
btn.disabled = true;
const res = await fetch('../api/save_miskin.php', {
method: 'POST',
const res = await fetch('../api/save_miskin.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
body: JSON.stringify(data)
});
const resJson = await res.json();
if (resJson.success) {
@@ -602,15 +643,15 @@
async function updateLocation(type, id, latlng, oldData) {
const endpoint = `../api/update_${type}.php`;
const payload = type === 'ibadah'
const payload = type === 'ibadah'
? { id, nama: oldData.nama, alamat: oldData.alamat, latitude: latlng.lat, longitude: latlng.lng, radius: oldData.radius }
: { id_rumah: id, alamat: oldData.alamat, jumlah_kk: oldData.jumlah_kk, jumlah_orang: oldData.jumlah_orang, latitude: latlng.lat, longitude: latlng.lng };
try {
const res = await fetch(endpoint, {
method: 'POST',
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
body: JSON.stringify(payload)
});
const resJson = await res.json();
if (resJson.success) {
@@ -635,10 +676,10 @@
document.getElementById('confirmDeleteBtn').onclick = async () => {
const { type, id } = deleteTarget;
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST',
const res = await fetch(`../api/delete_${type}.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
body: JSON.stringify(type === 'ibadah' ? { id } : { id_rumah: id })
});
const resJson = await res.json();
if (resJson.success) {
@@ -659,4 +700,5 @@
}
</script>
</body>
</html>
</html>