Files
webgis_Ikbal/spbu/index.php
T
2026-06-11 16:37:05 +07:00

750 lines
20 KiB
PHP

<?php
$conn = new mysqli("localhost", "root", "", "spbu");
// HANDLE INSERT
if(isset($_POST['nama'])){
$nama = $_POST['nama'];
$wa = $_POST['wa'];
$buka = $_POST['buka'];
$lat = $_POST['lat'];
$lng = $_POST['lng'];
$conn->query("INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude)
VALUES ('$nama','$wa','$buka','$lat','$lng')");
exit("success");
}
// HANDLE DELETE
if(isset($_POST['delete_id'])){
$id = intval($_POST['delete_id']);
$conn->query("DELETE FROM spbu WHERE id = $id");
exit("deleted");
}
// HANDLE MOVE (update lat/lng)
if(isset($_POST['move_id'])){
$id = intval($_POST['move_id']);
$lat = $_POST['move_lat'];
$lng = $_POST['move_lng'];
$conn->query("UPDATE spbu SET latitude='$lat', longitude='$lng' WHERE id=$id");
exit("moved");
}
// HANDLE GET DATA
if(isset($_GET['get'])){
$result = $conn->query("SELECT * FROM spbu");
$data = [];
while($row = $result->fetch_assoc()){
$data[] = $row;
}
echo json_encode($data);
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPBU Map — Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
<style>
:root {
--fuel-orange: #FF5722;
--fuel-amber: #FFA726;
--night: #0D0F14;
--panel: #13161E;
--panel-border: rgba(255,255,255,0.07);
--text: #F0F0F0;
--text-muted: #8A8FA0;
--green: #00C896;
--red: #FF4757;
--radius: 14px;
--shadow: 0 20px 60px rgba(0,0,0,0.5);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'DM Sans', sans-serif;
background: var(--night);
color: var(--text);
height: 100vh;
overflow: hidden;
}
#map {
position: absolute;
inset: 0;
z-index: 0;
filter: brightness(0.9) saturate(0.85);
}
/* ── HEADER ── */
#header {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 50px;
padding: 10px 22px 10px 16px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: var(--shadow), 0 0 0 1px rgba(255,87,34,0.15);
backdrop-filter: blur(20px);
animation: slideDown 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
white-space: nowrap;
}
.header-icon {
width: 34px;
height: 34px;
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
#header h1 {
font-family: 'Syne', sans-serif;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--text);
}
#header p {
font-size: 11px;
color: var(--text-muted);
font-weight: 300;
letter-spacing: 0.03em;
}
/* ── LEGEND ── */
#legend {
position: absolute;
bottom: 30px;
left: 20px;
z-index: 1000;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: var(--radius);
padding: 14px 18px;
box-shadow: var(--shadow);
backdrop-filter: blur(20px);
animation: slideUp 0.5s 0.1s cubic-bezier(0.34,1.56,0.64,1) both;
}
#legend h3 {
font-family: 'Syne', sans-serif;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 9px;
margin-bottom: 7px;
font-size: 12.5px;
color: var(--text);
}
.legend-item:last-child { margin-bottom: 0; }
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 0 8px currentColor;
}
.dot-green { background: var(--green); color: var(--green); }
.dot-red { background: var(--red); color: var(--red); }
/* ── HINT CHIP ── */
#hint {
position: absolute;
bottom: 30px;
right: 20px;
z-index: 1000;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 50px;
padding: 10px 16px;
font-size: 12px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 7px;
box-shadow: var(--shadow);
backdrop-filter: blur(20px);
animation: slideUp 0.5s 0.2s cubic-bezier(0.34,1.56,0.64,1) both;
}
.hint-pulse {
width: 8px;
height: 8px;
background: var(--fuel-orange);
border-radius: 50%;
animation: pulse 2s infinite;
flex-shrink: 0;
}
/* ── TOAST ── */
#toast {
position: absolute;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-10px);
z-index: 2000;
background: var(--panel);
border: 1px solid var(--panel-border);
border-radius: 50px;
padding: 10px 20px;
font-size: 13px;
color: var(--text);
box-shadow: var(--shadow);
backdrop-filter: blur(20px);
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
white-space: nowrap;
}
#toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
/* ── LEAFLET POPUP OVERRIDE ── */
.leaflet-popup-content-wrapper {
background: var(--panel) !important;
color: var(--text) !important;
border: 1px solid var(--panel-border) !important;
border-radius: var(--radius) !important;
box-shadow: var(--shadow) !important;
padding: 0 !important;
overflow: hidden;
min-width: 240px;
}
.leaflet-popup-content {
margin: 0 !important;
width: auto !important;
}
.leaflet-popup-tip {
background: var(--panel) !important;
}
.leaflet-popup-close-button {
color: var(--text-muted) !important;
font-size: 18px !important;
top: 10px !important;
right: 12px !important;
}
.leaflet-popup-close-button:hover {
color: var(--text) !important;
}
/* ── POPUP CONTENT ── */
.popup-header {
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
padding: 16px 18px 14px;
border-bottom: 1px solid var(--panel-border);
}
.popup-header .popup-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fuel-orange);
margin-bottom: 4px;
}
.popup-header .popup-title {
font-family: 'Syne', sans-serif;
font-size: 16px;
font-weight: 700;
color: var(--text);
line-height: 1.2;
}
.popup-body {
padding: 14px 18px 16px;
}
.popup-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 9px;
font-size: 13px;
color: var(--text-muted);
}
.popup-row:last-child { margin-bottom: 0; }
.popup-row svg { flex-shrink: 0; opacity: 0.7; }
.popup-row a { color: var(--fuel-amber); text-decoration: none; }
.popup-row a:hover { text-decoration: underline; }
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 50px;
font-size: 11px;
font-weight: 500;
}
.badge-green {
background: rgba(0,200,150,0.12);
color: var(--green);
border: 1px solid rgba(0,200,150,0.2);
}
.badge-red {
background: rgba(255,71,87,0.12);
color: var(--red);
border: 1px solid rgba(255,71,87,0.2);
}
/* ── POPUP FOOTER ── */
.popup-footer {
border-top: 1px solid var(--panel-border);
padding: 10px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.drag-hint {
font-size: 11px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 5px;
opacity: 0.65;
}
.btn-delete {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255,71,87,0.1);
color: var(--red);
border: 1px solid rgba(255,71,87,0.2);
border-radius: 8px;
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
font-family: 'DM Sans', sans-serif;
cursor: pointer;
transition: background 0.2s, transform 0.15s;
}
.btn-delete:hover { background: rgba(255,71,87,0.22); transform: translateY(-1px); }
.btn-delete:active { transform: translateY(0); }
/* ── FORM POPUP ── */
.form-popup-header {
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
padding: 14px 18px 12px;
border-bottom: 1px solid var(--panel-border);
}
.form-popup-header .fp-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--fuel-orange);
margin-bottom: 3px;
}
.form-popup-header .fp-title {
font-family: 'Syne', sans-serif;
font-size: 15px;
font-weight: 700;
}
.form-popup-body {
padding: 14px 18px 18px;
display: flex;
flex-direction: column;
gap: 10px;
}
.fp-field label {
display: block;
font-size: 10px;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 5px;
}
.fp-field input,
.fp-field select {
width: 100%;
background: rgba(255,255,255,0.05);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 9px 12px;
font-size: 13px;
font-family: 'DM Sans', sans-serif;
color: var(--text);
outline: none;
transition: border-color 0.2s, background 0.2s;
-webkit-appearance: none;
}
.fp-field input::placeholder { color: rgba(255,255,255,0.2); }
.fp-field input:focus,
.fp-field select:focus {
border-color: rgba(255,87,34,0.5);
background: rgba(255,87,34,0.05);
}
.fp-field select option { background: #1a1d26; color: var(--text); }
.fp-btn {
width: 100%;
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
color: #fff;
border: none;
border-radius: 8px;
padding: 11px;
font-size: 13px;
font-weight: 600;
font-family: 'Syne', sans-serif;
letter-spacing: 0.04em;
cursor: pointer;
transition: opacity 0.2s, transform 0.15s;
margin-top: 2px;
}
.fp-btn:hover { opacity: 0.88; transform: translateY(-1px); }
.fp-btn:active { transform: translateY(0); opacity: 1; }
/* ── ANIMATIONS ── */
@keyframes slideDown {
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.8); }
}
.leaflet-control-attribution {
background: rgba(13,15,20,0.7) !important;
color: #666 !important;
backdrop-filter: blur(8px);
border-radius: 6px 0 0 0 !important;
}
.leaflet-control-attribution a { color: #888 !important; }
.leaflet-control-zoom a {
background: var(--panel) !important;
color: var(--text) !important;
border-color: var(--panel-border) !important;
}
.leaflet-control-zoom a:hover {
background: rgba(255,87,34,0.15) !important;
color: var(--fuel-orange) !important;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="header">
<div class="header-icon">⛽</div>
<div>
<h1>SPBU Map</h1>
<p>Pontianak, Kalimantan Barat</p>
</div>
</div>
<div id="legend">
<h3>Keterangan</h3>
<div class="legend-item"><div class="legend-dot dot-green"></div>Buka 24 Jam</div>
<div class="legend-item"><div class="legend-dot dot-red"></div>Tidak 24 Jam</div>
</div>
<div id="hint">
<div class="hint-pulse"></div>
Klik peta untuk tambah SPBU
</div>
<div id="toast">
<div class="toast-dot" id="toast-dot"></div>
<span id="toast-msg"></span>
</div>
<script>
// ── TOAST ──
function showToast(msg, color) {
const toast = document.getElementById('toast');
document.getElementById('toast-dot').style.background = color || '#00C896';
document.getElementById('toast-msg').textContent = msg;
toast.classList.add('show');
clearTimeout(toast._t);
toast._t = setTimeout(() => toast.classList.remove('show'), 2500);
}
// ── ICONS ──
const greenIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41]
});
const redIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41]
});
// ── MAP ──
const map = L.map('map').setView([-0.05538, 109.34947], 16);
// ── TILE ──
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19
}).addTo(map);
// ── LAYER GROUPS (MUST BE HERE) ──
const layer24 = L.layerGroup();
const layerNon24 = L.layerGroup();
// ── CONTROL ──
const overlayMaps = {
"SPBU 24 Jam": layer24,
"SPBU Tidak 24 Jam": layerNon24
};
L.control.layers(null, overlayMaps, { collapsed: false }).addTo(map);
// ── DEFAULT SHOW ──
layer24.addTo(map);
layerNon24.addTo(map);
// ── BUILD POPUP HTML ──
function buildPopup(d) {
const is24 = d.buka_24_jam == 1;
return `
<div class="popup-header">
<div class="popup-label">Stasiun BBM</div>
<div class="popup-title">${d.nama_spbu}</div>
</div>
<div class="popup-body">
<div class="popup-row">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.63A2 2 0 012 0h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14h-1.08"/></svg>
<a href="https://wa.me/${d.nomor_wa}" target="_blank">${d.nomor_wa}</a>
</div>
<div class="popup-row">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span class="badge ${is24 ? 'badge-green' : 'badge-red'}">${is24 ? '✓ Buka 24 Jam' : '✗ Tidak 24 Jam'}</span>
</div>
</div>
<div class="popup-footer">
<span class="drag-hint">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/></svg>
Tarik untuk pindah
</span>
<button class="btn-delete" onclick="deleteEntry(${d.id}, this)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
Hapus
</button>
</div>
`;
}
// ── LOAD & CREATE MARKERS ──
// Keep a map of id → marker for deletion
const markerMap = {};
function loadData() {
// CLEAR OLD MARKERS FIRST
layer24.clearLayers();
layerNon24.clearLayers();
fetch('index.php?get=1')
.then(res => res.json())
.then(data => {
data.forEach(d => addMarker(d));
});
}
function addMarker(d) {
const is24 = d.buka_24_jam == 1;
const marker = L.marker([d.latitude, d.longitude], {
icon: is24 ? greenIcon : redIcon,
draggable: true
});
if (is24) {
marker.addTo(layer24);
} else {
marker.addTo(layerNon24);
}
marker.bindPopup(buildPopup(d));
markerMap[d.id] = marker;
// DRAG → AUTO SAVE
marker.on('dragend', function(e) {
map.closePopup();
const { lat, lng } = e.target.getLatLng();
const fd = new FormData();
fd.append('move_id', d.id);
fd.append('move_lat', lat);
fd.append('move_lng', lng);
fetch('index.php', { method: 'POST', body: fd })
.then(() => {
d.latitude = lat;
d.longitude = lng;
showToast('Posisi disimpan ✓', '#FFA726');
});
});
}
// ── LAYER GROUPS ──
loadData();
// ── DELETE ──
function deleteEntry(id) {
if (!confirm('Hapus SPBU ini?')) return;
const fd = new FormData();
fd.append('delete_id', id);
fetch('index.php', { method: 'POST', body: fd })
.then(() => {
map.closePopup();
if (markerMap[id]) {
layer24.removeLayer(markerMap[id]);
layerNon24.removeLayer(markerMap[id]);
delete markerMap[id];
}
showToast('SPBU dihapus', '#FF4757');
});
}
// ── ADD FORM ──
function createForm(lat, lng) {
return `
<div class="form-popup-header">
<div class="fp-label">Tambah Titik</div>
<div class="fp-title">SPBU Baru</div>
</div>
<div class="form-popup-body">
<div class="fp-field">
<label>Nama SPBU</label>
<input id="nama" placeholder="Contoh: Pertamina Sungai Raya">
</div>
<div class="fp-field">
<label>Nomor WhatsApp</label>
<input id="wa" placeholder="628xx...">
</div>
<div class="fp-field">
<label>Buka 24 Jam?</label>
<select id="buka">
<option value="1">✓ Iya, 24 Jam</option>
<option value="0">✗ Tidak</option>
</select>
</div>
<button class="fp-btn" onclick="submitData(${lat}, ${lng})">Simpan SPBU</button>
</div>
`;
}
// ── SUBMIT ──
function submitData(lat, lng) {
const nama = document.getElementById('nama').value;
const wa = document.getElementById('wa').value;
const buka = document.getElementById('buka').value;
const fd = new FormData();
fd.append('nama', nama);
fd.append('wa', wa);
fd.append('buka', buka);
fd.append('lat', lat);
fd.append('lng', lng);
fetch('index.php', { method: 'POST', body: fd })
.then(res => res.text())
.then(() => {
// Fetch the newly inserted row (last inserted id) by re-fetching all
// and finding the one not yet in markerMap
fetch('index.php?get=1')
.then(r => r.json())
.then(data => {
data.forEach(d => {
if (!markerMap[d.id]) addMarker(d);
});
});
map.closePopup();
showToast('SPBU berhasil disimpan ✓', '#00C896');
});
}
// ── CLICK MAP → ADD FORM ──
map.on('click', function(e) {
L.popup()
.setLatLng(e.latlng)
.setContent(createForm(e.latlng.lat, e.latlng.lng))
.openOn(map);
});
</script>
</body>
</html>