Initial commit
This commit is contained in:
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
]);
|
||||
?>
|
||||
@@ -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
@@ -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)} m²
|
||||
|
||||
<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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
$conn = new mysqli("localhost", "root", "", "webgis1");
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Koneksi gagal: " . $conn->connect_error);
|
||||
}
|
||||
?>
|
||||
@@ -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";
|
||||
}
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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";
|
||||
?>
|
||||
@@ -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";
|
||||
?>
|
||||
@@ -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
@@ -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: '© <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 © <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 ? `–${to}` : '+'}`);
|
||||
}
|
||||
|
||||
div.innerHTML = labels.join('<br>');
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+19407
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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>
|
||||
@@ -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');
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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
@@ -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)} }
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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">
|
||||
© 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>
|
||||
Reference in New Issue
Block a user