Initial commit

This commit is contained in:
2026-06-10 00:49:56 +07:00
commit 813cfcaf9f
27 changed files with 24734 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
CREATE TABLE lokasi (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_spbu VARCHAR(100) NOT NULL,
no_wa VARCHAR(50),
status VARCHAR(50),
latitude DOUBLE,
longitude DOUBLE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(100) NOT NULL,
status VARCHAR(50),
panjang DOUBLE,
geom TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE kavling (
id INT AUTO_INCREMENT PRIMARY KEY,
pemilik VARCHAR(100) NOT NULL,
status VARCHAR(50),
luas DOUBLE,
geom TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+37
View File
@@ -0,0 +1,37 @@
<?php
include 'koneksi.php';
// =====================
// SPBU (POINT)
// =====================
$spbu = [];
$q1 = $conn->query("SELECT * FROM lokasi");
while($row = $q1->fetch_assoc()){
$spbu[] = $row;
}
// =====================
// JALAN (POLYLINE)
// =====================
$jalan = [];
$q2 = $conn->query("SELECT * FROM jalan");
while($row = $q2->fetch_assoc()){
$jalan[] = $row;
}
// =====================
// KAVLING (POLYGON)
// =====================
$kavling = [];
$q3 = $conn->query("SELECT * FROM kavling");
while($row = $q3->fetch_assoc()){
$kavling[] = $row;
}
// gabungkan
echo json_encode([
"spbu" => $spbu,
"jalan" => $jalan,
"kavling" => $kavling
]);
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
include 'koneksi.php';
$id = $_GET['id'];
$type = $_GET['type'];
if($type == 'spbu'){
$conn->query("DELETE FROM lokasi WHERE id=$id");
}
elseif($type == 'jalan'){
$conn->query("DELETE FROM jalan WHERE id=$id");
}
elseif($type == 'kavling'){
$conn->query("DELETE FROM kavling WHERE id=$id");
}
echo "OK";
?>
+993
View File
@@ -0,0 +1,993 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU — Kota Pontianak</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
font-family: 'Inter', -apple-system, 'Segoe UI', sans-serif;
background-color: #f1f5f9;
}
#map {
margin-left: 300px;
height: 100vh;
width: calc(100% - 300px);
}
/* =========================================
SIDEBAR
========================================= */
#sidebar {
position: absolute;
left: 0; top: 0;
width: 300px;
height: 100%;
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
color: #f8fafc;
z-index: 1000;
padding: 0;
overflow-y: auto;
box-shadow: 4px 0 25px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
}
/* Scrollbar styling */
#sidebar::-webkit-scrollbar { width: 4px; }
#sidebar::-webkit-scrollbar-track { background: transparent; }
#sidebar::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
#sidebar h2 {
text-align: left;
margin: 0;
padding: 24px 20px 20px;
font-size: 20px;
font-weight: 700;
letter-spacing: -0.3px;
background: linear-gradient(135deg, rgba(6,182,212,0.1) 0%, rgba(139,92,246,0.08) 100%);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.section {
margin-top: 4px;
padding: 0 20px;
}
.section h3 {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #64748b;
margin-bottom: 8px;
margin-top: 16px;
font-weight: 700;
}
.section h4 {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
margin-top: 14px;
font-weight: 600;
}
/* Search input */
#searchInput {
width: calc(100% - 40px);
margin: 16px 20px 4px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #334155;
background: rgba(15, 23, 42, 0.6);
color: #f8fafc;
font-family: inherit;
font-size: 13px;
outline: none;
transition: all 0.3s;
}
#searchInput::placeholder { color: #475569; }
#searchInput:focus {
border-color: #06b6d4;
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1);
background: rgba(15, 23, 42, 0.8);
}
/* List items */
.list-item {
background: rgba(51, 65, 85, 0.5);
padding: 10px 12px;
margin-bottom: 6px;
border-radius: 10px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
}
.list-item:hover {
background: rgba(71, 85, 105, 0.6);
transform: translateX(4px);
border-color: rgba(6, 182, 212, 0.3);
box-shadow: 0 2px 12px rgba(6, 182, 212, 0.06);
}
.list-item div[style*="font-weight"] { font-size: 13px; }
.list-item small { font-size: 10px; color: #64748b !important; }
/* Sidebar buttons */
#sidebar button {
width: 100%;
padding: 10px;
margin-top: 5px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #0891b2, #06b6d4);
color: white;
font-weight: 600;
font-family: inherit;
font-size: 13px;
cursor: pointer;
transition: all 0.25s;
box-shadow: 0 2px 8px rgba(6, 182, 212, 0.2);
}
#sidebar button:hover {
background: linear-gradient(135deg, #0e7490, #0891b2);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
}
/* =========================================
POPUP FORM STYLING
========================================= */
.leaflet-popup-content {
font-family: 'Inter', sans-serif !important;
font-size: 13px;
line-height: 1.5;
min-width: 220px;
}
.leaflet-popup-content b {
font-size: 15px;
font-weight: 700;
color: #0f172a;
}
input[type="text"], select {
width: 100%;
padding: 8px 10px;
margin-bottom: 10px;
margin-top: 4px;
border-radius: 8px;
border: 1px solid #e2e8f0;
background: #f8fafc;
font-family: inherit;
font-size: 13px;
color: #0f172a;
outline: none;
transition: all 0.2s;
}
input[type="text"]:focus, select:focus {
border-color: #06b6d4;
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1);
background: #ffffff;
}
form button[type="submit"] {
background: linear-gradient(135deg, #059669, #10b981);
color: white;
border: none;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
width: 100%;
font-family: inherit;
font-size: 13px;
font-weight: 600;
transition: all 0.25s;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
}
form button[type="submit"]:hover {
background: linear-gradient(135deg, #047857, #059669);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
/* Popup edit/delete buttons */
.leaflet-popup-content button {
padding: 6px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 600;
transition: all 0.2s;
margin-right: 4px;
}
.leaflet-editing-icon {
width: 12px;
height: 12px;
}
</style>
</head>
<body>
<div id="sidebar">
<h2>🌍 WebGIS</h2>
<input type="text" id="searchInput" placeholder="Cari layer..." onkeyup="filterList()">
<div class="section">
<h3>📍 Layer</h3>
<h4>⛽ SPBU 24 Jam</h4>
<div id="list24"></div>
<h4>⛽ SPBU Tidak 24 Jam</h4>
<div id="listTidak"></div>
<h4>🛣️ Jalan</h4>
<div id="listJalan"></div>
<h4>🏡 Kavling</h4>
<div id="listKavling"></div>
</div>
<div class="section">
<h3>🎛 Tools</h3>
<button onclick="map.zoomIn()">Zoom In</button>
<button onclick="map.zoomOut()">Zoom Out</button>
</div>
</div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw/dist/leaflet.draw.css"/>
<script src="https://unpkg.com/leaflet-draw/dist/leaflet.draw.js"></script>
<script src="https://unpkg.com/leaflet-geometryutil"></script>
<script>
// =======================
// FUNGSI SEARCH LAYER
// =======================
function filterList() {
let input = document.getElementById('searchInput').value.toLowerCase();
let items = document.querySelectorAll('.list-item');
items.forEach(item => {
let text = item.innerText.toLowerCase();
item.style.display = text.includes(input) ? "block" : "none";
});
}
// =======================
// HAPUS DATA
// =======================
function hapusData(id){
if(confirm("Yakin hapus data?")){
fetch('hapus.php?id=' + id + '&type=spbu')
.then(res => res.text())
.then(res => {
alert("Data dihapus!");
location.reload();
});
}
}
// =======================
// EDIT DATA
// =======================
function editData(id, nama, wa, status){
let formEdit = `
<form id="editForm">
<input type="hidden" name="type" value="spbu">
<input type="hidden" name="id" value="${id}">
Nama SPBU:
<input type="text" name="nama_spbu" value="${nama}">
No WA:
<input type="text" name="no_wa" value="${wa}">
Status:
<select name="status">
<option value="24jam" ${status === '24jam' ? 'selected' : ''}>24 Jam</option>
<option value="tidak" ${status === 'tidak' ? 'selected' : ''}>Tidak</option>
</select>
<button type="submit">Update</button>
</form>
`;
L.popup()
.setLatLng(map.getCenter())
.setContent(formEdit)
.openOn(map);
}
// =======================
// SUBMIT EDIT
// =======================
document.addEventListener('submit', function(e){
if(e.target && e.target.id === 'editForm'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('update.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Data berhasil diupdate!");
location.reload();
} else {
alert("Gagal mengupdate data: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
// =======================
// INIT MAP
// =======================
// Menambahkan zoomControl: false agar tombol +- bawaan yang tertimpa sidebar hilang
const map = L.map('map', { zoomControl: false }).setView([-0.0557, 109.3491], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
// AKTIFKAN DRAW TOOL
var drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
var drawControl = new L.Control.Draw({
position: 'topright', // POSISI DRAW TOOL DIUBAH KE KANAN ATAS
draw: {
polygon: true,
polyline: true,
marker: true,
circle: false,
rectangle: false
},
edit: {
featureGroup: drawnItems,
edit: true,
remove: true
}
});
// HITUNG PANJANG & LUAS OTOMATIS
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var type = e.layerType;
// =====================
// MARKER (SPBU)
// =====================
if(type === 'marker'){
var spbuIcon = L.icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png',
iconSize: [32, 32]
});
layer.setIcon(spbuIcon);
let lat = layer.getLatLng().lat;
let lng = layer.getLatLng().lng;
let form = `
<form id="formData">
<b>Tambah SPBU</b><br><br>
Nama SPBU:
<input type="text" name="nama_spbu" required>
No WhatsApp:
<input type="text" name="no_wa" required>
Status:
<select name="status">
<option value="24jam">Buka 24 Jam</option>
<option value="tidak">Tidak</option>
</select>
<input type="hidden" name="latitude" value="${lat}">
<input type="hidden" name="longitude" value="${lng}">
<button type="submit">Simpan</button>
</form>
`;
layer.bindPopup(form).openPopup();
}
// =====================
// TAMBAH KE MAP
// =====================
drawnItems.addLayer(layer);
let geojson = layer.toGeoJSON();
// =====================
// POLYLINE
// =====================
if(type === 'polyline'){
layer.setStyle({
color: 'blue',
weight: 4
});
let latlngs = layer.getLatLngs();
let panjang = 0;
for(let i=0; i<latlngs.length-1; i++){
panjang += latlngs[i].distanceTo(latlngs[i+1]);
}
layer.bindPopup(`
<form id="formJalan">
Nama Jalan:
<input type="text" name="nama_jalan">
Status:
<select name="status">
<option value="nasional">Nasional</option>
<option value="provinsi">Provinsi</option>
<option value="kabupaten">Kabupaten</option>
</select>
Panjang: ${panjang.toFixed(2)} meter
<input type="hidden" name="panjang" value="${panjang}">
<input type="hidden" name="geom" value='${JSON.stringify(geojson)}'>
<button type="submit">Simpan</button>
</form>
`).openPopup();
}
// =====================
// POLYGON
// =====================
if(type === 'polygon'){
let latlngs = layer.getLatLngs()[0];
let luas = L.GeometryUtil.geodesicArea(latlngs);
layer.myId = null; // sementara
drawnItems.addLayer(layer);
layer.bindPopup(`
<form id="formKavling">
Nama Pemilik:
<input type="text" name="pemilik">
Status:
<select name="status">
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
Luas: ${luas.toFixed(2)}
<input type="hidden" name="luas" value="${luas}">
<input type="hidden" name="geom" value='${JSON.stringify(geojson)}'>
<button type="submit">Simpan</button>
</form>
`).openPopup();
}
});
// WARNA OTOMATIS
// Polyline
function getWarnaJalan(status){
if(status === 'nasional') return 'blue';
if(status === 'provinsi') return 'green';
return 'red';
}
// Polygon
function getWarnaKavling(status){
if(status === 'SHM') return 'green';
if(status === 'HGB') return 'blue';
if(status === 'HGU') return 'orange';
return 'purple';
}
// SIMPAN JALAN
document.addEventListener('submit', function(e){
if(e.target.id === 'formJalan'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('simpan_jalan.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Jalan disimpan!");
location.reload();
} else {
alert("Gagal menyimpan jalan: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
// SIMPAN KAVLING
document.addEventListener('submit', function(e){
if(e.target.id === 'formKavling'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('simpan_kavling.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Kavling disimpan!");
location.reload();
} else {
alert("Gagal menyimpan kavling: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
// =======================
// ICON MARKER
// =======================
const greenIcon = new L.Icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png',
iconSize: [32, 32]
});
const redIcon = new L.Icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/red-dot.png',
iconSize: [32, 32]
});
// =======================
// LAYER GROUP SPBU
// =======================
var spbu24Jam = L.layerGroup();
var spbuTidak24Jam = L.layerGroup();
// Tambahkan ke map default (opsional)
spbu24Jam.addTo(map);
spbuTidak24Jam.addTo(map);
fetch('get_all.php')
.then(res => res.json())
.then(data => {
// =====================
// SPBU (POINT)
// =====================
if(data.spbu) { // Ditambahkan pengecekan agar tidak error jika data kosong
data.spbu.forEach(item => {
let icon = item.status === '24jam' ? greenIcon : redIcon;
let marker = L.marker([item.latitude, item.longitude], {
icon: icon,
draggable: true
});
marker.myId = item.id;
// MASUKKAN KE GROUP SESUAI STATUS
drawnItems.addLayer(marker);
// tetap simpan ke group hanya untuk kontrol layer
if(item.status === '24jam'){
spbu24Jam.addLayer(marker);
} else {
spbuTidak24Jam.addLayer(marker);
}
// tetap tambahkan ke drawnItems jika ingin bisa edit
drawnItems.addLayer(marker);
marker.bindPopup(`
<b>${item.nama_spbu}</b><br>
WA: ${item.no_wa}<br>
Status: ${item.status}<br><br>
<button onclick="editData(${item.id}, '${item.nama_spbu}', '${item.no_wa}', '${item.status}')">Edit</button>
<button onclick="hapusData(${item.id})">Hapus</button>
`);
marker.on('dragend', function(e){
let posisi = e.target.getLatLng();
fetch('update_posisi.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `id=${item.id}&lat=${posisi.lat}&lng=${posisi.lng}`
});
});
tambahKeList(
item.nama_spbu,
[item.latitude, item.longitude],
marker,
item.status,
'spbu'
);
});
}
// =======================
// LAYER CONTROL
// =======================
var overlayMaps = {
"SPBU 24 Jam": spbu24Jam,
"SPBU Tidak 24 Jam": spbuTidak24Jam
};
L.control.layers(null, overlayMaps, {
collapsed: false
}).addTo(map);
// =====================
// KAVLING (POLYGON)
// =====================
if(data.kavling) {
data.kavling.forEach(item => {
try {
let geojson = JSON.parse(item.geom);
let warna = getWarnaKavling(item.status);
let layer = L.geoJSON(geojson, {
style: {
color: warna,
fillOpacity: 0.5
}
});
layer.eachLayer(l => {
l.myId = item.id;
drawnItems.addLayer(l);
l.bindPopup(`
<b>${item.pemilik || 'Tanpa Nama'}</b><br>
Status: ${item.status}<br>
Luas: ${parseFloat(item.luas).toFixed(2)} m²<br><br>
<button onclick="editKavling(${item.id}, '${item.pemilik || ''}', '${item.status}')">Edit</button>
<button onclick="hapusKavling(${item.id})">Hapus</button>
`);
tambahKeList(item.pemilik || 'Kavling', l.getBounds().getCenter(), l, null, 'kavling');
});
} catch(e){
console.log("Geom error kavling ID:", item.id);
}
});
}
// =====================
// JALAN (POLYLINE)
// =====================
if(data.jalan) {
data.jalan.forEach(item => {
try {
let geojson = JSON.parse(item.geom);
let warna = getWarnaJalan(item.status);
let layer = L.geoJSON(geojson, {
style: {
color: warna,
weight: 4
}
});
layer.eachLayer(l => {
l.myId = item.id;
drawnItems.addLayer(l);
l.bindPopup(`
<b>${item.nama_jalan}</b><br>
Status: ${item.status}<br>
Panjang: ${parseFloat(item.panjang).toFixed(2)} meter<br><br>
<button onclick="editJalan(${item.id}, '${item.nama_jalan}', '${item.status}')">Edit</button>
<button onclick="hapusJalan(${item.id})">Hapus</button>
`);
tambahKeList(item.nama_jalan, l.getBounds().getCenter(), l, null, 'jalan');
});
} catch(e){
console.log("Geom error jalan ID:", item.id);
}
});
}
});
// =======================
// SUBMIT FORM
// =======================
document.addEventListener('submit', function(e){
if(e.target && e.target.id === 'formData'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('simpan.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Data berhasil disimpan!");
location.reload();
} else {
alert("Gagal menyimpan data: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
map.on(L.Draw.Event.EDITED, function (e) {
let layers = e.layers;
layers.eachLayer(function(layer){
// =====================
// MARKER (SPBU)
// =====================
if(layer instanceof L.Marker){
let latlng = layer.getLatLng();
fetch('update_posisi.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `id=${layer.myId}&lat=${latlng.lat}&lng=${latlng.lng}`
});
}
// =====================
// POLYLINE / POLYGON
// =====================
else {
let geojson = layer.toGeoJSON();
let type = (layer instanceof L.Polygon) ? 'kavling' : 'jalan';
fetch('update_geom.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: layer.myId,
geom: geojson,
type: type
})
});
}
});
});
function hapusJalan(id){
if(confirm("Yakin hapus jalan?")){
fetch('hapus.php?id=' + id + '&type=jalan')
.then(res => res.text())
.then(res => {
alert("Jalan dihapus!");
location.reload();
});
}
}
function hapusKavling(id){
if(confirm("Yakin hapus kavling?")){
fetch('hapus.php?id=' + id + '&type=kavling')
.then(() => {
alert("Kavling dihapus!");
location.reload();
});
}
}
function editJalan(id, nama, status){
let form = `
<form id="editJalanForm">
<input type="hidden" name="type" value="jalan">
<input type="hidden" name="id" value="${id}">
Nama Jalan:
<input type="text" name="nama_jalan" value="${nama}" required>
Status:
<select name="status">
<option value="nasional" ${status==='nasional'?'selected':''}>Nasional</option>
<option value="provinsi" ${status==='provinsi'?'selected':''}>Provinsi</option>
<option value="kabupaten" ${status==='kabupaten'?'selected':''}>Kabupaten</option>
</select>
<button type="submit">Update</button>
</form>
`;
L.popup()
.setLatLng(map.getCenter())
.setContent(form)
.openOn(map);
}
document.addEventListener('submit', function(e){
// EDIT JALAN
if(e.target.id === 'editJalanForm'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('update.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Jalan berhasil diupdate!");
location.reload();
} else {
alert("Gagal mengupdate jalan: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
// EDIT KAVLING
if(e.target.id === 'editKavlingForm'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('update.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Kavling berhasil diupdate!");
location.reload();
} else {
alert("Gagal mengupdate kavling: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
function editKavling(id, pemilik, status){
let form = `
<form id="editKavlingForm">
<input type="hidden" name="type" value="kavling">
<input type="hidden" name="id" value="${id}">
Nama Pemilik:
<input type="text" name="pemilik" value="${pemilik}">
Status:
<select name="status">
<option value="SHM" ${status==='SHM'?'selected':''}>SHM</option>
<option value="HGB" ${status==='HGB'?'selected':''}>HGB</option>
<option value="HGU" ${status==='HGU'?'selected':''}>HGU</option>
<option value="HP" ${status==='HP'?'selected':''}>HP</option>
</select>
<button type="submit">Update</button>
</form>
`;
L.popup().setLatLng(map.getCenter()).setContent(form).openOn(map);
}
map.on(L.Draw.Event.DRAWSTART, function(){
console.log("Mulai gambar");
});
map.on(L.Draw.Event.DRAWSTOP, function(){
console.log("Selesai gambar");
});
function tambahKeList(nama, latlng, layer, status, type){
let div;
let icon = "";
if (type === 'jalan') {
div = document.getElementById('listJalan');
icon = "🛣️";
} else if (type === 'kavling') {
div = document.getElementById('listKavling');
icon = "🏡";
} else {
// PILIH CONTAINER BERDASARKAN STATUS
if(status === '24jam'){
div = document.getElementById('list24');
} else {
div = document.getElementById('listTidak');
}
icon = "⛽";
}
let item = document.createElement('div');
item.className = 'list-item';
item.innerHTML = `
<div style="font-weight:bold">${icon} ${nama}</div>
<small style="color:#cbd5f5;">Klik untuk fokus</small>
`;
item.onclick = function(){
map.setView(latlng, 16);
if(layer.getBounds){
map.fitBounds(layer.getBounds());
}
layer.openPopup();
};
div.appendChild(item);
}
</script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<?php
$conn = new mysqli("localhost", "root", "", "webgis1");
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
include 'koneksi.php';
$nama = $_POST['nama_spbu'];
$wa = $_POST['no_wa'];
$status = $_POST['status'];
$lat = $_POST['latitude'];
$lng = $_POST['longitude'];
$stmt = $conn->prepare("INSERT INTO lokasi (nama_spbu, no_wa, status, latitude, longitude) VALUES (?, ?, ?, ?, ?)");
$stmt->bind_param("sssdd", $nama, $wa, $status, $lat, $lng);
if($stmt->execute()){
echo "OK";
} else {
echo "Gagal";
}
?>
+36
View File
@@ -0,0 +1,36 @@
<?php
include 'koneksi.php';
// ambil data
$nama = $_POST['nama_jalan'];
$status = $_POST['status'];
$panjang = $_POST['panjang'];
$geom = $_POST['geom'];
// cek data kosong
if(empty($nama) || empty($geom)){
echo "Data tidak lengkap";
exit;
}
// prepare query
$stmt = $conn->prepare("INSERT INTO jalan (nama_jalan, status, panjang, geom) VALUES (?, ?, ?, ?)");
if(!$stmt){
echo "Prepare gagal: " . $conn->error;
exit;
}
// binding
$stmt->bind_param("ssds", $nama, $status, $panjang, $geom);
// eksekusi
if($stmt->execute()){
echo "OK";
} else {
echo "Gagal: " . $stmt->error;
}
$stmt->close();
$conn->close();
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
include 'koneksi.php';
$pemilik = $_POST['pemilik'];
$status = $_POST['status'];
$luas = $_POST['luas'];
$geom = $_POST['geom'];
if(empty($pemilik) || empty($geom)){
echo "Data tidak lengkap";
exit;
}
$stmt = $conn->prepare("INSERT INTO kavling (pemilik, status, luas, geom) VALUES (?, ?, ?, ?)");
if(!$stmt){
echo "Prepare gagal: " . $conn->error;
exit;
}
$stmt->bind_param("ssds", $pemilik, $status, $luas, $geom);
if($stmt->execute()){
echo "OK";
} else {
echo "Gagal: " . $stmt->error;
}
$stmt->close();
$conn->close();
?>
+50
View File
@@ -0,0 +1,50 @@
<?php
include 'koneksi.php';
$type = $_POST['type'];
// =====================
// UPDATE SPBU
// =====================
if($type == 'spbu'){
$id = $_POST['id'];
$nama = $_POST['nama_spbu'];
$wa = $_POST['no_wa'];
$status = $_POST['status'];
$stmt = $conn->prepare("UPDATE lokasi SET nama_spbu=?, no_wa=?, status=? WHERE id=?");
$stmt->bind_param("sssi", $nama, $wa, $status, $id);
$stmt->execute();
}
// =====================
// UPDATE JALAN
// =====================
elseif($type == 'jalan'){
$id = $_POST['id'];
$nama = $_POST['nama_jalan'];
$status = $_POST['status'];
$stmt = $conn->prepare("UPDATE jalan SET nama_jalan=?, status=? WHERE id=?");
$stmt->bind_param("ssi", $nama, $status, $id);
$stmt->execute();
}
// =====================
// UPDATE KAVLING
// =====================
elseif($type == 'kavling'){
$id = $_POST['id'];
$pemilik = $_POST['pemilik'];
$status = $_POST['status'];
$stmt = $conn->prepare("UPDATE kavling SET pemilik=?, status=? WHERE id=?");
$stmt->bind_param("ssi", $pemilik, $status, $id);
$stmt->execute();
}
echo "OK";
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
include 'koneksi.php';
// ambil data JSON dari JS
$data = json_decode(file_get_contents("php://input"), true);
$id = $data['id'];
$geom = json_encode($data['geom']);
$type = $data['type'];
if ($type == 'jalan') {
$conn->query("UPDATE jalan SET geom='$geom' WHERE id=$id");
} elseif ($type == 'kavling') {
$conn->query("UPDATE kavling SET geom='$geom' WHERE id=$id");
}
echo "OK";
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
include 'koneksi.php';
$id = $_POST['id'];
$lat = $_POST['lat'];
$lng = $_POST['lng'];
$stmt = $conn->prepare("UPDATE lokasi SET latitude=?, longitude=? WHERE id=?");
$stmt->bind_param("ddi", $lat, $lng, $id);
$stmt->execute();
echo "OK";
?>
+160
View File
@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kepadatan Penduduk Kota Pontianak</title>
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<style>
html, body {
height: 100%;
margin: 0;
}
#map {
width: 100%;
height: 100vh;
}
</style>
<style>
.info { padding: 6px 8px; font: 14px/16px Arial, Helvetica, sans-serif; background: white; background: rgba(255,255,255,0.8); box-shadow: 0 0 15px rgba(0,0,0,0.2); border-radius: 5px; } .info h4 { margin: 0 0 5px; color: #777; }
.legend { text-align: left; line-height: 18px; color: #555; } .legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; }</style>
</head>
<body>
<div id='map'></div>
<script type="text/javascript" src="pontianak.js"></script>
<script type="text/javascript">
const map = L.map('map').setView([-0.02, 109.34], 12);
const tiles = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// control that shows state info on hover
const info = L.control();
info.onAdd = function (map) {
this._div = L.DomUtil.create('div', 'info');
this.update();
return this._div;
};
info.update = function (props) {
const contents = props
? `<b>${props.Ket}</b><br />Jumlah Penduduk: ${props.jmlPenddk} jiwa`
: 'Arahkan kursor ke wilayah';
this._div.innerHTML = `<h4>Kepadatan Penduduk Kota Pontianak</h4>${contents}`;
};
info.addTo(map);
// get color depending on population density value
function getColor(d) {
return d > 100000 ? '#800026' :
d > 80000 ? '#BD0026' :
d > 60000 ? '#E31A1C' :
d > 40000 ? '#FC4E2A' :
d > 20000 ? '#FD8D3C' :
d > 10000 ? '#FEB24C' :
d > 5000 ? '#FED976' :
'#FFEDA0';
}
function style(feature) {
return {
weight: 2,
opacity: 1,
color: 'white',
dashArray: '3',
fillOpacity: 0.7,
fillColor: getColor(feature.properties.jmlPenddk)
};
}
function highlightFeature(e) {
const layer = e.target;
layer.setStyle({
weight: 5,
color: '#666',
dashArray: '',
fillOpacity: 0.7
});
layer.bringToFront();
info.update(layer.feature.properties);
}
/* global statesData */
const geojson = L.geoJson(datapontianak, {
style,
onEachFeature
}).addTo(map);
map.fitBounds(geojson.getBounds());
function resetHighlight(e) {
geojson.resetStyle(e.target);
info.update();
}
function zoomToFeature(e) {
map.fitBounds(e.target.getBounds());
}
function onEachFeature(feature, layer) {
layer.on({
mouseover: highlightFeature,
mouseout: resetHighlight,
click: zoomToFeature
});
}
map.attributionControl.addAttribution('Population data &copy; <a href="http://census.gov/">US Census Bureau</a>');
const legend = L.control({position: 'bottomright'});
legend.onAdd = function (map) {
const div = L.DomUtil.create('div', 'info legend');
const grades = [0, 5000, 10000, 20000, 40000, 60000, 80000, 100000];
const labels = [];
let from, to;
for (let i = 0; i < grades.length; i++) {
from = grades[i];
to = grades[i + 1];
labels.push(`<i style="background:${getColor(from + 1)}"></i> ${from}${to ? `&ndash;${to}` : '+'}`);
}
div.innerHTML = labels.join('<br>');
return div;
};
legend.addTo(map);
</script>
</body>
</html>
+19407
View File
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* get_data.php v3 — Ambil Semua Data
*/
require_once 'koneksi.php';
$response = ['success'=>true, 'centers'=>[], 'houses'=>[], 'reports'=>[]];
// ======================== Centers
$result = $conn->query("SELECT * FROM religious_centers ORDER BY id ASC");
if ($result) {
while ($row = $result->fetch_assoc()) {
$response['centers'][] = [
'id' => (int)$row['id'],
'name' => $row['name'],
'address' => $row['address'],
'kas' => (float)($row['kas'] ?? 0),
'latitude' => (float)$row['latitude'],
'longitude' => (float)$row['longitude'],
'radius' => (int)$row['radius']
];
}
$result->free();
}
// ======================== Houses (all fields incl. anggota JSON)
$result = $conn->query("SELECT * FROM houses ORDER BY id ASC");
if ($result) {
while ($row = $result->fetch_assoc()) {
$anggota = [];
if (!empty($row['anggota'])) {
$decoded = json_decode($row['anggota'], true);
if (is_array($decoded)) $anggota = $decoded;
}
$response['houses'][] = [
'id' => (int)$row['id'],
'latitude' => (float)$row['latitude'],
'longitude' => (float)$row['longitude'],
'address' => $row['address'] ?? '',
'rt' => $row['rt'] ?? '',
'rw' => $row['rw'] ?? '',
'kelurahan' => $row['kelurahan'] ?? '',
'status_miskin' => $row['status_miskin'] ?? '',
'jumlah_anggota'=> (int)($row['jumlah_anggota'] ?? 0),
'anggota' => $anggota,
'aid_status' => $row['aid_status'] ?? 'outside',
'has_data' => (int)($row['has_data'] ?? 0)
];
}
$result->free();
}
// ======================== Reports
$result = $conn->query("SELECT * FROM laporan ORDER BY created_at DESC LIMIT 50");
if ($result) {
while ($row = $result->fetch_assoc()) {
$response['reports'][] = [
'id' => (int)$row['id'],
'name' => $row['pelapor'] ?? 'Anonim',
'text' => $row['deskripsi'],
'imgBase64'=> $row['foto_base64'] ?? null,
'time' => date('d/m/Y H:i', strtotime($row['created_at']))
];
}
$result->free();
}
echo json_encode($response);
$conn->close();
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* hapus_pusat.php — Hapus Rumah Ibadah
* POST: id
*/
require_once 'koneksi.php';
$id = intval($_POST['id'] ?? 0);
if ($id < 1) { echo json_encode(['success'=>false,'message'=>'ID tidak valid']); exit; }
// Hapus log yang terkait
$conn->query("DELETE FROM aid_logs WHERE religious_center_id=$id");
if ($conn->query("DELETE FROM religious_centers WHERE id=$id")) {
echo json_encode(['success'=>true,'id'=>$id]);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
$conn->close();
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* hapus_rumah.php — Hapus Data Rumah Miskin
* POST: id
*/
require_once 'koneksi.php';
$id = intval($_POST['id'] ?? 0);
if ($id < 1) { echo json_encode(['success'=>false,'message'=>'ID tidak valid']); exit; }
// Hapus log yang terkait
$conn->query("DELETE FROM aid_logs WHERE house_id=$id");
if ($conn->query("DELETE FROM houses WHERE id=$id")) {
echo json_encode(['success'=>true,'id'=>$id]);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
$conn->close();
+410
View File
@@ -0,0 +1,410 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS — Distribusi Bantuan Sosial Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<!-- =====================================================================
TOP NAVBAR — selalu terlihat di semua halaman
===================================================================== -->
<nav id="topNav">
<!-- Kiri: Logo -->
<div class="nav-brand">
<span class="nav-brand-icon">🕌</span>
<div>
<div class="nav-brand-title">BantSOSial GIS</div>
<div class="nav-brand-sub">Pontianak · Kalimantan Barat</div>
</div>
</div>
<!-- Tengah: Menu navigasi -->
<div class="nav-menu">
<button class="nav-item active" id="navHome" onclick="navigateTo('map')">
<span class="nav-item-icon">🗺</span>
<span class="nav-item-label">Peta</span>
</button>
<button class="nav-item" id="navReport" onclick="navigateTo('report')">
<span class="nav-item-icon">📢</span>
<span class="nav-item-label">Pelaporan</span>
<span class="nav-badge hidden" id="navReportBadge">0</span>
</button>
</div>
<!-- Kanan: Role switcher -->
<div class="nav-right">
<div id="roleIndicator" class="role-pill">
<span id="roleIcon">👑</span>
<span id="roleLabel">Admin</span>
</div>
<button id="roleSwitchBtn" onclick="openRoleModal()">Ganti Role ▾</button>
</div>
</nav>
<!-- =====================================================================
ROLE MODAL
===================================================================== -->
<div id="roleModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:420px">
<div class="modal-header">
<div class="modal-title">🔐 Pilih Role Pengguna</div>
<button class="modal-close" onclick="closeRoleModal()"></button>
</div>
<div class="modal-body" style="padding:16px">
<p style="font-size:12px;color:var(--text-muted);margin-bottom:14px">Pilih role sesuai hak akses Anda. Perubahan langsung berlaku.</p>
<div class="role-cards">
<div class="role-card" id="roleCard_admin" onclick="setRole('admin')">
<div class="role-card-icon">👑</div>
<div class="role-card-name">Admin</div>
<div class="role-card-desc">Akses penuh — tambah, edit, hapus, lihat semua data dan laporan</div>
<div class="role-card-perms">
<span class="perm perm-green">✓ Tambah & Edit</span>
<span class="perm perm-green">✓ Hapus & Reset</span>
<span class="perm perm-green">✓ Lihat laporan</span>
<span class="perm perm-green">✓ Kirim laporan</span>
</div>
</div>
<div class="role-card" id="roleCard_surveyer" onclick="setRole('surveyer')">
<div class="role-card-icon">📋</div>
<div class="role-card-name">Surveyer</div>
<div class="role-card-desc">Input data & kelola laporan — tidak bisa hapus atau reset</div>
<div class="role-card-perms">
<span class="perm perm-green">✓ Tambah & Edit</span>
<span class="perm perm-red">✕ Hapus & Reset</span>
<span class="perm perm-green">✓ Lihat laporan</span>
<span class="perm perm-green">✓ Kirim laporan</span>
</div>
</div>
<div class="role-card" id="roleCard_viewer" onclick="setRole('viewer')">
<div class="role-card-icon">👁</div>
<div class="role-card-name">Viewer</div>
<div class="role-card-desc">Hanya lihat peta & kirim laporan masyarakat</div>
<div class="role-card-perms">
<span class="perm perm-green">✓ Lihat peta</span>
<span class="perm perm-red">✕ Edit data</span>
<span class="perm perm-red">✕ Lihat laporan</span>
<span class="perm perm-green">✓ Kirim laporan</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- =====================================================================
PAGE: PETA (default)
===================================================================== -->
<div id="pagePeta" class="page active-page">
<!-- SIDEBAR -->
<div id="sidebar">
<div id="modeIndicator" class="mode-badge hidden">
<span class="pulse-dot"></span>
<span id="modeIndicatorText">Klik peta untuk menempatkan titik</span>
</div>
<!-- Viewer notice -->
<div id="viewerNotice" style="display:none;margin:10px 14px 0;padding:10px 12px;background:var(--purple-light);border:1px solid #c4b5fd;border-radius:8px;font-size:11px;color:#5b21b6;font-weight:500;text-align:center;">
👁 Mode Viewer — Anda hanya dapat melihat data
</div>
<!-- Action buttons -->
<div class="btn-group" id="actionBtnGroup">
<button id="btnAddCenter" onclick="startAddingCenter()">🕌 Tambah Rumah Ibadah</button>
<button id="btnAddHouse" onclick="startAddingHouse()">🏠 Tambah Rumah Miskin</button>
</div>
<!-- Stats -->
<div class="stat-panel">
<div class="stat-title">📊 Statistik Bantuan</div>
<div class="stat-grid">
<div class="stat-card">
<div class="stat-num" id="statTotal">0</div>
<div class="stat-label">Total Rumah</div>
</div>
<div class="stat-card red">
<div class="stat-num" id="statInside">0</div>
<div class="stat-label">Dalam Radius</div>
<div class="stat-icon">🔴</div>
</div>
<div class="stat-card yellow">
<div class="stat-num" id="statHelped">0</div>
<div class="stat-label">Sudah Dibantu</div>
<div class="stat-icon">🟡</div>
</div>
<div class="stat-card green">
<div class="stat-num" id="statOutside">0</div>
<div class="stat-label">Luar Radius</div>
<div class="stat-icon">🟢</div>
</div>
</div>
</div>
<!-- Rumah Ibadah -->
<div class="section">
<div class="section-title">
🕌 Rumah Ibadah
<span id="centerCount" class="badge badge-teal">0</span>
</div>
<div class="search-wrap">
<input type="text" id="searchCenter" class="search-input" placeholder="🔍 Cari nama rumah ibadah..." oninput="filterCenters()"/>
</div>
<select id="sortCenter" class="filter-select" onchange="filterCenters()">
<option value="">— Urutkan —</option>
<option value="kas_desc">💰 Kas terbanyak</option>
<option value="kas_asc">💰 Kas terkecil</option>
<option value="tanggungan_desc">👨‍👩‍👧 Tanggungan terbanyak</option>
<option value="tanggungan_asc">👨‍👩‍👧 Tanggungan terkecil</option>
</select>
<div id="centerList"><div class="empty-state">Belum ada rumah ibadah.</div></div>
</div>
<!-- Rumah Miskin -->
<div class="section">
<div class="section-title">
🏠 Data Rumah Miskin
<span id="houseCount" class="badge badge-orange">0</span>
</div>
<div class="search-wrap">
<input type="text" id="searchHouse" class="search-input" placeholder="🔍 Cari nama kepala keluarga..." oninput="filterHouses()"/>
</div>
<div class="filter-row">
<select id="filterStatus" class="filter-select" onchange="filterHouses()">
<option value="">Semua Status Bantuan</option>
<option value="yellow">✅ Sudah dibantu</option>
<option value="red">⏳ Belum dibantu</option>
<option value="green">🟢 Luar radius</option>
</select>
<select id="filterMiskin" class="filter-select" onchange="filterHouses()">
<option value="">Semua Kemiskinan</option>
<option value="sangat_miskin">😢 Sangat Miskin</option>
<option value="miskin">😟 Miskin</option>
<option value="tidak_miskin">😊 Tidak Miskin</option>
</select>
</div>
<div id="houseList"><div class="empty-state">Belum ada data rumah miskin.</div></div>
</div>
<!-- Legenda -->
<div class="section">
<div class="section-title">🗺 Legenda</div>
<div class="legend-list">
<div class="legend-item"><span style="font-size:13px">🕌</span> Masjid/Musholla</div>
<div class="legend-item"><span style="font-size:13px"></span> Gereja Katolik</div>
<div class="legend-item"><span style="font-size:13px">✝️</span> Gereja Protestan</div>
<div class="legend-item"><span style="font-size:13px">🛕</span> Vihara/Klenteng/Pura</div>
<div class="legend-item"><span class="legend-dot green"></span> Luar radius</div>
<div class="legend-item"><span class="legend-dot red"></span> Dalam radius</div>
<div class="legend-item"><span class="legend-dot yellow"></span> Sudah dibantu</div>
<div class="legend-item"><span class="legend-circle"></span> Area jangkauan</div>
</div>
</div>
<!-- Reset -->
<button id="btnReset" class="btn-danger" onclick="confirmReset()">🗑 Reset Semua Data</button>
</div>
<!-- MAP -->
<div id="map"></div>
<div id="radiusTooltip" class="radius-tooltip hidden"></div>
</div><!-- end #pagePeta -->
<!-- =====================================================================
PAGE: PELAPORAN
===================================================================== -->
<div id="pagePelaporan" class="page">
<div class="report-page">
<!-- KIRI: Form kirim laporan (semua role bisa) -->
<div class="report-col report-col-form">
<div class="report-col-header">
<div class="report-col-title">📢 Kirim Laporan</div>
<div class="report-col-sub">Laporkan kondisi kemiskinan yang belum terdata</div>
</div>
<div class="report-form-body">
<div class="form-group">
<label class="form-label">Deskripsi Laporan *</label>
<textarea id="reportText" class="form-input form-textarea" rows="5"
placeholder="Contoh: Ada keluarga miskin di Jl. Merdeka No.5 yang belum terdata, kondisi rumah sangat memprihatinkan dan membutuhkan bantuan segera..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Nama Pelapor (opsional)</label>
<input type="text" id="reportName" class="form-input" placeholder="Nama Anda..."/>
</div>
<div class="form-group">
<label class="form-label">Lokasi / Alamat (opsional)</label>
<input type="text" id="reportLocation" class="form-input" placeholder="Jl. Contoh No.1, Kelurahan..."/>
</div>
<div class="form-group">
<label class="form-label">Foto / Bukti (opsional)</label>
<div class="upload-area" id="uploadArea" onclick="document.getElementById('reportImg').click()">
<div class="upload-icon">📷</div>
<div class="upload-text">Klik untuk upload foto</div>
<div class="upload-sub">JPG, PNG, maksimal 5MB</div>
<input type="file" id="reportImg" accept="image/*" style="display:none" onchange="previewImage(event)"/>
</div>
<div id="imgPreview" style="display:none;margin-top:8px;">
<img id="previewImg" style="width:100%;border-radius:8px;max-height:200px;object-fit:cover;"/>
<button class="remove-img-btn" onclick="removeImage()">✕ Hapus foto</button>
</div>
</div>
<button class="form-submit orange-btn" onclick="submitReport()" style="margin-top:4px;padding:12px;font-size:13px;">
📤 Kirim Laporan
</button>
</div>
</div>
<!-- KANAN: Daftar laporan (hanya admin & surveyer) -->
<div class="report-col report-col-list" id="reportColList">
<div class="report-col-header">
<div class="report-col-title">
📋 Laporan Masuk
<span id="reportCount" class="badge badge-orange" style="margin-left:8px">0</span>
</div>
<div class="report-col-sub">Daftar laporan yang diterima dari masyarakat</div>
</div>
<!-- Filter & search laporan -->
<div class="report-list-toolbar">
<input type="text" id="searchReport" class="search-input" placeholder="🔍 Cari laporan..." oninput="filterReports()" style="flex:1"/>
<select id="filterReportStatus" class="filter-select" onchange="filterReports()" style="width:auto;margin:0">
<option value="">Semua</option>
<option value="baru">🆕 Baru</option>
<option value="ditangani">🔄 Ditangani</option>
<option value="selesai">✅ Selesai</option>
</select>
</div>
<div id="reportList" class="report-list-container">
<div class="empty-state" style="margin-top:20px">Belum ada laporan masuk.</div>
</div>
</div>
<!-- Viewer: panel info (tidak bisa lihat list) -->
<div class="report-col report-col-viewer" id="reportColViewer" style="display:none">
<div class="report-col-header">
<div class="report-col-title">👁 Mode Viewer</div>
<div class="report-col-sub">Daftar laporan hanya tersedia untuk Admin dan Surveyer</div>
</div>
<div style="padding:40px 20px;text-align:center">
<div style="font-size:48px;margin-bottom:16px">🔒</div>
<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:8px">Akses Terbatas</div>
<div style="font-size:12px;color:var(--text-muted);line-height:1.6">
Sebagai Viewer, Anda dapat mengirim laporan tapi tidak dapat melihat daftar laporan yang masuk.<br><br>
Hubungi Admin untuk mengubah role akses Anda.
</div>
</div>
</div>
</div>
</div><!-- end #pagePelaporan -->
<!-- =====================================================================
CUSTOM DATE PICKER
===================================================================== -->
<div id="datepickerOverlay" class="datepicker-overlay hidden" onclick="closeDatepickerIfOutside(event)">
<div class="datepicker-box" id="datepickerBox"></div>
</div>
<!-- =====================================================================
MODAL — FORM DATA RUMAH
===================================================================== -->
<div id="houseModal" class="modal-overlay hidden">
<div class="modal-box">
<div class="modal-header">
<div class="modal-title">🏠 Data Kemiskinan</div>
<button class="modal-close" onclick="closeHouseModal()"></button>
</div>
<div class="modal-body" id="houseModalBody"></div>
</div>
</div>
<!-- =====================================================================
MODAL — DETAIL RUMAH
===================================================================== -->
<div id="detailModal" class="modal-overlay hidden">
<div class="modal-box modal-wide">
<div class="modal-header">
<div class="modal-title">📋 Detail Data Rumah</div>
<button class="modal-close" onclick="closeDetailModal()"></button>
</div>
<div class="modal-body" id="detailModalBody"></div>
</div>
</div>
<!-- =====================================================================
MODAL — DETAIL LAPORAN (admin/surveyer)
===================================================================== -->
<div id="reportDetailModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:500px">
<div class="modal-header">
<div class="modal-title">📋 Detail Laporan</div>
<button class="modal-close" onclick="closeReportDetail()"></button>
</div>
<div class="modal-body" id="reportDetailBody"></div>
</div>
</div>
<!-- =====================================================================
MODAL — EDIT RUMAH IBADAH
===================================================================== -->
<div id="centerEditModal" class="modal-overlay hidden">
<div class="modal-box" style="max-width:460px">
<div class="modal-header" style="background:linear-gradient(135deg,#f0fdf4,#e0f2fe)">
<div class="modal-title">🕌 Edit Rumah Ibadah</div>
<button class="modal-close" onclick="closeCenterEditModal()"></button>
</div>
<div class="modal-body" id="centerEditModalBody">
<input type="hidden" id="ce_id"/>
<div class="form-group">
<label class="form-label">Jenis Rumah Ibadah</label>
<select class="form-input" id="ce_type" onchange="updateCenterEditHeader()">
<option value="masjid">🕌 Masjid / Musholla</option>
<option value="gereja katedral">⛪ Gereja Katolik / Katedral</option>
<option value="gereja protestan">✝️ Gereja Protestan / Kapel</option>
<option value="vihara">🛕 Vihara / Klenteng / Pura</option>
<option value="sinagog">✡️ Sinagog</option>
<option value="default">🏛️ Lainnya</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Nama Rumah Ibadah *</label>
<input class="form-input" id="ce_name" type="text" placeholder="Nama rumah ibadah..."/>
</div>
<div class="form-group">
<label class="form-label">Alamat</label>
<input class="form-input" id="ce_address" type="text" placeholder="Alamat..."/>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label class="form-label">Kas / Dana (Rp)</label>
<input class="form-input" id="ce_kas" type="number" min="0" step="1000" placeholder="0"/>
</div>
<div class="form-group" style="flex:1">
<label class="form-label">Radius (meter)</label>
<input class="form-input" id="ce_radius" type="number" min="50" step="10" placeholder="300"/>
</div>
</div>
</div>
<div class="modal-actions">
<button class="btn-modal-cancel" onclick="closeCenterEditModal()">Batal</button>
<button class="btn-modal-save" onclick="saveCenterEdit()" style="background:var(--primary)">💾 Simpan Perubahan</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="script.js"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* koneksi.php — Koneksi Database MySQL
* WebGIS BantSOSial Pontianak
*
* Ubah kredensial sesuai konfigurasi server lokal Anda.
*/
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // Ganti sesuai MySQL user Anda
define('DB_PASS', ''); // Ganti sesuai password MySQL Anda
define('DB_NAME', 'webgis_bansos');
// Buat koneksi
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
// Cek error koneksi
if ($conn->connect_error) {
http_response_code(500);
die(json_encode([
'success' => false,
'message' => 'Koneksi database gagal: ' . $conn->connect_error
]));
}
// Set charset UTF-8
$conn->set_charset('utf8mb4');
// Header JSON untuk semua response
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
+16
View File
@@ -0,0 +1,16 @@
<?php
/**
* reset.php — Reset Semua Data
* Menghapus semua rumah ibadah, rumah miskin, dan log
*/
require_once 'koneksi.php';
$conn->query("DELETE FROM aid_logs");
$conn->query("DELETE FROM houses");
$conn->query("DELETE FROM religious_centers");
$conn->query("ALTER TABLE aid_logs AUTO_INCREMENT = 1");
$conn->query("ALTER TABLE houses AUTO_INCREMENT = 1");
$conn->query("ALTER TABLE religious_centers AUTO_INCREMENT = 1");
echo json_encode(['success' => true, 'message' => 'Semua data berhasil direset']);
$conn->close();
+1636
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
<?php
/**
* simpan_laporan.php — Simpan Laporan Masyarakat
* POST: name, text, img (base64, opsional)
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success'=>false,'message'=>'Hanya POST']); exit;
}
$name = $conn->real_escape_string(strip_tags($_POST['name'] ?? 'Anonim'));
$text = $conn->real_escape_string(strip_tags($_POST['text'] ?? ''));
$img = $_POST['img'] ?? '';
if (empty($text)) {
echo json_encode(['success'=>false,'message'=>'Deskripsi kosong']); exit;
}
// Validasi ukuran base64 img (max ~5MB raw → ~6.7MB base64)
if (strlen($img) > 7_000_000) {
echo json_encode(['success'=>false,'message'=>'Ukuran foto terlalu besar (max 5MB)']); exit;
}
$imgEsc = $conn->real_escape_string($img);
$sql = "INSERT INTO laporan (pelapor, deskripsi, foto_base64) VALUES ('$name', '$text', '$imgEsc')";
if ($conn->query($sql)) {
echo json_encode(['success'=>true,'id'=>$conn->insert_id]);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
$conn->close();
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* simpan_pusat.php — Simpan / Update Rumah Ibadah
*
* POST params:
* id - ID (jika update, opsional)
* name - Nama rumah ibadah
* address - Alamat
* kas - Jumlah kas/dana (Rp)
* lat - Latitude
* lng - Longitude
* radius - Radius dalam meter
* update - "1" jika update existing record
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']);
exit;
}
$id = intval($_POST['id'] ?? 0);
$name = $conn->real_escape_string(strip_tags($_POST['name'] ?? ''));
$address = $conn->real_escape_string(strip_tags($_POST['address'] ?? ''));
$kas = floatval($_POST['kas'] ?? 0);
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$radius = intval($_POST['radius'] ?? 300);
$isUpdate = isset($_POST['update']) && $_POST['update'] === '1';
if (empty($name)) {
echo json_encode(['success' => false, 'message' => 'Nama wajib diisi']);
exit;
}
if ($radius < 50) $radius = 50;
if ($id > 0 || $isUpdate) {
$sql = "UPDATE religious_centers
SET name='$name', address='$address', kas=$kas, latitude=$lat, longitude=$lng, radius=$radius, updated_at=NOW()
WHERE id=$id";
if ($conn->query($sql)) {
echo json_encode(['success' => true, 'id' => $id, 'action' => 'updated']);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
} else {
$sql = "INSERT INTO religious_centers (name, address, kas, latitude, longitude, radius)
VALUES ('$name', '$address', $kas, $lat, $lng, $radius)";
if ($conn->query($sql)) {
$newId = $conn->insert_id;
echo json_encode(['success' => true, 'id' => $newId, 'action' => 'inserted']);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
}
$conn->close();
+79
View File
@@ -0,0 +1,79 @@
<?php
/**
* simpan_rumah.php v3 — Simpan / Update Data Rumah Miskin
*
* POST params:
* id - ID rumah (opsional, jika update)
* lat, lng - Koordinat
* address - Alamat lengkap
* rt, rw - RT dan RW
* kelurahan - Nama kelurahan
* status_miskin - sangat_miskin / miskin / tidak_miskin
* jumlah_anggota - Jumlah anggota keluarga
* anggota - JSON array data tiap anggota
* aid_status - helped / not_helped / outside
* has_data - 0 atau 1
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya POST']);
exit;
}
$id = intval($_POST['id'] ?? 0);
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$address = $conn->real_escape_string(strip_tags($_POST['address'] ?? ''));
$rt = $conn->real_escape_string(strip_tags($_POST['rt'] ?? ''));
$rw = $conn->real_escape_string(strip_tags($_POST['rw'] ?? ''));
$kelurahan = $conn->real_escape_string(strip_tags($_POST['kelurahan'] ?? ''));
$statusMiskin = $conn->real_escape_string($_POST['status_miskin'] ?? '');
$jumlahAnggota = intval($_POST['jumlah_anggota'] ?? 0);
$anggotaRaw = $_POST['anggota'] ?? '[]';
$aidStatus = $conn->real_escape_string($_POST['aid_status'] ?? 'outside');
$hasData = intval($_POST['has_data'] ?? 0);
// Validasi anggota JSON
$anggotaDecoded = json_decode($anggotaRaw, true);
if (!is_array($anggotaDecoded)) $anggotaDecoded = [];
$anggotaJson = $conn->real_escape_string(json_encode($anggotaDecoded, JSON_UNESCAPED_UNICODE));
$validStatuses = ['sangat_miskin','miskin','tidak_miskin',''];
if (!in_array($statusMiskin, $validStatuses)) $statusMiskin = '';
$validAid = ['helped','not_helped','outside'];
if (!in_array($aidStatus, $validAid)) $aidStatus = 'outside';
if ($id > 0) {
// UPDATE
$sql = "UPDATE houses SET
latitude=$lat, longitude=$lng,
address='$address', rt='$rt', rw='$rw', kelurahan='$kelurahan',
status_miskin='$statusMiskin', jumlah_anggota=$jumlahAnggota,
anggota='$anggotaJson', aid_status='$aidStatus', has_data=$hasData,
updated_at=NOW()
WHERE id=$id";
if ($conn->query($sql)) {
echo json_encode(['success'=>true,'id'=>$id,'action'=>'updated']);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
} else {
// INSERT
$sql = "INSERT INTO houses
(latitude, longitude, address, rt, rw, kelurahan,
status_miskin, jumlah_anggota, anggota, aid_status, has_data)
VALUES
($lat, $lng, '$address', '$rt', '$rw', '$kelurahan',
'$statusMiskin', $jumlahAnggota, '$anggotaJson', '$aidStatus', $hasData)";
if ($conn->query($sql)) {
$newId = $conn->insert_id;
echo json_encode(['success'=>true,'id'=>$newId,'action'=>'inserted']);
} else {
echo json_encode(['success'=>false,'message'=>$conn->error]);
}
}
$conn->close();
+557
View File
@@ -0,0 +1,557 @@
/* ============================================
WebGIS BantSOSial — style.css v5
Navbar + halaman Peta & Pelaporan terpisah
============================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--nav-h: 56px;
--sidebar-w: 320px;
--bg: #f0f4f8;
--sidebar-bg: #ffffff;
--border: #e2e8f0;
--text: #1e293b;
--text-muted: #64748b;
--primary: #0f766e;
--primary-light: #ccfbf1;
--primary-hover: #0d6563;
--orange: #ea580c;
--orange-light: #ffedd5;
--orange-hover: #c2410c;
--danger: #dc2626;
--danger-light: #fee2e2;
--warning: #d97706;
--warning-light: #fef3c7;
--success: #16a34a;
--success-light: #dcfce7;
--purple: #7c3aed;
--purple-light: #ede9fe;
--radius-sm: 8px;
--radius-md: 12px;
--shadow: 0 4px 24px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 40px rgba(0,0,0,0.14);
--font: 'DM Sans', sans-serif;
--mono: 'DM Mono', monospace;
}
html, body { height: 100%; font-family: var(--font); background: var(--bg); overflow: hidden; color: var(--text); }
/* ============================================
TOP NAVBAR
============================================ */
#topNav {
position: fixed; top: 0; left: 0; right: 0;
height: var(--nav-h); z-index: 2000;
background: white; border-bottom: 1px solid var(--border);
box-shadow: 0 2px 12px rgba(0,0,0,0.07);
display: flex; align-items: center;
padding: 0 20px; gap: 16px;
}
/* Brand */
.nav-brand {
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
width: var(--sidebar-w);
}
.nav-brand-icon { font-size: 22px; }
.nav-brand-title { font-size: 13px; font-weight: 700; letter-spacing: -.3px; line-height: 1.2; }
.nav-brand-sub { font-size: 9px; color: var(--text-muted); font-family: var(--mono); }
/* Center menu */
.nav-menu {
display: flex; align-items: center; gap: 4px;
flex: 1; justify-content: center;
}
.nav-item {
display: flex; align-items: center; gap: 6px;
padding: 7px 18px; border: none; background: none;
border-radius: 20px; font-family: var(--font);
font-size: 13px; font-weight: 600; color: var(--text-muted);
cursor: pointer; transition: all .2s; position: relative;
}
.nav-item:hover { background: var(--bg); color: var(--text); }
.nav-item.active {
background: var(--primary-light); color: var(--primary);
}
.nav-item-icon { font-size: 15px; }
.nav-badge {
position: absolute; top: 2px; right: 8px;
background: var(--orange); color: white;
font-size: 9px; font-weight: 700; font-family: var(--mono);
min-width: 16px; height: 16px; padding: 0 4px;
border-radius: 8px; display: flex; align-items: center; justify-content: center;
}
.nav-badge.hidden { display: none; }
/* Right: role */
.nav-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.role-pill {
display: flex; align-items: center; gap: 6px;
background: var(--bg); border: 1.5px solid var(--border);
border-radius: 20px; padding: 5px 12px;
font-size: 12px; font-weight: 600;
}
#roleSwitchBtn {
padding: 6px 14px; background: var(--primary); color: white;
border: none; border-radius: 20px; font-family: var(--font);
font-size: 12px; font-weight: 600; cursor: pointer; transition: all .2s;
box-shadow: 0 2px 8px rgba(15,118,110,.3);
}
#roleSwitchBtn:hover { background: var(--primary-hover); }
/* ============================================
PAGE SYSTEM
============================================ */
.page {
position: fixed;
top: var(--nav-h); left: 0; right: 0;
bottom: 0;
display: none;
opacity: 0;
transition: opacity .25s ease;
}
.page.active-page {
display: flex;
opacity: 1;
}
/* ============================================
PAGE: PETA — sidebar + map layout
============================================ */
#pagePeta { display: none; }
#pagePeta.active-page { display: block; }
#sidebar {
position: fixed;
left: 0; top: var(--nav-h);
width: var(--sidebar-w);
height: calc(100% - var(--nav-h));
background: white; z-index: 1000;
overflow-y: auto; overflow-x: hidden;
box-shadow: var(--shadow-lg);
display: flex; flex-direction: column;
border-right: 1px solid var(--border);
}
#sidebar::-webkit-scrollbar { width: 4px; }
#sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
#map {
position: fixed;
left: var(--sidebar-w); top: var(--nav-h);
width: calc(100% - var(--sidebar-w));
height: calc(100% - var(--nav-h));
z-index: 1;
}
#map.adding-mode { cursor: crosshair !important; }
#map.adding-mode .leaflet-interactive { cursor: crosshair !important; }
.radius-tooltip {
position: fixed; bottom: 24px; right: 24px;
background: rgba(15,23,42,.88); color: white;
padding: 10px 16px; border-radius: var(--radius-md);
font-family: var(--mono); font-size: 14px; font-weight: 500;
z-index: 2000; backdrop-filter: blur(8px);
box-shadow: var(--shadow-lg); pointer-events: none; transition: opacity .2s;
}
.radius-tooltip.hidden { opacity: 0; pointer-events: none; }
.radius-handle { cursor: ew-resize; }
/* ============================================
PAGE: PELAPORAN — dua kolom
============================================ */
#pagePelaporan { display: none; background: var(--bg); }
#pagePelaporan.active-page { display: block; overflow-y: auto; }
.report-page {
display: grid;
grid-template-columns: 420px 1fr;
gap: 0;
min-height: calc(100vh - var(--nav-h));
}
.report-col {
display: flex; flex-direction: column;
height: calc(100vh - var(--nav-h));
overflow: hidden;
}
.report-col-form {
background: white;
border-right: 1px solid var(--border);
overflow-y: auto;
}
.report-col-list {
background: var(--bg);
overflow: hidden;
display: flex; flex-direction: column;
}
.report-col-viewer {
background: var(--bg);
}
.report-col-header {
padding: 20px 24px 16px;
border-bottom: 1px solid var(--border);
background: white; flex-shrink: 0;
}
.report-col-title {
font-size: 16px; font-weight: 700;
display: flex; align-items: center; gap: 6px;
}
.report-col-sub {
font-size: 11px; color: var(--text-muted); margin-top: 3px;
}
.report-form-body { padding: 20px 24px; }
/* List toolbar */
.report-list-toolbar {
padding: 12px 20px;
background: white; border-bottom: 1px solid var(--border);
display: flex; gap: 8px; align-items: center; flex-shrink: 0;
}
.report-list-container {
flex: 1; overflow-y: auto; padding: 16px 20px;
}
.report-list-container::-webkit-scrollbar { width: 4px; }
.report-list-container::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
/* Report card */
.report-card {
background: white; border: 1px solid var(--border);
border-radius: var(--radius-md); padding: 14px 16px;
margin-bottom: 10px; cursor: pointer;
transition: all .2s; animation: fadeIn .3s;
position: relative;
}
.report-card:hover { box-shadow: var(--shadow); transform: translateY(-1px); border-color: var(--primary); }
.report-card-top {
display: flex; align-items: flex-start; justify-content: space-between;
margin-bottom: 6px; gap: 8px;
}
.report-card-name { font-size: 13px; font-weight: 700; }
.report-card-time { font-size: 10px; color: var(--text-muted); font-family: var(--mono); flex-shrink: 0; }
.report-card-text {
font-size: 12px; color: var(--text-muted); line-height: 1.5;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.report-card-location { font-size: 10px; color: var(--text-muted); margin-top: 5px; }
.report-card-footer {
display: flex; align-items: center; justify-content: space-between;
margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border);
}
.report-status-badge {
font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 12px;
}
.report-status-badge.baru { background: #eff6ff; color: #1d4ed8; }
.report-status-badge.ditangani { background: var(--warning-light); color: var(--warning); }
.report-status-badge.selesai { background: var(--success-light); color: var(--success); }
.report-card-img { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
.report-card-has-img { border-left: 3px solid var(--orange); }
/* Report detail modal */
.report-detail-img { width: 100%; border-radius: var(--radius-sm); margin: 10px 0; max-height: 260px; object-fit: cover; }
.report-detail-row { display: flex; justify-content: space-between; font-size: 12px; padding: 6px 0; border-bottom: 1px solid var(--border); }
.report-detail-row:last-of-type { border: none; }
.report-detail-label { color: var(--text-muted); }
.report-detail-val { font-weight: 600; text-align: right; max-width: 60%; }
/* Status change buttons */
.status-change-btns { display: flex; gap: 6px; margin-top: 12px; }
.btn-status {
flex: 1; padding: 8px; border: none; border-radius: var(--radius-sm);
font-family: var(--font); font-size: 11px; font-weight: 700; cursor: pointer; transition: all .2s;
}
.btn-status.baru { background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; }
.btn-status.ditangani { background: var(--warning-light); color: var(--warning); border: 1px solid #fcd34d; }
.btn-status.selesai { background: var(--success-light); color: var(--success); border: 1px solid #86efac; }
.btn-status.active { opacity: 1; transform: scale(1.02); box-shadow: var(--shadow); }
/* ============================================
SIDEBAR INNER STYLES
============================================ */
.mode-badge {
margin: 10px 14px 0; padding: 8px 12px;
background: #fef9c3; border: 1px solid #fde047;
border-radius: var(--radius-sm); font-size: 11px;
font-weight: 500; color: #854d0e;
display: flex; align-items: center; gap: 8px;
}
.mode-badge.hidden { display: none; }
.mode-badge.house-mode { background: #fff7ed; border-color: #fdba74; color: #9a3412; }
.pulse-dot { width: 8px; height: 8px; background: #facc15; border-radius: 50%; flex-shrink: 0; animation: pulse 1.2s infinite; }
.mode-badge.house-mode .pulse-dot { background: #fb923c; }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.5;transform:scale(1.3)} }
.btn-group { padding: 12px 14px 0; display: flex; flex-direction: column; gap: 7px; }
#btnAddCenter {
padding: 10px 14px; background: var(--primary); color: white;
border: none; border-radius: var(--radius-md); font-family: var(--font);
font-size: 13px; font-weight: 600; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 7px;
transition: all .2s; box-shadow: 0 2px 8px rgba(15,118,110,.25);
}
#btnAddCenter:hover { background: var(--primary-hover); transform: translateY(-1px); }
#btnAddCenter.active { background: #0369a1; }
#btnAddHouse {
padding: 10px 14px; background: var(--orange); color: white;
border: none; border-radius: var(--radius-md); font-family: var(--font);
font-size: 13px; font-weight: 600; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 7px;
transition: all .2s; box-shadow: 0 2px 8px rgba(234,88,12,.25);
}
#btnAddHouse:hover { background: var(--orange-hover); transform: translateY(-1px); }
#btnAddHouse.active { background: #9a3412; }
.btn-danger {
margin: 6px 14px 20px; padding: 9px 14px; background: white;
color: var(--danger); border: 1.5px solid var(--danger);
border-radius: var(--radius-md); font-family: var(--font);
font-size: 12px; font-weight: 600; cursor: pointer;
width: calc(100% - 28px); transition: all .2s;
}
.btn-danger:hover { background: var(--danger-light); }
.stat-panel { margin: 12px 14px 0; background: var(--bg); border-radius: var(--radius-md); padding: 12px; }
.stat-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; color: var(--text-muted); margin-bottom: 8px; }
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.stat-card { background: white; border-radius: var(--radius-sm); padding: 8px 10px; border: 1px solid var(--border); position: relative; overflow: hidden; transition: all .3s; }
.stat-card:hover { transform: translateY(-1px); box-shadow: var(--shadow); }
.stat-card.red { border-color: #fca5a5; background: #fff5f5; }
.stat-card.yellow { border-color: #fcd34d; background: #fffbeb; }
.stat-card.green { border-color: #86efac; background: #f0fdf4; }
.stat-num { font-size: 20px; font-weight: 700; font-family: var(--mono); line-height: 1; transition: all .4s; }
.stat-card.red .stat-num { color: var(--danger); }
.stat-card.yellow .stat-num { color: var(--warning); }
.stat-card.green .stat-num { color: var(--success); }
.stat-label { font-size: 9px; color: var(--text-muted); margin-top: 2px; font-weight: 500; }
.stat-icon { position: absolute; right: 6px; top: 6px; font-size: 13px; opacity: .3; }
.section { padding: 12px 14px 0; }
.section-title { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; color: var(--text-muted); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.badge { font-size: 10px; padding: 1px 6px; border-radius: 20px; font-family: var(--mono); margin-left: auto; color: white; }
.badge-teal { background: var(--primary); }
.badge-orange { background: var(--orange); }
.search-wrap { margin-bottom: 6px; }
.search-input { width: 100%; padding: 7px 10px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; outline: none; transition: border-color .2s; background: white; }
.search-input:focus { border-color: var(--primary); }
.filter-row { display: flex; gap: 6px; margin-bottom: 8px; }
.filter-select { flex: 1; padding: 6px 8px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; outline: none; background: white; color: var(--text); cursor: pointer; margin-bottom: 6px; width: 100%; }
.filter-select:focus { border-color: var(--primary); }
#centerList, #houseList { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.empty-state { text-align: center; padding: 16px 10px; font-size: 11px; color: var(--text-muted); line-height: 1.6; background: var(--bg); border-radius: var(--radius-sm); border: 1.5px dashed var(--border); }
.center-item { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 30px 9px 10px; cursor: pointer; transition: all .2s; position: relative; animation: fadeIn .3s ease; }
.center-item:hover { background: var(--primary-light); border-color: var(--primary); transform: translateX(3px); }
.center-item-name { font-size: 12px; font-weight: 600; }
.center-item-addr { font-size: 10px; color: var(--text-muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.center-item-kas { font-size: 10px; color: var(--primary); font-family: var(--mono); font-weight: 600; margin-top: 2px; }
.center-item-radius { font-size: 10px; color: #3b82f6; font-family: var(--mono); margin-top: 1px; }
.house-item { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 9px 30px 9px 10px; cursor: pointer; transition: all .2s; position: relative; animation: fadeIn .3s ease; }
.house-item:hover { background: var(--orange-light); border-color: var(--orange); transform: translateX(3px); }
.house-item.status-red { border-left: 3px solid var(--danger); }
.house-item.status-yellow { border-left: 3px solid var(--warning); }
.house-item.status-green { border-left: 3px solid var(--success); }
.house-item-name { font-size: 12px; font-weight: 600; }
.house-item-sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
.house-item-status { font-size: 10px; font-weight: 600; margin-top: 2px; }
.house-item-status.green { color: var(--success); }
.house-item-status.red { color: var(--danger); }
.house-item-status.yellow { color: var(--warning); }
.item-delete { position: absolute; right: 7px; top: 7px; background: none; border: none; cursor: pointer; font-size: 12px; opacity: .35; transition: opacity .2s; padding: 2px 4px; border-radius: 4px; }
.item-delete:hover { opacity: 1; background: var(--danger-light); }
.legend-list { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-muted); }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.legend-dot.green { background: #16a34a; }
.legend-dot.red { background: #dc2626; }
.legend-dot.yellow { background: #d97706; }
.legend-circle { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #3b82f6; background: rgba(59,130,246,.1); flex-shrink: 0; }
/* upload & report form */
.form-textarea { width: 100%; padding: 8px 10px; resize: vertical; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; outline: none; transition: border-color .2s; background: white; color: var(--text); }
.form-textarea:focus { border-color: var(--orange); }
.upload-area { border: 2px dashed var(--border); border-radius: var(--radius-sm); padding: 20px; text-align: center; cursor: pointer; transition: all .2s; background: var(--bg); }
.upload-area:hover { border-color: var(--orange); background: var(--orange-light); }
.upload-icon { font-size: 28px; margin-bottom: 6px; }
.upload-text { font-size: 12px; font-weight: 600; color: var(--text-muted); }
.upload-sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; }
.remove-img-btn { display: block; width: 100%; margin-top: 6px; padding: 5px; background: var(--danger-light); color: var(--danger); border: 1px solid #fca5a5; border-radius: 6px; font-size: 11px; cursor: pointer; font-family: var(--font); font-weight: 600; }
/* ============================================
LEAFLET POPUP
============================================ */
.leaflet-popup-content-wrapper { border-radius: var(--radius-md) !important; box-shadow: var(--shadow-lg) !important; border: 1px solid var(--border); font-family: var(--font) !important; overflow: hidden; padding: 0 !important; }
.leaflet-popup-content { margin: 0 !important; width: 300px !important; }
.leaflet-popup-tip { background: white !important; }
.map-popup { padding: 13px 15px; }
.map-popup-title { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
.map-popup-addr { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.4; }
.map-popup-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; padding: 2px 8px; border-radius: 20px; margin-bottom: 8px; }
.map-popup-badge.green { background: var(--success-light); color: var(--success); }
.map-popup-badge.red { background: var(--danger-light); color: var(--danger); }
.map-popup-badge.yellow { background: var(--warning-light); color: var(--warning); }
.map-popup-badge.nodata { background: #f1f5f9; color: var(--text-muted); }
.map-popup-actions { padding: 9px 15px; background: var(--bg); display: flex; gap: 7px; }
.popup-btn { flex: 1; padding: 7px 8px; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; font-weight: 600; cursor: pointer; transition: all .2s; }
.popup-btn.primary { background: var(--primary); color: white; }
.popup-btn.primary:hover { background: var(--primary-hover); }
.popup-btn.orange { background: var(--orange); color: white; }
.popup-btn.orange:hover { background: var(--orange-hover); }
.center-popup { padding: 13px 15px; }
.center-popup-header { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.center-popup-icon { width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; }
.center-popup-title { font-size: 13px; font-weight: 700; line-height: 1.2; }
.center-popup-addr { font-size: 10px; color: var(--text-muted); margin-bottom: 8px; line-height: 1.4; }
.kas-badge { display: flex; align-items: center; gap: 6px; background: #f0fdf4; border: 1px solid #86efac; border-radius: var(--radius-sm); padding: 6px 10px; margin-bottom: 7px; }
.kas-label { font-size: 10px; color: var(--success); font-weight: 600; }
.kas-value { font-size: 13px; font-weight: 700; color: var(--success); font-family: var(--mono); margin-left: auto; }
.radius-info-box { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: var(--radius-sm); padding: 7px 10px; font-size: 11px; color: #1d4ed8; display: flex; align-items: center; gap: 6px; margin-bottom: 5px; }
.radius-val { font-family: var(--mono); font-weight: 700; font-size: 13px; margin-left: auto; }
.radius-drag-hint { font-size: 10px; color: var(--text-muted); text-align: center; padding: 2px 0 4px; }
/* ============================================
FORMS (shared)
============================================ */
.form-group { margin-bottom: 12px; }
.form-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--text-muted); margin-bottom: 4px; display: block; }
.form-input { width: 100%; padding: 8px 10px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; color: var(--text); outline: none; transition: border-color .2s; background: white; }
.form-input:focus { border-color: var(--primary); }
.form-row { display: flex; gap: 7px; }
.form-row .form-group { flex: 1; }
.form-divider { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--text-muted); border-bottom: 1px solid var(--border); padding-bottom: 4px; margin: 10px 0 8px; }
.form-submit { width: 100%; padding: 9px; background: var(--primary); color: white; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; font-weight: 700; cursor: pointer; margin-top: 10px; transition: all .2s; }
.form-submit:hover { background: var(--primary-hover); }
.form-submit.orange-btn { background: var(--orange); }
.form-submit.orange-btn:hover { background: var(--orange-hover); }
.form-cancel { width: 100%; padding: 7px; background: none; color: var(--text-muted); border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; cursor: pointer; margin-top: 5px; transition: all .2s; }
.form-cancel:hover { background: var(--bg); }
.loading-addr { font-size: 10px; color: var(--primary); padding: 4px 0; animation: blink 1s infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.4} }
.form-popup { padding: 14px 15px; }
.form-popup h3 { font-size: 13px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 6px; }
/* ============================================
CUSTOM DATE PICKER
============================================ */
.datepicker-overlay { position: fixed; inset: 0; z-index: 5000; display: flex; align-items: center; justify-content: center; background: rgba(15,23,42,.4); backdrop-filter: blur(3px); }
.datepicker-overlay.hidden { display: none; }
.datepicker-box { background: white; border-radius: 16px; box-shadow: 0 24px 80px rgba(0,0,0,.2); width: 320px; overflow: hidden; animation: slideUp .2s ease; }
.dp-header { background: linear-gradient(135deg, var(--primary), #0d9488); color: white; padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; }
.dp-header-label { font-size: 13px; font-weight: 600; opacity: .8; }
.dp-header-value { font-size: 22px; font-weight: 700; font-family: var(--mono); }
.dp-tabs { display: flex; border-bottom: 1px solid var(--border); }
.dp-tab { flex: 1; padding: 10px; text-align: center; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .5px; cursor: pointer; color: var(--text-muted); border-bottom: 2px solid transparent; transition: all .2s; }
.dp-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
.dp-body { padding: 14px; min-height: 180px; }
.dp-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.dp-grid-7 { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.dp-cell { padding: 8px 4px; text-align: center; font-size: 12px; font-weight: 500; border-radius: var(--radius-sm); cursor: pointer; border: 1.5px solid transparent; transition: all .15s; color: var(--text); }
.dp-cell:hover { background: var(--primary-light); border-color: var(--primary); color: var(--primary); }
.dp-cell.selected { background: var(--primary); color: white; border-color: var(--primary); }
.dp-cell.today { border-color: var(--primary); color: var(--primary); font-weight: 700; }
.dp-year-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.dp-nav-btn { padding: 5px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; font-size: 12px; cursor: pointer; font-weight: 700; transition: all .2s; }
.dp-nav-btn:hover { background: var(--primary-light); border-color: var(--primary); }
.dp-year-range { font-size: 11px; color: var(--text-muted); font-family: var(--mono); }
.dp-weekday { text-align: center; font-size: 9px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; padding: 4px 0; }
.dp-actions { padding: 10px 14px; border-top: 1px solid var(--border); display: flex; gap: 8px; background: var(--bg); }
.dp-btn-ok { flex: 2; padding: 8px; background: var(--primary); color: white; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; font-weight: 700; cursor: pointer; transition: background .2s; }
.dp-btn-ok:hover { background: var(--primary-hover); }
.dp-btn-cancel { flex: 1; padding: 8px; background: white; color: var(--text-muted); border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; cursor: pointer; }
.date-trigger { width: 100%; padding: 7px 9px; border: 1.5px solid var(--border); border-radius: var(--radius-sm); font-family: var(--font); font-size: 12px; color: var(--text); outline: none; cursor: pointer; background: white; text-align: left; transition: border-color .2s; }
.date-trigger:hover, .date-trigger:focus { border-color: var(--primary); }
.date-trigger-placeholder { color: var(--text-muted); }
/* ============================================
MODAL (shared)
============================================ */
.modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,.55); z-index: 3000; display: flex; align-items: center; justify-content: center; padding: 16px; backdrop-filter: blur(3px); }
.modal-overlay.hidden { display: none; }
.modal-box { background: white; border-radius: 16px; width: 100%; max-width: 540px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 24px 80px rgba(0,0,0,.2); animation: slideUp .25s ease; }
.modal-wide { max-width: 640px; }
@keyframes slideUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--border); background: linear-gradient(135deg,#f0fdf4,#e0f2fe); flex-shrink: 0; }
.modal-title { font-size: 15px; font-weight: 700; }
.modal-close { background: none; border: none; font-size: 16px; cursor: pointer; color: var(--text-muted); padding: 4px 8px; border-radius: 6px; transition: all .2s; }
.modal-close:hover { background: var(--bg); }
.modal-body { overflow-y: auto; padding: 20px; flex: 1; }
.modal-body::-webkit-scrollbar { width: 4px; }
.modal-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.modal-section { margin-bottom: 16px; }
.modal-section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: var(--text-muted); padding-bottom: 6px; border-bottom: 1px solid var(--border); margin-bottom: 10px; }
.info-box { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; margin-bottom: 10px; }
.info-box-row { display: flex; justify-content: space-between; font-size: 11px; padding: 3px 0; }
.info-box-label { color: var(--text-muted); }
.info-box-val { font-weight: 600; color: var(--text); font-family: var(--mono); font-size: 10px; text-align: right; max-width: 60%; }
.member-card { border: 1.5px solid var(--border); border-radius: var(--radius-md); padding: 12px; margin-bottom: 10px; background: #fafafa; }
.member-card-header { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 10px; }
.member-num { width: 22px; height: 22px; background: var(--primary); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
.salary-row { display: none; }
.salary-row.visible { display: flex; }
.status-pills { display: flex; gap: 7px; margin-bottom: 2px; }
.status-pill { flex: 1; padding: 8px 6px; border: 2px solid var(--border); border-radius: var(--radius-sm); text-align: center; cursor: pointer; font-size: 11px; font-weight: 600; transition: all .2s; background: white; }
.status-pill:hover { border-color: var(--orange); }
.status-pill.selected.sangat_miskin { border-color: var(--danger); background: var(--danger-light); color: var(--danger); }
.status-pill.selected.miskin { border-color: var(--warning); background: var(--warning-light); color: var(--warning); }
.status-pill.selected.tidak_miskin { border-color: var(--success); background: var(--success-light); color: var(--success); }
.modal-actions { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; background: var(--bg); flex-shrink: 0; }
.btn-modal-save { flex: 2; padding: 10px; background: var(--orange); color: white; border: none; border-radius: var(--radius-md); font-family: var(--font); font-size: 13px; font-weight: 700; cursor: pointer; transition: all .2s; }
.btn-modal-save:hover { background: var(--orange-hover); }
.btn-modal-cancel { flex: 1; padding: 10px; background: white; color: var(--text-muted); border: 1.5px solid var(--border); border-radius: var(--radius-md); font-family: var(--font); font-size: 12px; cursor: pointer; }
.btn-modal-cancel:hover { background: var(--bg); }
/* detail modal */
.detail-header-band { background: linear-gradient(135deg,var(--orange-light),#fff7ed); border-radius: var(--radius-sm); padding: 12px 14px; margin-bottom: 14px; }
.detail-addr { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
.detail-center-row { background: var(--primary-light); border: 1px solid #99f6e4; border-radius: var(--radius-sm); padding: 8px 12px; margin-bottom: 12px; font-size: 11px; display: flex; align-items: center; gap: 8px; }
.detail-miskin-badge { display: inline-flex; align-items: center; gap: 5px; padding: 4px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; margin-bottom: 12px; }
.detail-miskin-badge.sangat_miskin { background: var(--danger-light); color: var(--danger); }
.detail-miskin-badge.miskin { background: var(--warning-light); color: var(--warning); }
.detail-miskin-badge.tidak_miskin { background: var(--success-light); color: var(--success); }
.members-grid { display: flex; flex-direction: column; gap: 8px; }
.member-detail-card { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px; background: var(--bg); }
.member-detail-name { font-size: 13px; font-weight: 700; margin-bottom: 4px; }
.member-detail-row { display: flex; justify-content: space-between; font-size: 11px; padding: 2px 0; color: var(--text-muted); }
.member-detail-row strong { color: var(--text); font-family: var(--mono); font-size: 10px; }
.bantuan-section { margin-top: 14px; padding: 12px 14px; background: var(--bg); border-radius: var(--radius-sm); border: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.bantuan-label { font-size: 12px; font-weight: 600; flex: 1; }
.btn-bantuan { padding: 7px 14px; border: none; border-radius: var(--radius-sm); font-family: var(--font); font-size: 11px; font-weight: 700; cursor: pointer; transition: all .2s; }
.btn-bantuan.help { background: var(--success); color: white; }
.btn-bantuan.unhelp { background: var(--danger-light); color: var(--danger); border: 1px solid #fca5a5; }
/* Role modal */
.role-cards { display: flex; flex-direction: column; gap: 10px; }
.role-card { border: 2px solid var(--border); border-radius: var(--radius-md); padding: 14px 16px; cursor: pointer; transition: all .2s; background: var(--bg); }
.role-card:hover { border-color: var(--primary); background: var(--primary-light); transform: translateX(3px); }
.role-card.active-role { border-color: var(--primary); background: var(--primary-light); }
.role-card.active-role[id="roleCard_surveyer"] { border-color: var(--orange); background: var(--orange-light); }
.role-card.active-role[id="roleCard_viewer"] { border-color: var(--purple); background: var(--purple-light); }
.role-card-icon { font-size: 24px; margin-bottom: 4px; }
.role-card-name { font-size: 14px; font-weight: 700; margin-bottom: 3px; }
.role-card-desc { font-size: 11px; color: var(--text-muted); margin-bottom: 8px; line-height: 1.4; }
.role-card-perms { display: flex; flex-wrap: wrap; gap: 4px; }
.perm { font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 12px; }
.perm-green { background: var(--success-light); color: var(--success); }
.perm-red { background: var(--danger-light); color: var(--danger); }
/* ============================================
ANIMATIONS
============================================ */
@keyframes fadeIn { from{opacity:0;transform:translateX(-8px)} to{opacity:1;transform:translateX(0)} }
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* update_radius.php — Update Radius Pusat Keagamaan
*
* POST params:
* id - ID pusat keagamaan
* radius - Radius baru dalam meter
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']);
exit;
}
$id = intval($_POST['id'] ?? 0);
$radius = intval($_POST['radius'] ?? 300);
if ($id < 1) {
echo json_encode(['success' => false, 'message' => 'ID tidak valid']);
exit;
}
// Batasi radius minimum 50 meter
if ($radius < 50) $radius = 50;
$sql = "UPDATE religious_centers SET radius=$radius WHERE id=$id";
if ($conn->query($sql)) {
echo json_encode([
'success' => true,
'id' => $id,
'radius' => $radius
]);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
$conn->close();
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* update_status.php — Update Status Rumah
*
* POST params:
* house_id - ID rumah
* status - Status baru (green/red/yellow)
* center_id - ID pusat keagamaan terdekat
*/
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'message' => 'Hanya menerima POST']);
exit;
}
$houseId = intval($_POST['house_id'] ?? 0);
$status = $conn->real_escape_string($_POST['status'] ?? 'green');
$centerId = intval($_POST['center_id'] ?? 0);
// Validasi status
$validStatuses = ['green', 'red', 'yellow'];
if (!in_array($status, $validStatuses)) {
echo json_encode(['success' => false, 'message' => 'Status tidak valid']);
exit;
}
// Validasi house_id
if ($houseId < 1) {
echo json_encode(['success' => false, 'message' => 'house_id tidak valid']);
exit;
}
// ========================
// Upsert status rumah
// ========================
// Cek apakah rumah sudah ada
$check = $conn->query("SELECT id FROM houses WHERE id = $houseId");
if ($check && $check->num_rows > 0) {
// Update
$sql = "UPDATE houses SET status='$status' WHERE id=$houseId";
} else {
// Insert (ambil posisi dari fixed positions yang sama seperti di JS)
// Posisi disimpan juga agar konsisten
$lat = floatval($_POST['lat'] ?? 0);
$lng = floatval($_POST['lng'] ?? 0);
$sql = "INSERT INTO houses (id, latitude, longitude, status) VALUES ($houseId, $lat, $lng, '$status')";
}
if ($conn->query($sql)) {
// ========================
// Catat ke aid_logs
// ========================
if ($status === 'yellow' && $centerId > 0) {
$logSql = "INSERT INTO aid_logs (house_id, religious_center_id, status, timestamp)
VALUES ($houseId, $centerId, 'helped', NOW())";
$conn->query($logSql);
} elseif ($status !== 'yellow') {
// Batalkan bantuan — catat sebagai reverted
$logSql = "INSERT INTO aid_logs (house_id, religious_center_id, status, timestamp)
VALUES ($houseId, $centerId, 'reverted', NOW())";
$conn->query($logSql);
}
echo json_encode([
'success' => true,
'house_id' => $houseId,
'status' => $status
]);
} else {
echo json_encode(['success' => false, 'message' => $conn->error]);
}
$conn->close();
+868
View File
@@ -0,0 +1,868 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pontianak — Portfolio Proyek</title>
<meta name="description" content="Kumpulan proyek WebGIS Kota Pontianak — Kepadatan Penduduk, SPBU, dan Distribusi Bantuan Sosial.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-primary: #f8fafc;
--bg-card: #ffffff;
--bg-card-hover: #ffffff;
--border: #e2e8f0;
--border-hover: #cbd5e1;
--text: #0f172a;
--text-muted: #475569;
--text-dim: #64748b;
--accent-1: #0891b2;
--accent-2: #7c3aed;
--accent-3: #d97706;
--accent-4: #059669;
--font: 'Inter', -apple-system, system-ui, sans-serif;
--mono: 'DM Mono', monospace;
}
html { scroll-behavior: smooth; }
body {
font-family: var(--font);
background: var(--bg-primary);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* =========================================
AMBIENT BACKGROUND
========================================= */
.bg-ambient {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
overflow: hidden;
}
.bg-orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.08;
animation: orbFloat 20s ease-in-out infinite;
}
.bg-orb:nth-child(1) {
width: 600px; height: 600px;
background: var(--accent-1);
top: -10%; left: -10%;
animation-delay: 0s;
}
.bg-orb:nth-child(2) {
width: 500px; height: 500px;
background: var(--accent-2);
bottom: -10%; right: -5%;
animation-delay: -7s;
}
.bg-orb:nth-child(3) {
width: 400px; height: 400px;
background: var(--accent-3);
top: 40%; left: 50%;
animation-delay: -14s;
}
@keyframes orbFloat {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -40px) scale(1.05); }
66% { transform: translate(-20px, 30px) scale(0.95); }
}
/* Grid pattern overlay */
.bg-grid {
position: fixed; inset: 0; z-index: 0; pointer-events: none;
background-image:
linear-gradient(rgba(15, 23, 42, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(15, 23, 42, 0.03) 1px, transparent 1px);
background-size: 60px 60px;
}
/* =========================================
NAVBAR
========================================= */
.navbar {
position: fixed; top: 0; left: 0; right: 0;
z-index: 100;
padding: 16px 40px;
display: flex; align-items: center; justify-content: space-between;
background: rgba(248, 250, 252, 0.8);
backdrop-filter: blur(20px) saturate(1.4);
border-bottom: 1px solid var(--border);
transition: all 0.3s;
}
.navbar.scrolled {
padding: 12px 40px;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 20px rgba(15, 23, 42, 0.03);
}
.nav-brand {
display: flex; align-items: center; gap: 12px;
}
.nav-logo {
width: 36px; height: 36px;
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
box-shadow: 0 4px 20px rgba(6, 182, 212, 0.3);
}
.nav-title {
font-size: 16px; font-weight: 700;
letter-spacing: -0.3px;
}
.nav-title span {
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-links {
display: flex; gap: 8px;
}
.nav-link {
padding: 8px 16px;
font-size: 13px; font-weight: 500;
color: var(--text-muted);
text-decoration: none;
border-radius: 8px;
transition: all 0.2s;
}
.nav-link:hover {
color: var(--text);
background: rgba(15, 23, 42, 0.05);
}
/* =========================================
HERO SECTION
========================================= */
.hero {
position: relative; z-index: 1;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center;
padding: 160px 40px 80px;
min-height: 70vh;
}
.hero-badge {
display: inline-flex; align-items: center; gap: 8px;
padding: 6px 16px 6px 8px;
background: rgba(139, 92, 246, 0.12);
border: 1px solid rgba(139, 92, 246, 0.2);
border-radius: 100px;
font-size: 12px; font-weight: 600; color: var(--accent-2);
margin-bottom: 24px;
animation: fadeInUp 0.6s ease;
}
.hero-badge-dot {
width: 8px; height: 8px;
background: var(--accent-2);
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.3); }
}
.hero h1 {
font-size: clamp(36px, 6vw, 64px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -1.5px;
margin-bottom: 20px;
animation: fadeInUp 0.6s ease 0.1s both;
}
.hero h1 .gradient-text {
background: linear-gradient(135deg, var(--accent-1) 0%, var(--accent-2) 50%, var(--accent-3) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-desc {
font-size: 17px; color: var(--text-muted);
max-width: 560px; line-height: 1.7;
margin-bottom: 36px;
animation: fadeInUp 0.6s ease 0.2s both;
}
.hero-stats {
display: flex; gap: 40px;
animation: fadeInUp 0.6s ease 0.3s both;
}
.hero-stat {
text-align: center;
}
.hero-stat-num {
font-size: 28px; font-weight: 800;
font-family: var(--mono);
background: linear-gradient(135deg, var(--accent-1), var(--accent-4));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-stat-label {
font-size: 11px; font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 4px;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* =========================================
PROJECTS SECTION
========================================= */
.projects {
position: relative; z-index: 1;
padding: 40px 40px 100px;
max-width: 1200px;
margin: 0 auto;
}
.section-label {
display: flex; align-items: center; gap: 10px;
font-size: 11px; font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--accent-1);
margin-bottom: 12px;
}
.section-label::before {
content: '';
width: 24px; height: 2px;
background: var(--accent-1);
border-radius: 2px;
}
.section-title {
font-size: 32px; font-weight: 800;
letter-spacing: -0.8px;
margin-bottom: 48px;
}
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 24px;
}
/* =========================================
PROJECT CARD
========================================= */
.project-card {
position: relative;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
text-decoration: none;
color: inherit;
display: flex; flex-direction: column;
box-shadow: 0 4px 20px rgba(15, 23, 42, 0.02);
}
.project-card:hover {
transform: translateY(-6px);
background: var(--bg-card-hover);
border-color: var(--border-hover);
box-shadow:
0 20px 45px rgba(15, 23, 42, 0.06),
0 0 0 1px rgba(15, 23, 42, 0.03);
}
.project-card:hover .card-icon-glow {
opacity: 0.25;
}
.project-card:hover .card-arrow {
transform: translate(3px, -3px);
opacity: 1;
}
.project-card:hover .card-preview {
transform: scale(1.03);
}
/* Card top: preview area */
.card-preview-wrap {
position: relative;
height: 200px;
overflow: hidden;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.02), rgba(15, 23, 42, 0.04));
}
.card-preview {
width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-icon-glow {
position: absolute;
width: 200px; height: 200px;
border-radius: 50%;
filter: blur(60px);
opacity: 0.12;
transition: opacity 0.4s;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
}
/* SVG icons */
.card-icon-svg {
position: relative; z-index: 1;
width: 100px; height: 100px;
}
/* Card number badge */
.card-number {
position: absolute; top: 16px; left: 16px;
font-family: var(--mono);
font-size: 11px; font-weight: 600;
color: var(--text-dim);
padding: 4px 10px;
background: rgba(15, 23, 42, 0.04);
border: 1px solid var(--border);
border-radius: 6px;
}
/* Card arrow */
.card-arrow {
position: absolute; top: 16px; right: 16px;
width: 32px; height: 32px;
background: rgba(15, 23, 42, 0.04);
border: 1px solid var(--border);
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
opacity: 0.5;
transition: all 0.3s;
}
.card-arrow svg {
width: 14px; height: 14px;
stroke: var(--text-muted);
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Card body */
.card-body {
padding: 24px 24px 28px;
flex: 1;
display: flex; flex-direction: column;
}
.card-tag {
display: inline-flex;
align-self: flex-start;
padding: 3px 10px;
font-size: 10px; font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
border-radius: 6px;
margin-bottom: 12px;
}
.card-tag.choropleth {
background: rgba(245, 158, 11, 0.12);
color: var(--accent-3);
border: 1px solid rgba(245, 158, 11, 0.2);
}
.card-tag.crud {
background: rgba(6, 182, 212, 0.12);
color: var(--accent-1);
border: 1px solid rgba(6, 182, 212, 0.2);
}
.card-tag.fullstack {
background: rgba(16, 185, 129, 0.12);
color: var(--accent-4);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.card-title {
font-size: 20px; font-weight: 700;
letter-spacing: -0.3px;
margin-bottom: 8px;
line-height: 1.3;
}
.card-desc {
font-size: 13px; color: var(--text-muted);
line-height: 1.7;
flex: 1;
}
/* Card tech tags */
.card-techs {
display: flex; flex-wrap: wrap; gap: 6px;
margin-top: 16px; padding-top: 16px;
border-top: 1px solid var(--border);
}
.card-tech {
padding: 3px 8px;
font-size: 10px; font-weight: 600;
color: var(--text-muted);
background: rgba(15, 23, 42, 0.03);
border: 1px solid var(--border);
border-radius: 5px;
font-family: var(--mono);
}
/* =========================================
FOOTER
========================================= */
.footer {
position: relative; z-index: 1;
text-align: center;
padding: 40px;
border-top: 1px solid var(--border);
}
.footer-text {
font-size: 12px; color: var(--text-dim);
}
.footer-text a {
color: var(--accent-1);
text-decoration: none;
}
/* =========================================
RESPONSIVE
========================================= */
@media (max-width: 768px) {
.navbar { padding: 12px 20px; }
.nav-links { display: none; }
.hero { padding: 120px 20px 60px; }
.hero-stats { gap: 24px; }
.projects { padding: 20px 20px 60px; }
.project-grid { grid-template-columns: 1fr; }
.card-preview-wrap { height: 160px; }
}
/* =========================================
SCROLL ANIMATIONS
========================================= */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: all 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body>
<!-- Ambient Background -->
<div class="bg-ambient">
<div class="bg-orb"></div>
<div class="bg-orb"></div>
<div class="bg-orb"></div>
</div>
<div class="bg-grid"></div>
<!-- Navbar -->
<nav class="navbar" id="navbar">
<div class="nav-brand">
<div class="nav-logo">🌍</div>
<div class="nav-title"><span>WebGIS</span> Pontianak</div>
</div>
<div class="nav-links">
<a href="#projects" class="nav-link">Proyek</a>
<a href="https://github.com" class="nav-link" target="_blank">GitHub</a>
</div>
</nav>
<!-- Hero -->
<section class="hero">
<div class="hero-badge">
<div class="hero-badge-dot"></div>
Sistem Informasi Geografis Berbasis Web
</div>
<h1>
Eksplorasi <span class="gradient-text">WebGIS</span><br>
Kota Pontianak
</h1>
<p class="hero-desc">
Kumpulan proyek WebGIS interaktif yang mencakup analisis kepadatan penduduk,
pemetaan SPBU, hingga distribusi bantuan sosial berbasis peta digital.
</p>
<div class="hero-stats">
<div class="hero-stat">
<div class="hero-stat-num">3</div>
<div class="hero-stat-label">Proyek</div>
</div>
<div class="hero-stat">
<div class="hero-stat-num">Leaflet</div>
<div class="hero-stat-label">Map Engine</div>
</div>
<div class="hero-stat">
<div class="hero-stat-num">PHP</div>
<div class="hero-stat-label">Backend</div>
</div>
</div>
</section>
<!-- Projects -->
<section class="projects" id="projects">
<div class="section-label">Portfolio</div>
<h2 class="section-title">Proyek yang Telah Dibuat</h2>
<div class="project-grid">
<!-- Card 1: WebGIS SPBU -->
<a href="http://localhost/webgis/03/index.html" class="project-card reveal" id="card-03">
<div class="card-preview-wrap">
<div class="card-icon-glow" style="background: var(--accent-1);"></div>
<div class="card-number">03</div>
<div class="card-arrow">
<svg viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
</div>
<div class="card-preview">
<!-- SPBU / Gas Station Icon -->
<svg class="card-icon-svg" viewBox="0 0 120 120" fill="none">
<!-- Gas pump body -->
<rect x="25" y="35" width="40" height="55" rx="6" fill="#06b6d4" opacity="0.8"/>
<rect x="30" y="42" width="30" height="20" rx="3" fill="#0e7490" opacity="0.6"/>
<!-- Display -->
<rect x="33" y="45" width="24" height="14" rx="2" fill="#164e63"/>
<text x="38" y="55" font-family="monospace" font-size="8" fill="#22d3ee">24H</text>
<!-- Nozzle -->
<path d="M65 45 L75 40 L78 50 L80 75" stroke="#06b6d4" stroke-width="3" fill="none" stroke-linecap="round"/>
<circle cx="80" cy="78" r="4" fill="#06b6d4" opacity="0.7"/>
<!-- Base -->
<rect x="20" y="88" width="50" height="6" rx="3" fill="#06b6d4" opacity="0.3"/>
<!-- Map pin markers -->
<circle cx="95" cy="35" r="6" fill="#10b981" opacity="0.8"/>
<circle cx="95" cy="35" r="2.5" fill="white"/>
<circle cx="100" cy="55" r="5" fill="#ef4444" opacity="0.8"/>
<circle cx="100" cy="55" r="2" fill="white"/>
<!-- Lines / roads -->
<path d="M85 45 L105 45" stroke="#334155" stroke-width="2" stroke-dasharray="4 3" opacity="0.4"/>
<path d="M88 65 L108 65" stroke="#334155" stroke-width="2" stroke-dasharray="4 3" opacity="0.4"/>
<!-- Polygon hint -->
<path d="M88 75 L105 72 L108 85 L90 88 Z" fill="#8b5cf6" opacity="0.15" stroke="#8b5cf6" stroke-width="1"/>
</svg>
</div>
</div>
<div class="card-body">
<div class="card-tag crud">CRUD + Draw Tools</div>
<div class="card-title">WebGIS SPBU Kota Pontianak</div>
<div class="card-desc">
Sistem pemetaan SPBU interaktif dengan fitur CRUD lengkap — tambah, edit, hapus
titik SPBU. Dilengkapi draw tools untuk menggambar jalan (polyline) dan kavling (polygon)
langsung di peta, serta filter status operasional 24 jam.
</div>
<div class="card-techs">
<span class="card-tech">Leaflet.js</span>
<span class="card-tech">Leaflet Draw</span>
<span class="card-tech">PHP + MySQL</span>
<span class="card-tech">CRUD</span>
</div>
</div>
</a>
<!-- Card 2: Kepadatan Penduduk -->
<a href="http://localhost/webgis/02/index.html" class="project-card reveal" id="card-02" style="animation-delay: 0.1s">
<div class="card-preview-wrap">
<div class="card-icon-glow" style="background: var(--accent-3);"></div>
<div class="card-number">02</div>
<div class="card-arrow">
<svg viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
</div>
<div class="card-preview">
<!-- Choropleth Map Icon -->
<svg class="card-icon-svg" viewBox="0 0 120 120" fill="none">
<!-- Map regions -->
<path d="M20 30 L55 20 L60 50 L35 60 Z" fill="#f59e0b" opacity="0.8" rx="4"/>
<path d="M55 20 L95 25 L90 55 L60 50 Z" fill="#d97706" opacity="0.7" rx="4"/>
<path d="M35 60 L60 50 L65 85 L25 90 Z" fill="#fbbf24" opacity="0.6" rx="4"/>
<path d="M60 50 L90 55 L95 80 L65 85 Z" fill="#b45309" opacity="0.9" rx="4"/>
<!-- Map outline glow -->
<path d="M20 30 L55 20 L95 25 L90 55 L95 80 L65 85 L25 90 L35 60 Z"
stroke="#f59e0b" stroke-width="1.5" fill="none" opacity="0.4"/>
<!-- People dots -->
<circle cx="45" cy="40" r="3" fill="white" opacity="0.7"/>
<circle cx="75" cy="38" r="4" fill="white" opacity="0.8"/>
<circle cx="50" cy="68" r="2.5" fill="white" opacity="0.6"/>
<circle cx="78" cy="65" r="5" fill="white" opacity="0.9"/>
<!-- Legend bar -->
<rect x="30" y="98" width="60" height="6" rx="3" fill="url(#grad-heat)" opacity="0.7"/>
<defs>
<linearGradient id="grad-heat" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#fef3c7"/>
<stop offset="50%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#7c2d12"/>
</linearGradient>
</defs>
</svg>
</div>
</div>
<div class="card-body">
<div class="card-tag choropleth">Choropleth Map</div>
<div class="card-title">Kepadatan Penduduk Kota Pontianak</div>
<div class="card-desc">
Visualisasi peta tematik kepadatan penduduk per kelurahan di Kota Pontianak
menggunakan teknik choropleth dengan gradasi warna berdasarkan jumlah jiwa.
Dilengkapi hover interaktif dan legenda dinamis.
</div>
<div class="card-techs">
<span class="card-tech">Leaflet.js</span>
<span class="card-tech">GeoJSON</span>
<span class="card-tech">Choropleth</span>
<span class="card-tech">Interactive</span>
</div>
</div>
</a>
<!-- Card 3: BantSOSial GIS -->
<a href="http://localhost/webgis/04/index.html" class="project-card reveal" id="card-04" style="animation-delay: 0.2s">
<div class="card-preview-wrap">
<div class="card-icon-glow" style="background: var(--accent-4);"></div>
<div class="card-number">04</div>
<div class="card-arrow">
<svg viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
</div>
<div class="card-preview">
<!-- Social Aid / Distribution Icon -->
<svg class="card-icon-svg" viewBox="0 0 120 120" fill="none">
<!-- Central mosque/worship icon -->
<path d="M60 25 Q60 15 60 15 Q65 20 65 25" fill="#10b981" opacity="0.8"/>
<rect x="45" y="25" width="30" height="35" rx="4" fill="#10b981" opacity="0.7"/>
<path d="M40 60 L80 60 L80 65 L40 65 Z" fill="#10b981" opacity="0.5"/>
<!-- Dome -->
<ellipse cx="60" cy="28" rx="12" ry="8" fill="#059669" opacity="0.6"/>
<!-- Radius circle -->
<circle cx="60" cy="50" r="35" fill="none" stroke="#10b981" stroke-width="1.5"
stroke-dasharray="6 4" opacity="0.35"/>
<!-- Houses around -->
<rect x="20" y="40" width="10" height="10" rx="2" fill="#ef4444" opacity="0.7"/>
<rect x="90" y="45" width="10" height="10" rx="2" fill="#f59e0b" opacity="0.7"/>
<rect x="30" y="75" width="10" height="10" rx="2" fill="#22c55e" opacity="0.7"/>
<rect x="80" y="70" width="10" height="10" rx="2" fill="#ef4444" opacity="0.7"/>
<rect x="55" y="80" width="10" height="10" rx="2" fill="#f59e0b" opacity="0.7"/>
<!-- Connection lines -->
<line x1="25" y1="45" x2="45" y2="45" stroke="#ef4444" stroke-width="0.8" opacity="0.4" stroke-dasharray="3 2"/>
<line x1="80" y1="50" x2="95" y2="50" stroke="#f59e0b" stroke-width="0.8" opacity="0.4" stroke-dasharray="3 2"/>
<!-- Legend dots -->
<circle cx="25" cy="100" r="4" fill="#ef4444" opacity="0.7"/>
<circle cx="50" cy="100" r="4" fill="#f59e0b" opacity="0.7"/>
<circle cx="75" cy="100" r="4" fill="#22c55e" opacity="0.7"/>
<text x="32" y="103" font-size="7" fill="#64748b" font-family="sans-serif">Belum</text>
<text x="57" y="103" font-size="7" fill="#64748b" font-family="sans-serif">Sudah</text>
<text x="82" y="103" font-size="7" fill="#64748b" font-family="sans-serif">Luar</text>
</svg>
</div>
</div>
<div class="card-body">
<div class="card-tag fullstack">Full-Stack WebGIS</div>
<div class="card-title">Distribusi Bantuan Sosial Pontianak</div>
<div class="card-desc">
Aplikasi WebGIS lengkap untuk memetakan distribusi bantuan sosial berbasis rumah ibadah.
Fitur meliputi role system (Admin, Surveyer, Viewer), radius jangkauan,
input data keluarga miskin, status bantuan, dan sistem pelaporan masyarakat.
</div>
<div class="card-techs">
<span class="card-tech">Leaflet.js</span>
<span class="card-tech">PHP + MySQL</span>
<span class="card-tech">Role System</span>
<span class="card-tech">Pelaporan</span>
</div>
</div>
</a>
</div>
</section>
<div class="card-arrow">
<svg viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
</div>
<div class="card-preview">
<!-- Choropleth Map Icon -->
<svg class="card-icon-svg" viewBox="0 0 120 120" fill="none">
<!-- Map regions -->
<path d="M20 30 L55 20 L60 50 L35 60 Z" fill="#f59e0b" opacity="0.8" rx="4"/>
<path d="M55 20 L95 25 L90 55 L60 50 Z" fill="#d97706" opacity="0.7" rx="4"/>
<path d="M35 60 L60 50 L65 85 L25 90 Z" fill="#fbbf24" opacity="0.6" rx="4"/>
<path d="M60 50 L90 55 L95 80 L65 85 Z" fill="#b45309" opacity="0.9" rx="4"/>
<!-- Map outline glow -->
<path d="M20 30 L55 20 L95 25 L90 55 L95 80 L65 85 L25 90 L35 60 Z"
stroke="#f59e0b" stroke-width="1.5" fill="none" opacity="0.4"/>
<!-- People dots -->
<circle cx="45" cy="40" r="3" fill="white" opacity="0.7"/>
<circle cx="75" cy="38" r="4" fill="white" opacity="0.8"/>
<circle cx="50" cy="68" r="2.5" fill="white" opacity="0.6"/>
<circle cx="78" cy="65" r="5" fill="white" opacity="0.9"/>
<!-- Legend bar -->
<rect x="30" y="98" width="60" height="6" rx="3" fill="url(#grad-heat)" opacity="0.7"/>
<defs>
<linearGradient id="grad-heat" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#fef3c7"/>
<stop offset="50%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#7c2d12"/>
</linearGradient>
</defs>
</svg>
</div>
</div>
<div class="card-body">
<div class="card-tag choropleth">Choropleth Map</div>
<div class="card-title">Kepadatan Penduduk Kota Pontianak</div>
<div class="card-desc">
Visualisasi peta tematik kepadatan penduduk per kelurahan di Kota Pontianak
menggunakan teknik choropleth dengan gradasi warna berdasarkan jumlah jiwa.
Dilengkapi hover interaktif dan legenda dinamis.
</div>
<div class="card-techs">
<span class="card-tech">Leaflet.js</span>
<span class="card-tech">GeoJSON</span>
<span class="card-tech">Choropleth</span>
<span class="card-tech">Interactive</span>
</div>
</div>
</a>
<div class="card-preview-wrap">
<div class="card-icon-glow" style="background: var(--accent-1);"></div>
<div class="card-number">03</div>
<div class="card-arrow">
<svg viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
</div>
<div class="card-preview">
<!-- SPBU / Gas Station Icon -->
<svg class="card-icon-svg" viewBox="0 0 120 120" fill="none">
<!-- Gas pump body -->
<rect x="25" y="35" width="40" height="55" rx="6" fill="#06b6d4" opacity="0.8"/>
<rect x="30" y="42" width="30" height="20" rx="3" fill="#0e7490" opacity="0.6"/>
<!-- Display -->
<rect x="33" y="45" width="24" height="14" rx="2" fill="#164e63"/>
<text x="38" y="55" font-family="monospace" font-size="8" fill="#22d3ee">24H</text>
<!-- Nozzle -->
<path d="M65 45 L75 40 L78 50 L80 75" stroke="#06b6d4" stroke-width="3" fill="none" stroke-linecap="round"/>
<circle cx="80" cy="78" r="4" fill="#06b6d4" opacity="0.7"/>
<!-- Base -->
<rect x="20" y="88" width="50" height="6" rx="3" fill="#06b6d4" opacity="0.3"/>
<!-- Map pin markers -->
<circle cx="95" cy="35" r="6" fill="#10b981" opacity="0.8"/>
<circle cx="95" cy="35" r="2.5" fill="white"/>
<circle cx="100" cy="55" r="5" fill="#ef4444" opacity="0.8"/>
<circle cx="100" cy="55" r="2" fill="white"/>
<!-- Lines / roads -->
<path d="M85 45 L105 45" stroke="#334155" stroke-width="2" stroke-dasharray="4 3" opacity="0.4"/>
<path d="M88 65 L108 65" stroke="#334155" stroke-width="2" stroke-dasharray="4 3" opacity="0.4"/>
<!-- Polygon hint -->
<path d="M88 75 L105 72 L108 85 L90 88 Z" fill="#8b5cf6" opacity="0.15" stroke="#8b5cf6" stroke-width="1" opacity="0.3"/>
</svg>
</div>
</div>
<div class="card-body">
<div class="card-tag crud">CRUD + Draw Tools</div>
<div class="card-title">WebGIS SPBU Kota Pontianak</div>
<div class="card-desc">
Sistem pemetaan SPBU interaktif dengan fitur CRUD lengkap — tambah, edit, hapus
titik SPBU. Dilengkapi draw tools untuk menggambar jalan (polyline) dan kavling (polygon)
langsung di peta, serta filter status operasional 24 jam.
</div>
<div class="card-techs">
<span class="card-tech">Leaflet.js</span>
<span class="card-tech">Leaflet Draw</span>
<span class="card-tech">PHP + MySQL</span>
<span class="card-tech">CRUD</span>
</div>
</div>
</a>
<!-- Card 3: BantSOSial GIS -->
<a href="http://localhost/webgis/04/index.html" class="project-card reveal" id="card-04" style="animation-delay: 0.2s">
<div class="card-preview-wrap">
<div class="card-icon-glow" style="background: var(--accent-4);"></div>
<div class="card-number">04</div>
<div class="card-arrow">
<svg viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H7M17 7V17"/></svg>
</div>
<div class="card-preview">
<!-- Social Aid / Distribution Icon -->
<svg class="card-icon-svg" viewBox="0 0 120 120" fill="none">
<!-- Central mosque/worship icon -->
<path d="M60 25 Q60 15 60 15 Q65 20 65 25" fill="#10b981" opacity="0.8"/>
<rect x="45" y="25" width="30" height="35" rx="4" fill="#10b981" opacity="0.7"/>
<path d="M40 60 L80 60 L80 65 L40 65 Z" fill="#10b981" opacity="0.5"/>
<!-- Dome -->
<ellipse cx="60" cy="28" rx="12" ry="8" fill="#059669" opacity="0.6"/>
<!-- Radius circle -->
<circle cx="60" cy="50" r="35" fill="none" stroke="#10b981" stroke-width="1.5"
stroke-dasharray="6 4" opacity="0.35"/>
<!-- Houses around -->
<rect x="20" y="40" width="10" height="10" rx="2" fill="#ef4444" opacity="0.7"/>
<rect x="90" y="45" width="10" height="10" rx="2" fill="#f59e0b" opacity="0.7"/>
<rect x="30" y="75" width="10" height="10" rx="2" fill="#22c55e" opacity="0.7"/>
<rect x="80" y="70" width="10" height="10" rx="2" fill="#ef4444" opacity="0.7"/>
<rect x="55" y="80" width="10" height="10" rx="2" fill="#f59e0b" opacity="0.7"/>
<!-- Connection lines -->
<line x1="25" y1="45" x2="45" y2="45" stroke="#ef4444" stroke-width="0.8" opacity="0.4" stroke-dasharray="3 2"/>
<line x1="80" y1="50" x2="95" y2="50" stroke="#f59e0b" stroke-width="0.8" opacity="0.4" stroke-dasharray="3 2"/>
<!-- Legend dots -->
<circle cx="25" cy="100" r="4" fill="#ef4444" opacity="0.7"/>
<circle cx="50" cy="100" r="4" fill="#f59e0b" opacity="0.7"/>
<circle cx="75" cy="100" r="4" fill="#22c55e" opacity="0.7"/>
<text x="32" y="103" font-size="7" fill="#64748b" font-family="sans-serif">Belum</text>
<text x="57" y="103" font-size="7" fill="#64748b" font-family="sans-serif">Sudah</text>
<text x="82" y="103" font-size="7" fill="#64748b" font-family="sans-serif">Luar</text>
</svg>
</div>
</div>
<div class="card-body">
<div class="card-tag fullstack">Full-Stack WebGIS</div>
<div class="card-title">Distribusi Bantuan Sosial Pontianak</div>
<div class="card-desc">
Aplikasi WebGIS lengkap untuk memetakan distribusi bantuan sosial berbasis rumah ibadah.
Fitur meliputi role system (Admin, Surveyer, Viewer), radius jangkauan,
input data keluarga miskin, status bantuan, dan sistem pelaporan masyarakat.
</div>
<div class="card-techs">
<span class="card-tech">Leaflet.js</span>
<span class="card-tech">PHP + MySQL</span>
<span class="card-tech">Role System</span>
<span class="card-tech">Pelaporan</span>
</div>
</div>
</a>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="footer-text">
&copy; 2026 WebGIS Pontianak — Dibuat dengan
<a href="https://leafletjs.com" target="_blank">Leaflet.js</a> & ❤️
</div>
</footer>
<script>
// Navbar scroll effect
const navbar = document.getElementById('navbar');
window.addEventListener('scroll', () => {
navbar.classList.toggle('scrolled', window.scrollY > 40);
});
// Scroll reveal animation
const revealEls = document.querySelectorAll('.reveal');
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach((entry, i) => {
if (entry.isIntersecting) {
setTimeout(() => {
entry.target.classList.add('visible');
}, i * 120);
revealObserver.unobserve(entry.target);
}
});
}, { threshold: 0.15 });
revealEls.forEach(el => revealObserver.observe(el));
</script>
</body>
</html>