feat: implement responsive UI layouts and collapsible control panels across application pages

This commit is contained in:
z0rayy
2026-06-11 05:28:31 +07:00
parent b027f02cf0
commit 4e73e17656
7 changed files with 435 additions and 83 deletions
+120 -6
View File
@@ -1099,6 +1099,67 @@
}
}
/* ---- Floating Panels Toggle ---- */
.map-floating-panel {
transition: width 0.3s ease, height 0.3s ease, background 0.3s ease;
overflow: hidden;
}
.map-floating-panel .panel-icon,
.map-floating-panel .panel-close-btn {
display: none;
}
.map-floating-panel .panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
@media (max-width: 900px) {
.map-floating-panel.collapsed {
width: 44px !important;
height: 44px !important;
min-height: 44px !important;
padding: 0 !important;
display: flex !important;
align-items: center;
justify-content: center;
cursor: pointer;
}
.map-floating-panel.collapsed:hover {
background: rgba(59, 130, 246, 0.2) !important;
border-color: rgba(59, 130, 246, 0.5) !important;
}
.map-floating-panel.collapsed > *:not(.panel-icon) {
display: none !important;
}
.map-floating-panel .panel-icon {
display: none;
font-size: 20px;
line-height: 1;
}
.map-floating-panel.collapsed .panel-icon {
display: block;
}
.map-floating-panel .panel-close-btn {
display: flex;
background: none;
border: none;
color: var(--clr-muted);
cursor: pointer;
font-size: 18px;
line-height: 1;
padding: 4px;
border-radius: 4px;
align-items: center;
justify-content: center;
}
.map-floating-panel .panel-close-btn:hover {
color: var(--clr-text);
background: rgba(255,255,255,0.1);
}
}
/* ---- Custom Marker ---- */
.custom-marker {
display: flex; justify-content: center; align-items: center;
@@ -1163,6 +1224,46 @@
}
.routing-panel h3 { font-size: 12px; font-weight: 700; color: #f1f5f9; margin-bottom: 12px; border-bottom: 1px solid #334155; padding-bottom: 8px; text-transform: uppercase; margin-top:0; }
@media (max-width: 900px) {
.app-wrapper { height: 100dvh; }
header { height: auto; min-height: 64px; padding: 10px 14px; gap: 10px; flex-wrap: wrap; }
.header-title h1 { font-size: 16px; }
.header-actions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
.content-panel-header { align-items: flex-start; flex-direction: column; gap: 10px; }
.content-panel-header .btn-group { width: 100%; flex-wrap: wrap; }
.content-panel-header .btn-group .btn { flex: 1; min-width: 130px; }
.table { min-width: 680px; }
.modal { width: calc(100vw - 24px); max-height: calc(100dvh - 24px); }
.modal-footer { flex-wrap: wrap; }
.modal-footer .btn { flex: 1; min-width: 120px; }
.search-panel { left: 82px; right: 12px; top: 12px; width: auto; margin-left: 0; transform: none; }
.routing-panel { top: auto; right: 12px; left: 82px; bottom: 12px; width: auto; }
}
@media (max-width: 560px) {
.sidebar { width: 56px; }
.sidebar-header { padding: 12px 8px; justify-content: center; }
.sidebar-logo-icon { width: 34px; height: 34px; font-size: 14px; }
.sidebar-toggle,
.sidebar-logo-text,
.sidebar-item-text,
.sidebar-item-badge,
.sidebar-section-title { display: none !important; }
.sidebar-nav { padding: 10px 0; }
.sidebar-section { padding: 8px 0; }
.sidebar-item { padding: 12px 14px; justify-content: center; }
.content-panel-body { padding: 12px; }
.search-box input,
.form-group input,
.form-group select,
.form-group textarea { font-size: 16px; }
.search-panel { left: 68px; right: 8px; top: 8px; padding: 10px; }
.search-results { max-height: 135px; }
.routing-panel { left: 68px; right: 8px; bottom: 8px; padding: 10px; }
.routing-panel h3,
.search-panel h3 { font-size: 11px; margin-bottom: 8px; }
}
</style>
</head>
<body>
@@ -1272,8 +1373,12 @@
<div class="panel-map" id="map" role="main" aria-label="Peta persebaran WebGIS"></div>
<!-- Search Panel (Top Center) -->
<div class="search-panel" id="admin-search-panel">
<h3>🔍 Pencarian Global</h3>
<div class="search-panel map-floating-panel collapsed" id="admin-search-panel" onclick="if(this.classList.contains('collapsed')) this.classList.remove('collapsed');">
<div class="panel-icon">🔍</div>
<div class="panel-header">
<h3 style="margin-bottom: 0;">🔍 Pencarian Global</h3>
<button class="panel-close-btn" onclick="event.stopPropagation(); document.getElementById('admin-search-panel').classList.add('collapsed');" title="Tutup">×</button>
</div>
<div class="search-input-wrap">
<input type="text" id="global-search" class="ctrl-input" placeholder="Cari SPBU, Jalan, Ibadah..." autocomplete="off">
</div>
@@ -1295,14 +1400,18 @@
</div> -->
<!-- Floating "Tambah Data" Toolbar -->
<div id="map-add-toolbar" style="
<div id="map-add-toolbar" class="map-floating-panel collapsed" onclick="if(this.classList.contains('collapsed')) this.classList.remove('collapsed');" style="
position:absolute; top:80px; left:16px; z-index:1000;
background:rgba(15,23,42,0.96); backdrop-filter:blur(10px);
border:1px solid #334155; border-radius:12px;
padding:14px 12px; box-shadow:0 4px 24px rgba(0,0,0,0.6);
width:158px; font-family:'Inter',sans-serif;
">
<div style="font-size:10px;font-weight:700;color:#60a5fa;text-transform:uppercase;letter-spacing:0.6px;margin-bottom:10px;">⊕ Tambah Data</div>
<div class="panel-icon" style="color:#60a5fa;"></div>
<div class="panel-header" style="margin-bottom:10px;">
<div style="font-size:10px;font-weight:700;color:#60a5fa;text-transform:uppercase;letter-spacing:0.6px;margin-bottom:0;">⊕ Tambah Data</div>
<button class="panel-close-btn" onclick="event.stopPropagation(); document.getElementById('map-add-toolbar').classList.add('collapsed');" title="Tutup">×</button>
</div>
<div style="font-size:10px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:0.4px;margin-bottom:5px;">📍 Titik</div>
<button class="map-tool-btn" id="map-add-spbu"
@@ -1330,8 +1439,13 @@
" onclick="window.mapToolCancel()">✕ Batal</button>
</div>
<div class="legend" id="legend-peta">
<div class="legend-title">SPBU</div>
<div class="legend map-floating-panel collapsed" id="legend-peta" onclick="if(this.classList.contains('collapsed')) this.classList.remove('collapsed');">
<div class="panel-icon">📋</div>
<div class="panel-header" style="margin-bottom:8px; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 8px;">
<div style="font-size:11px;font-weight:700;color:var(--clr-text);text-transform:uppercase;margin:0;">Legenda</div>
<button class="panel-close-btn" onclick="event.stopPropagation(); document.getElementById('legend-peta').classList.add('collapsed');" title="Tutup">×</button>
</div>
<div class="legend-title" style="margin-top:0;">SPBU</div>
<div class="legend-item">
<div class="legend-dot" style="background:#22c55e;"></div>
<span>Beroperasi 24 Jam</span>
+14 -6
View File
@@ -14,9 +14,9 @@
.bg-pattern { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: radial-gradient(#334155 1px, transparent 1px); background-size: 40px 40px; opacity: 0.3; z-index: -1; }
.glow { position: absolute; width: 600px; height: 600px; background: radial-gradient(circle, rgba(59,130,246,0.15) 0%, rgba(15,23,42,0) 70%); top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: -1; pointer-events: none; }
header { text-align: center; margin-bottom: 60px; z-index: 10; }
header h1 { font-size: 42px; font-weight: 800; background: linear-gradient(135deg, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 16px; }
header p { font-size: 18px; color: #cbd5e1; max-width: 600px; margin: 0 auto; line-height: 1.6; }
header { text-align: center; margin-bottom: 48px; z-index: 10; padding: 0 20px; }
header h1 { font-size: clamp(32px, 5vw, 42px); font-weight: 800; background: linear-gradient(135deg, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 16px; }
header p { font-size: clamp(15px, 2.4vw, 18px); color: #cbd5e1; max-width: 640px; margin: 0 auto; line-height: 1.6; }
.cards-container { display: flex; gap: 28px; max-width: 1160px; width: 100%; padding: 0 20px; z-index: 10; flex-wrap: wrap; justify-content: center; }
@@ -48,9 +48,17 @@
.admin-link:hover { color: #f8fafc; }
@media (max-width: 768px) {
body { justify-content: flex-start; padding-top: 32px; }
.choice-card { width: 100%; }
header h1 { font-size: 32px; }
body { justify-content: flex-start; padding: 32px 0 86px; }
header { margin-bottom: 28px; }
.cards-container { gap: 18px; padding: 0 14px; }
.choice-card { width: 100%; min-width: 0; max-width: 430px; padding: 28px 22px; }
.choice-card .icon-box { width: 68px; height: 68px; font-size: 30px; margin-bottom: 18px; }
.choice-card h2 { font-size: 21px; }
.admin-link { bottom: 18px; }
}
@media (min-width: 769px) and (max-width: 1100px) {
.choice-card { width: calc(50% - 18px); }
}
</style>
</head>
+112 -43
View File
@@ -112,6 +112,14 @@
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
.legend-line { width: 24px; height: 0; border-top: 4px solid; border-radius: 999px; flex-shrink: 0; }
.legend-swatch { width: 14px; height: 14px; border-radius: 4px; flex-shrink: 0; border: 1px solid rgba(255,255,255,.3); }
.panel-toggle {
width: 100%; display: none; align-items: center; justify-content: space-between; gap: 10px;
background: transparent; border: none; color: var(--clr-text); cursor: pointer;
font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .4px; padding: 0;
}
.panel-toggle .toggle-icon { color: var(--clr-muted); transition: transform var(--transition); }
.panel-collapsed .panel-body { display: none; }
.panel-collapsed .toggle-icon { transform: rotate(-90deg); }
.popup-title { font-weight: 700; color: #0f172a; margin-bottom: 4px; }
.popup-row { font-size: 12px; line-height: 1.5; }
@@ -119,15 +127,51 @@
@media (max-width: 900px) {
.sidebar { width: var(--sidebar-collapsed); }
.sidebar-logo-text, .sidebar-item-text, .sidebar-section-title { display: none; }
.sidebar-header { padding: 16px 14px; }
header { align-items: flex-start; flex-direction: column; padding: 10px 14px; }
.header-title h1 { font-size: 16px; }
.header-breadcrumb { font-size: 12px; }
.search-panel, .filter-panel, .stats-panel, .legend-panel {
left: 12px; right: 12px; width: auto;
}
.search-panel { top: 12px; }
.filter-panel { top: 190px; }
.stats-panel { display: none; }
.stats-panel { bottom: 58px; }
.legend-panel { bottom: 12px; }
}
@media (max-width: 760px) {
.app-wrapper { height: 100dvh; }
.sidebar { width: 56px; }
.sidebar-header { padding: 12px 8px; justify-content: center; }
.sidebar-logo-icon { width: 34px; height: 34px; font-size: 12px; }
.sidebar-toggle { display: none; }
.sidebar-nav { padding: 10px 0; }
.sidebar-item { padding: 12px 14px; justify-content: center; }
.header-actions { width: 100%; justify-content: space-between; }
.search-panel { top: 10px; padding: 10px; }
.search-results { max-height: 112px; }
.filter-panel { top: 58px; padding: 10px; }
.panel-toggle { display: flex; }
.search-panel h3,
.filter-panel h3,
.stats-panel h3,
.legend-panel h3 { display: none; }
.panel-body { margin-top: 10px; }
.mode-grid { gap: 6px; margin-bottom: 8px; }
.mode-btn { padding: 7px; font-size: 11px; }
.field-label { margin: 7px 0 4px; }
.ctrl-input, .ctrl-select { padding: 8px 10px; font-size: 12px; }
.analysis-box { margin-top: 8px; padding-top: 8px; max-height: 74px; overflow-y: auto; }
.legend-panel { bottom: 10px; padding: 10px; }
.legend-row { font-size: 11px; margin-bottom: 5px; }
}
@media (max-width: 420px) {
.header-breadcrumb { display: none; }
.count-pill, .refresh-btn { font-size: 12px; padding: 5px 9px; }
.filter-panel { top: 56px; }
}
</style>
</head>
<body>
@@ -176,67 +220,79 @@
<div id="map"></div>
<div class="panel search-panel">
<button class="panel-toggle" type="button" data-panel-toggle="search-panel"><span>Pencarian</span><span class="toggle-icon"></span></button>
<h3>Pencarian</h3>
<input id="search-input" class="ctrl-input" type="text" placeholder="Cari nama jalan atau pemilik parsil..." autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="panel-body">
<input id="search-input" class="ctrl-input" type="text" placeholder="Cari nama jalan atau pemilik parsil..." autocomplete="off">
<div id="search-results" class="search-results"></div>
</div>
</div>
<div class="panel filter-panel">
<button class="panel-toggle" type="button" data-panel-toggle="filter-panel"><span>Filter & Analisis</span><span class="toggle-icon"></span></button>
<h3>Filter & Analisis</h3>
<div class="mode-grid">
<button class="mode-btn active" data-mode="all">Semua</button>
<button class="mode-btn" data-mode="jalan">Jalan</button>
<button class="mode-btn" data-mode="parsil">Parsil</button>
<button class="mode-btn" data-mode="analysis">Akses</button>
</div>
<div class="panel-body">
<div class="mode-grid">
<button class="mode-btn active" data-mode="all">Semua</button>
<button class="mode-btn" data-mode="jalan">Jalan</button>
<button class="mode-btn" data-mode="parsil">Parsil</button>
<button class="mode-btn" data-mode="analysis">Akses</button>
</div>
<label class="field-label" for="filter-jalan">Status Jalan</label>
<select id="filter-jalan" class="ctrl-select">
<option value="all">Semua status</option>
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
<label class="field-label" for="filter-jalan">Status Jalan</label>
<select id="filter-jalan" class="ctrl-select">
<option value="all">Semua status</option>
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
<label class="field-label" for="filter-parsil">Status Parsil</label>
<select id="filter-parsil" class="ctrl-select">
<option value="all">Semua status</option>
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
<label class="field-label" for="filter-parsil">Status Parsil</label>
<select id="filter-parsil" class="ctrl-select">
<option value="all">Semua status</option>
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
<label class="field-label" for="filter-access">Akses Parsil ke Jalan</label>
<select id="filter-access" class="ctrl-select">
<option value="all">Semua jarak</option>
<option value="near">Dekat (&lt; 50 m)</option>
<option value="medium">Sedang (50-200 m)</option>
<option value="far">Jauh (&gt; 200 m)</option>
</select>
<label class="field-label" for="filter-access">Akses Parsil ke Jalan</label>
<select id="filter-access" class="ctrl-select">
<option value="all">Semua jarak</option>
<option value="near">Dekat (&lt; 50 m)</option>
<option value="medium">Sedang (50-200 m)</option>
<option value="far">Jauh (&gt; 200 m)</option>
</select>
<div id="analysis-box" class="analysis-box">
<span class="analysis-empty">Klik satu parsil untuk melihat jalan terdekat dan estimasi jaraknya.</span>
<div id="analysis-box" class="analysis-box">
<span class="analysis-empty">Klik satu parsil untuk melihat jalan terdekat dan estimasi jaraknya.</span>
</div>
</div>
</div>
<div class="panel stats-panel">
<button class="panel-toggle" type="button" data-panel-toggle="stats-panel"><span>Statistik</span><span class="toggle-icon"></span></button>
<h3>Statistik Data</h3>
<div class="stats-grid">
<div class="stat-card"><div id="stat-jalan" class="stat-value">0</div><div class="stat-label">Total jalan tampil</div></div>
<div class="stat-card"><div id="stat-parsil" class="stat-value">0</div><div class="stat-label">Total parsil tampil</div></div>
<div class="stat-card"><div id="stat-panjang" class="stat-value">0 km</div><div class="stat-label">Akumulasi panjang jalan</div></div>
<div class="stat-card"><div id="stat-luas" class="stat-value">0 m2</div><div class="stat-label">Akumulasi luas parsil</div></div>
<div class="panel-body">
<div class="stats-grid">
<div class="stat-card"><div id="stat-jalan" class="stat-value">0</div><div class="stat-label">Total jalan tampil</div></div>
<div class="stat-card"><div id="stat-parsil" class="stat-value">0</div><div class="stat-label">Total parsil tampil</div></div>
<div class="stat-card"><div id="stat-panjang" class="stat-value">0 km</div><div class="stat-label">Akumulasi panjang jalan</div></div>
<div class="stat-card"><div id="stat-luas" class="stat-value">0 m2</div><div class="stat-label">Akumulasi luas parsil</div></div>
</div>
</div>
</div>
<div class="panel legend-panel">
<button class="panel-toggle" type="button" data-panel-toggle="legend-panel"><span>Legenda</span><span class="toggle-icon"></span></button>
<h3>Legenda</h3>
<div class="legend-row"><div class="legend-line" style="border-color:#ef4444;"></div> Jalan Nasional</div>
<div class="legend-row"><div class="legend-line" style="border-color:#3b82f6;"></div> Jalan Provinsi</div>
<div class="legend-row"><div class="legend-line" style="border-color:#22c55e;"></div> Jalan Kabupaten</div>
<div class="legend-row"><div class="legend-swatch" style="background:#14b8a6;"></div> Parsil SHM</div>
<div class="legend-row"><div class="legend-swatch" style="background:#f59e0b;"></div> Parsil HGB/HGU/HP</div>
<div class="panel-body">
<div class="legend-row"><div class="legend-line" style="border-color:#ef4444;"></div> Jalan Nasional</div>
<div class="legend-row"><div class="legend-line" style="border-color:#3b82f6;"></div> Jalan Provinsi</div>
<div class="legend-row"><div class="legend-line" style="border-color:#22c55e;"></div> Jalan Kabupaten</div>
<div class="legend-row"><div class="legend-swatch" style="background:#14b8a6;"></div> Parsil SHM</div>
<div class="legend-row"><div class="legend-swatch" style="background:#f59e0b;"></div> Parsil HGB/HGU/HP</div>
</div>
</div>
</section>
</main>
@@ -249,6 +305,19 @@
document.getElementById('sidebar').classList.toggle('collapsed');
});
function setupCollapsiblePanels() {
document.querySelectorAll('[data-panel-toggle]').forEach(btn => {
const panel = btn.closest('.panel');
btn.addEventListener('click', () => panel.classList.toggle('panel-collapsed'));
});
if (window.matchMedia('(max-width: 760px)').matches) {
document.querySelectorAll('.search-panel, .filter-panel, .stats-panel, .legend-panel').forEach(panel => {
panel.classList.add('panel-collapsed');
});
}
}
setupCollapsiblePanels();
const map = L.map('map').setView([-0.026, 109.342], 13);
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 });
const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 });
+2
View File
@@ -894,6 +894,7 @@ function initMap() {
L.control.layers(baseMaps).addTo(map);
// Initialize Geoman Controls (only if Geoman library is loaded)
/*
if (map.pm && typeof map.pm.addControls === 'function') {
map.pm.addControls({
position: 'topleft',
@@ -906,6 +907,7 @@ function initMap() {
removalMode: false
});
}
*/
// ALWAYS set mapInstance — regardless of whether Geoman loaded
window.appState.mapInstance = map;
+9 -1
View File
@@ -7,7 +7,7 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; display: flex; align-items: center; justify-content: center; height: 100vh; }
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
.login-container { background: #1e293b; padding: 40px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); width: 100%; max-width: 400px; position: relative; }
.login-container h2 { margin-bottom: 24px; text-align: center; color: #3b82f6; }
.form-group { margin-bottom: 20px; }
@@ -26,6 +26,14 @@
.form-section { display: none; }
.form-section.active { display: block; }
@media (max-width: 480px) {
body { align-items: flex-start; padding: 18px 14px; }
.login-container { padding: 28px 20px; border-radius: 10px; }
.login-container h2 { font-size: 22px; margin-bottom: 20px; }
.form-group { margin-bottom: 16px; }
.form-group input, .btn { min-height: 44px; }
}
</style>
</head>
<body>
+83 -15
View File
@@ -135,6 +135,14 @@
.legend-panel h3 { font-size: 11px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.panel-toggle {
width: 100%; display: none; align-items: center; justify-content: space-between; gap: 10px;
background: transparent; border: none; color: var(--clr-text); cursor: pointer;
font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .4px; padding: 0;
}
.panel-toggle .toggle-icon { color: var(--clr-muted); transition: transform var(--transition); }
.panel-collapsed .panel-body { display: none; }
.panel-collapsed .toggle-icon { transform: rotate(-90deg); }
/* ---- Popup Slider ---- */
.popup-slider-wrap { margin-top: 10px; }
@@ -172,6 +180,48 @@
0% { transform: scale(0.5); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
@media (max-width: 1024px) {
.sidebar { width: var(--sidebar-collapsed); }
.sidebar-logo-text,
.sidebar-item-text,
.sidebar-section-title,
.sidebar-item-user { display: none; }
.sidebar-header { padding: 16px 14px; }
header { height: auto; min-height: 64px; padding: 10px 14px; }
.header-title h1 { font-size: 16px; }
.header-breadcrumb { font-size: 12px; }
}
@media (max-width: 760px) {
.app-wrapper { height: 100dvh; }
.sidebar { width: 56px; }
.sidebar-header { padding: 12px 8px; justify-content: center; }
.sidebar-logo-icon { width: 34px; height: 34px; font-size: 12px; }
.sidebar-toggle { display: none; }
.sidebar-nav { padding: 10px 0; }
.sidebar-item { padding: 12px 14px; justify-content: center; }
.search-panel,
.legend-panel {
left: 10px; right: 10px; width: auto;
}
.search-panel { top: 10px; padding: 10px; }
.search-results { max-height: 105px; }
.search-panel h3 { font-size: 11px; margin-bottom: 8px; }
.panel-toggle { display: flex; }
.search-panel h3,
.legend-panel h3 { display: none; }
.panel-body { margin-top: 10px; }
.legend-panel { bottom: 10px; padding: 10px; }
.legend-panel h3 { margin-bottom: 7px; }
.legend-row { font-size: 11px; margin-bottom: 5px; }
}
@media (max-width: 420px) {
.header-breadcrumb { display: none; }
.search-panel { top: 8px; }
.ctrl-input { padding: 8px 10px; font-size: 12px; }
}
</style>
</head>
<body>
@@ -222,28 +272,33 @@
<!-- Search Panel (Left) -->
<div class="search-panel">
<button class="panel-toggle" type="button" data-panel-toggle="search-panel"><span>Pencarian</span><span class="toggle-icon"></span></button>
<h3><i class="fa-solid fa-magnifying-glass" style="color:var(--clr-primary)"></i> Pencarian</h3>
<div style="font-size:11px; font-weight:600; color:#94a3b8; margin-bottom:4px;">RUMAH IBADAH</div>
<div class="search-input-wrap">
<input id="search-ibadah" class="ctrl-input" type="text" placeholder="Cari Rumah Ibadah..." autocomplete="off">
</div>
<div id="results-ibadah" class="search-results"></div>
<div class="panel-body">
<div style="font-size:11px; font-weight:600; color:#94a3b8; margin-bottom:4px;">RUMAH IBADAH</div>
<div class="search-input-wrap">
<input id="search-ibadah" class="ctrl-input" type="text" placeholder="Cari Rumah Ibadah..." autocomplete="off">
</div>
<div id="results-ibadah" class="search-results"></div>
<div style="font-size:11px; font-weight:600; color:#94a3b8; margin-top:8px; margin-bottom:4px;">PENDUDUK MISKIN</div>
<div class="search-input-wrap">
<input id="search-miskin" class="ctrl-input" type="text" placeholder="Cari Nama KK Penduduk Miskin..." autocomplete="off">
<div style="font-size:11px; font-weight:600; color:#94a3b8; margin-top:8px; margin-bottom:4px;">PENDUDUK MISKIN</div>
<div class="search-input-wrap">
<input id="search-miskin" class="ctrl-input" type="text" placeholder="Cari Nama KK Penduduk Miskin..." autocomplete="off">
</div>
<div id="results-miskin" class="search-results"></div>
</div>
<div id="results-miskin" class="search-results"></div>
</div>
<div class="legend-panel">
<button class="panel-toggle" type="button" data-panel-toggle="legend-panel"><span>Legenda</span><span class="toggle-icon"></span></button>
<h3>Legenda</h3>
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;"></div> Rumah Ibadah</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;"></div> Penduduk Miskin (Dalam Radius)</div>
<div class="legend-row"><div class="legend-dot" style="background:#1e293b; border: 1px solid #64748b;"></div> Penduduk Miskin (Luar Radius)</div>
<hr style="border-color:#334155; margin: 8px 0;">
<div style="font-size:11px; color: var(--clr-muted);">💡 Klik Rumah Ibadah untuk atur radius jangkauan</div>
<div class="panel-body">
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;"></div> Rumah Ibadah</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;"></div> Penduduk Miskin (Dalam Radius)</div>
<div class="legend-row"><div class="legend-dot" style="background:#1e293b; border: 1px solid #64748b;"></div> Penduduk Miskin (Luar Radius)</div>
<hr style="border-color:#334155; margin: 8px 0;">
<div style="font-size:11px; color: var(--clr-muted);">💡 Klik Rumah Ibadah untuk atur radius jangkauan</div>
</div>
</div>
</div>
</div>
@@ -257,6 +312,19 @@
document.getElementById('sidebar').classList.toggle('collapsed');
});
function setupCollapsiblePanels() {
document.querySelectorAll('[data-panel-toggle]').forEach(btn => {
const panel = btn.closest('.search-panel, .legend-panel');
btn.addEventListener('click', () => panel.classList.toggle('panel-collapsed'));
});
if (window.matchMedia('(max-width: 760px)').matches) {
document.querySelectorAll('.search-panel, .legend-panel').forEach(panel => {
panel.classList.add('panel-collapsed');
});
}
}
setupCollapsiblePanels();
// Global state
let ibadahData = [], miskinData = [];
let ibadahLayer, miskinLayer, map;
+95 -12
View File
@@ -147,6 +147,14 @@
.legend-panel h3 { font-size: 11px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.panel-toggle {
width: 100%; display: none; align-items: center; justify-content: space-between; gap: 10px;
background: transparent; border: none; color: var(--clr-text); cursor: pointer;
font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .4px; padding: 0;
}
.panel-toggle .toggle-icon { color: var(--clr-muted); transition: transform var(--transition); }
.panel-collapsed .panel-body { display: none; }
.panel-collapsed .toggle-icon { transform: rotate(-90deg); }
/* ---- Custom Marker ---- */
.custom-marker {
@@ -175,6 +183,59 @@
0% { transform: scale(0.5); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
@media (max-width: 1024px) {
.sidebar { width: var(--sidebar-collapsed); }
.sidebar-logo-text,
.sidebar-item-text,
.sidebar-section-title { display: none; }
.sidebar-header { padding: 16px 14px; }
header { height: auto; min-height: 64px; padding: 10px 14px; gap: 10px; flex-wrap: wrap; }
.header-title h1 { font-size: 16px; }
.header-breadcrumb { font-size: 12px; }
}
@media (max-width: 760px) {
.app-wrapper { height: 100dvh; }
.sidebar { width: 56px; }
.sidebar-header { padding: 12px 8px; justify-content: center; }
.sidebar-logo-icon { width: 34px; height: 34px; font-size: 12px; }
.sidebar-toggle { display: none; }
.sidebar-nav { padding: 10px 0; }
.sidebar-item { padding: 12px 14px; justify-content: center; }
header { align-items: flex-start; }
header > div:last-child { width: 100%; justify-content: space-between; }
.content-area { min-height: 0; }
.search-panel,
.controls-panel,
.legend-panel {
left: 10px; right: 10px; width: auto;
}
.search-panel { top: 10px; padding: 10px; }
.search-results { max-height: 130px; }
.controls-panel { top: 142px; padding: 10px; }
.controls-panel h3,
.search-panel h3 { font-size: 11px; margin-bottom: 8px; }
.panel-toggle { display: flex; }
.search-panel h3,
.controls-panel h3,
.legend-panel h3 { display: none; }
.search-panel,
.controls-panel,
.legend-panel { padding: 10px; }
.panel-body { margin-top: 10px; }
.ctrl-btn { padding: 9px; font-size: 12px; }
.legend-panel { bottom: 10px; padding: 10px; }
.legend-panel h3 { margin-bottom: 7px; }
.legend-row { font-size: 11px; margin-bottom: 5px; }
}
@media (max-width: 420px) {
.search-panel { top: 8px; }
.controls-panel { top: 136px; }
.header-breadcrumb { display: none; }
#data-count, #btn-refresh { font-size: 12px !important; padding: 5px 9px !important; }
}
</style>
</head>
<body>
@@ -228,29 +289,38 @@
<!-- Search Panel (Left) -->
<div class="search-panel">
<button class="panel-toggle" type="button" data-panel-toggle="search-panel"><span>Cari SPBU</span><span class="toggle-icon"></span></button>
<h3><i class="fa-solid fa-magnifying-glass" style="color:var(--clr-primary)"></i> Cari SPBU</h3>
<div class="search-input-wrap">
<input id="search-spbu" class="ctrl-input" type="text" placeholder="Ketik nama atau no SPBU..." autocomplete="off">
<div class="panel-body">
<div class="search-input-wrap">
<input id="search-spbu" class="ctrl-input" type="text" placeholder="Ketik nama atau no SPBU..." autocomplete="off">
</div>
<div id="search-results" class="search-results"></div>
</div>
<div id="search-results" class="search-results"></div>
</div>
<div class="controls-panel">
<button class="panel-toggle" type="button" data-panel-toggle="controls-panel"><span>Lokasi & Rute</span><span class="toggle-icon"></span></button>
<h3>📍 Lokasi & Rute</h3>
<button id="btn-location" class="ctrl-btn">📍 Dapatkan Lokasi Saya</button>
<button id="btn-nearest" class="ctrl-btn" disabled>🎯 Cari SPBU Terdekat</button>
<div id="info-box" class="info-box">
<strong>SPBU Terdekat:</strong><br>
<span id="nearest-name">-</span><br>
<strong>Jarak:</strong> <span id="nearest-distance">-</span> km
<div class="panel-body">
<button id="btn-location" class="ctrl-btn">📍 Dapatkan Lokasi Saya</button>
<button id="btn-nearest" class="ctrl-btn" disabled>🎯 Cari SPBU Terdekat</button>
<div id="info-box" class="info-box">
<strong>SPBU Terdekat:</strong><br>
<span id="nearest-name">-</span><br>
<strong>Jarak:</strong> <span id="nearest-distance">-</span> km
</div>
</div>
</div>
<div class="legend-panel">
<button class="panel-toggle" type="button" data-panel-toggle="legend-panel"><span>Legenda</span><span class="toggle-icon"></span></button>
<h3>Legenda</h3>
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;"></div> SPBU 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#f59e0b;"></div> SPBU Tidak 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;"></div> Lokasi Anda</div>
<div class="panel-body">
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;"></div> SPBU 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#f59e0b;"></div> SPBU Tidak 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;"></div> Lokasi Anda</div>
</div>
</div>
</div>
</div>
@@ -264,6 +334,19 @@
document.getElementById('sidebar').classList.toggle('collapsed');
});
function setupCollapsiblePanels() {
document.querySelectorAll('[data-panel-toggle]').forEach(btn => {
const panel = btn.closest('.search-panel, .controls-panel, .legend-panel');
btn.addEventListener('click', () => panel.classList.toggle('panel-collapsed'));
});
if (window.matchMedia('(max-width: 760px)').matches) {
document.querySelectorAll('.search-panel, .controls-panel, .legend-panel').forEach(panel => {
panel.classList.add('panel-collapsed');
});
}
}
setupCollapsiblePanels();
// Init Map
const map = L.map('map').setView([-0.026, 109.342], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {